본문 바로가기

Flutter

플러터 화면 별로 프로세스를 좀 정리해보자..

아 진짜 적응 안 되네.. 

일단 내가 사용할 디렉토리 구조, 하는 일을 정리해보자..

 

pubspec.yaml 

flutter dependencies에 사용할 플러그인 버전들을 등록한다.

get: ^4.6.5
flutter_svg: ^2.0.0+1
intl: ^0.18.0

 

/main.dart 

테마를 적용하고 initialBinding, initialRoute를 설정하고 getPage 설정

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      debugShowCheckedModeBanner: false,
      title: "Youtube Clone App",
      theme: ThemeData(
        primaryColor: Colors.white,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialBinding: InitBinding(),
      initialRoute: '/',
      getPages: [
        GetPage(name: '/', page: ()=>App()),
        GetPage(name: '/detail/:videoId', page: () => YoutubeDetail()),
      ],
    );
  }
}

 

/src/app.dart

//GetView<>를 사용해서 initialBinding 에 올려둔 해당 컨트롤러를 불러와서 사용한다.
//app.dart 파일에 body와 bottom navigation을 정의하고 body 안에서 appcontroller에서 받아온
//index에 따라서 페이지전환이 되도록 한다. Obx 사용

class App extends GetView<Appcontroller> {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Obx(() {
        switch(RoutName.values[controller.currentIndex.value]){
          case RoutName.Home:
            return Home();
            break;
          case RoutName.Explore:
            return Explore();
            break;
          case RoutName.Add:
            //bottom sheet
            break;
          case RoutName.Subs:
            return Subscribe();
            break;
          case RoutName.Library:
            return Library();
            break;
        }
        return Container();
      }),
      bottomNavigationBar: Obx(
        () => BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          currentIndex: controller.currentIndex.value,
          // 반응형 인덱스 정보를 반영하기 위해 Obx로 둘러 싸고 이렇게 변경
          showSelectedLabels: true,
          selectedItemColor: Colors.black,
          onTap: controller.changePageIndex,
          // 상단 상속 받는 클래스를 GetView<Appcontroller>로 바꾸고 onTap 을 이렇게 고친다.
          items: [
            BottomNavigationBarItem(
                icon: SvgPicture.asset('assets/svg/icons/home_off.svg'),
                activeIcon: SvgPicture.asset('assets/svg/icons/home_on.svg'),
                label: '홈'),
            BottomNavigationBarItem(
                icon: SvgPicture.asset(
                  'assets/svg/icons/compass_off.svg',
                  width: 22,
                ),
                activeIcon: SvgPicture.asset(
                  'assets/svg/icons/compass_on.svg',
                  width: 22,
                ),
                label: '탐색'),
            BottomNavigationBarItem(
              icon: Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: SvgPicture.asset(
                  'assets/svg/icons/plus.svg',
                  width: 35,
                ),
              ),
              label: '',
            ),
            BottomNavigationBarItem(
                icon: SvgPicture.asset('assets/svg/icons/subs_off.svg'),
                activeIcon: SvgPicture.asset('assets/svg/icons/subs_on.svg'),
                label: '구독'),
            BottomNavigationBarItem(
                icon: SvgPicture.asset('assets/svg/icons/library_off.svg'),
                activeIcon: SvgPicture.asset('assets/svg/icons/library_on.svg'),
                label: '보관함'),
          ],
        ),
      ),
    );
  }
}

 

 

/src/bindings/ 

앱에서 주로 사용할 컨트롤러를 올려둘 바인딩 파일을 올려둔다.

// implements로 인터페이스를 상속 받는데, implements의 경우 꼭 @override로 재정의 해야 함

class InitBinding implements Bindings { 
  @override
  void dependencies() {
    Get.put(YoutubeRepository(), permanent: true); // 계속 사용할 컨트롤러는 permanent true를 준다
    Get.put(Appcontroller());
  }

}

 

/src/pages/

각각의 페이지들 파일이 있는 곳

바인딩파일에 등록하지 않고 해당 페이지에서 사용해야 하는 컨트롤러를 Get.put으로 등록한다.

컴포넌트에 만들어둔 위젯들을 불러온다.

 

class Home extends StatelessWidget {
  Home({Key? key}) : super(key: key);

  final HomeController controller = Get.put(HomeController()); //페이지 뜰 때 홈컨트롤러 등록

  @override
  Widget build(BuildContext context) {
    // 앱바가 스크롤 되어서 위로 가면 사라지고 내려오면 나타나는 ui
    return SafeArea(
      child: Obx(() => CustomScrollView(
        slivers: [
          SliverAppBar(
            title: CustomAppBar(),
            // 이 부분이 내려갈 때 앱바 나오게 하는 부분
            floating: true,
            snap: true,
            // 이 부분이 내려갈 때 앱바 나오게 하는 부분
            backgroundColor: Colors.white,
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return GestureDetector(
                  onTap: (){
                    Get.toNamed('/detail/239587');
                    print('detail');
                  },
                  child: VideoWidget(
                    video: controller.youtubeResult.value.items[index] // 받은 비디오 데이타를 넘겨준다.
                  ),
                );
              },
              childCount: controller.youtubeResult.value.items==null ? 0 : controller.youtubeResult.value.items.length, //items거 널이면 0을 넣어준다.
             // childCount: 1000,
            ),
          ),
        ],
      ),
      ),
    );
  }
}

 

 

/src/controller/ 

페이지나 위젯에 변경되는 함수나 클래스를 작성한다.

// 바텀네비게이션에서 누른 index 값에 따라서 페이지를 이동하게 하는 예

enum RoutName {Home, Explore, Add, Subs, Library}

class Appcontroller extends GetxService {
  static Appcontroller get to => Get.find();
  RxInt currentIndex = 0.obs;

  void changePageIndex(int index){
    if (index==2){
      _showBottomSheet();
    }else{
      currentIndex(index);
    }
  }

  void _showBottomSheet() {
    Get.bottomSheet(YoutubeBottomSheet());
  }
}

 

 

/src/components/

각각의 페이지 안에서 사용할 각각의 위젯 덩어리들을 정의 / 위젯을 꾸미고 api로 데이터를 불러오고..

class YoutubeDetail extends StatelessWidget {
  const YoutubeDetail({Key? key}) : super(key: key);

  Widget _titleZone() {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('개발하는 남자 유튜브 영상 더보기', style: TextStyle(fontSize: 15)),
          Row(
            children: [
              Text(
                '조회수 1000회',
                style: TextStyle(
                    fontSize: 13, color: Colors.black.withOpacity(0.5)),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _descriptionZone(){
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30),
      child: Text('안녕하세요. 개발하는 남자 개남입니다.', style: TextStyle(fontSize: 14), ),
    );
  }

  Widget _buttonOne(String iconPath, String text){
        return Column(
          children: [
            Column(
              children: [
                SvgPicture.asset('assets/svg/icons/$iconPath.svg'),
                Text(text),
              ],
            ),
          ],
        );
  }

  Widget _buttonZone(){
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        _buttonOne('like', '1000'),
        _buttonOne('dislike', '0'),
        _buttonOne('share', '공유'),
        _buttonOne('save', '저장'),
      ],
    );
  }

  Widget get line => Container(
    height: 1,
    color: Colors.black.withOpacity(0.1),
  );

  Widget _ownerZone(){
    return Container(
      padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
      child: Row(
        children: [
          CircleAvatar(
            radius: 30,
            backgroundColor: Colors.grey.withOpacity(0.5),
            backgroundImage: Image.network('https://yigam.co.kr/img/logo_210517d.jpg').image,
          ),
          SizedBox(width: 15,),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Text('개발하는 남자', style: TextStyle(fontSize: 15),),
                Text('구독자 10000', style: TextStyle(fontSize: 14, color: Colors.black.withOpacity(0.6)), ),
              ],
            ),
          ),
          GestureDetector(
            child: Text('구독', style: TextStyle(color: Colors.red, fontSize: 16),           ),
          )
        ],
      ),
    );

  }

  Widget _description() {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _titleZone(),
          Container(
            height: 1,
            color: Colors.black.withOpacity(0.1),
          ),
          _descriptionZone(),
          _buttonZone(),
          SizedBox(
            height: 20,
          ),
          line,
          _ownerZone(),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          Container(
            height: 250,
            color: Colors.grey.withOpacity(0.5),
          ),
          Expanded(
            child: _description(),
          )
        ],
      ),
    );
  }
}

 

 

/src/models/

api에서 받아온 데이타를 jSon 형태로 처리하게 하는 파일들을 모아두는 곳 

불러온 데이타는 jSon to Dart를 통해서 자동으로 클래스파일을 만든다.

사용할 item을 따로 map 으로 변형해서 repository에서 불러와서 사용한다.

 

//json to dart로 변환한 파일 임포트
import 'package:youtube/src/models/video.dart';

// repository에서 사용할 수 있게 가공

class YoutubeVideoResult {
  int totalResults;
  int resultsPerPage;
  String nextPagetoken;
  List<Video> items;

  YoutubeVideoResult({required this.totalResults, required this.resultsPerPage, required this.nextPagetoken, required this.items });

  // 아직 잘 모르겠지만, factory를 사용했다. 음..
  factory YoutubeVideoResult.fromJson(Map<String, dynamic> json) =>
      YoutubeVideoResult(
        totalResults: json["pageInfo"]["totalResults"],
        resultsPerPage: json["pageInfo"]["resultsPerPage"],
        nextPagetoken: json["pageInfo"]["nextPagetoken"] ?? "",
        items: List<Video>.from(json["items"].map((data)=>Video.fromJson(data)))
      );

}

 

 

 

/src/repositories/

GetConnect, httpClient를 통해서 api로 데이타를 불러와서 models에 가공한 값을 돌려주는 클래스

 

import 'package:youtube/src/models/youtube_video_result.dart';

class YoutubeRepository extends GetConnect {
  static YoutubeRepository get to => Get.find();

  @override
  void onInit(){
    httpClient.baseUrl = "https://www.googleapis.com";
    super.onInit();
  }

  Future<YoutubeVideoResult?> loadVideos() async {
    String url = "/youtube/v3/search?part=snippet&maxResults=10&order=date&type=video&videoDefinition=high&key=AIzaSyAFRAvFot5LCgUGdWsBiHg8SPQO-wibUNA&channelId=UC8Au4X76OUpaTFMTnptJfew";
    final response = await get(url);
    if(response.status.hasError){
      return Future.error(response.statusText as Object);
    }else{
      if(response.body["items"]!=null && response.body["items"].length>0){
        return YoutubeVideoResult.fromJson(response.body);
      }
     // print(response.body['items']);
    }
  }


}