플러터 공부/프로젝트기록

플러터 - 넷플릭스 클론

Lee_SH 2023. 6. 12. 19:55

https://github.com/qoridhc/Flutter_Project

 

GitHub - qoridhc/Flutter_Project

Contribute to qoridhc/Flutter_Project development by creating an account on GitHub.

github.com

 

플러그인 설치

  1. Firebase_Core : https://pub.dev/packages/firebase_core/install
  2. Cloud_firestore : https://pub.dev/packages/cloud_firestore
  3. Carousel_slider : https://pub.dev/packages/carousel_slider

하단 네비게이션바 만들기

import 'package:flutter/material.dart';
import 'package:mini_netflix_clone_app/screen/home_screen.dart';
import 'widget/bottom_bar.dart';

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CloneFlix',
      theme: ThemeData(
        brightness: Brightness.dark,
        primaryColor: Colors.black,
      ),
      home: DefaultTabController(
        length: 4,  // 네비게이션 바 메뉴 갯수
        child: Scaffold(
          body: TabBarView(
            physics: NeverScrollableScrollPhysics(), // 스크롤로 화면 넘기기 제한
            children: [
              HomeScreen(),
              Container(
                child: Center(
                  child: Text('search'),
                ),
              ),
              Container(
                child: Center(
                  child: Text('save'),
                ),
              ),
	    MoreScreen(),
            ],
          ),
          bottomNavigationBar: Bottom(),
        ),
      ),
    );
  }
}

DefaultTabController위젯의 child로 Scaffold를 넣어주고 body에 TabBarVIew를 넣어주고 네이게이션바를 클릭 했을때 이동할 스크린을 각각 넣어준다.

 

Scaffold의 bottomNavigationBar속성에 네이게이션바의 탭들을 TabBar 위젯에 만들어 넣어준다. TabBarView에 넣은 스크린들과 bottomNavigationBar에 넣어준 탭들이 1:1로 매칭에 된다.

 

만약 home을 누르면 homeScreen(), search를 누르면 Text(’search’)를 담은 스크린이 각각 출력되는식

네비게이션바를 커스텀해주기위해 따로 Bottom이라는 클래스로 빼서 커스텀한 TabBar를 넣어준다.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black,
      child: Container(
        height: 50,
        child: TabBar(
            labelColor: Colors.white,
            unselectedLabelColor: Colors.white60,
            indicatorColor: Colors.transparent,
            tabs: [
              Tab(
                icon: Icon(
                  Icons.home,
                  size: 18,
                ),
                child: Text(
                  '홈',
                  style: TextStyle(fontSize: 9),
                ),
              ),Tab(
                icon: Icon(
                  Icons.search,
                  size: 18,
                ),
                child: Text(
                  '검색',
                  style: TextStyle(fontSize: 9),
                ),
              ),Tab(
                icon: Icon(
                  Icons.save_alt,
                  size: 18,
                ),
                child: Text(
                  '저장한 컨텐츠',
                  style: TextStyle(fontSize: 9),
                ),
              ),Tab(
                icon: Icon(
                  Icons.list,
                  size: 18,
                ),
                child: Text(
                  '더보기',
                  style: TextStyle(fontSize: 9),
                ),
              ),
            ]),
      ),
    );
  }
}

Movie 데이터 모델링 

class Movie {
  final String title;
  final String keyword;
  final String poster;
  final bool like;

  Movie.fromMap(Map<String, dynamic> map)
      : title = map['title'],
        keyword = map['keyword'],
        poster = map['poster'],
        like = map['like'] as bool;

  @override
  String toString() => "Movie<$title:$keyword>";
}

영화 데이터를 받을 변수들을 선언하고 map형태로 전달받은 영화 데이터를 생성자를통해 저장.

class _HomeScreenState extends State<HomeScreen> {
		List<Movie> movies = [
		    Movie.fromMap(
		        {
		          'title' : '사랑의 불시착',
		          'keyword' : '사랑/로맨스/판타지',
		          'poster' : 'test_movie_1.png',
		          'like' : false
		        }
		    ),Movie.fromMap(
		        {
		          'title' : '사랑의 불시착',
		          'keyword' : '사랑/로맨스/판타지',
		          'poster' : 'test_movie_1.png',
		          'like' : false
		        }
		    ),Movie.fromMap(
		        {
		          'title' : '사랑의 불시착',
		          'keyword' : '사랑/로맨스/판타지',
		          'poster' : 'test_movie_1.png',
		          'like' : false
		        }
		    ),Movie.fromMap(
		        {
		          'title' : '사랑의 불시착',
		          'keyword' : '사랑/로맨스/판타지',
		          'poster' : 'test_movie_1.png',
		          'like' : false
		        }
		    ),
		  ];
}

데이터를 관리해야하므로 HomeScreen을 Stateful위젯으로 바꿔주고 더미영화데이터를 생성해준다. 이후에 실제 데이터를 연동할때 바꿔줌.

캐루셀 슬라이더만들기

  • 자동 스크롤 슬라이딩을 할 수 있는 위젯
class CarouselImage extends StatefulWidget {
  final List<Movie> movies;

  CarouselImage({
    required this.movies,
    Key? key,
  }) : super(key: key);

  @override
  State<CarouselImage> createState() => _CarouselImageState();
}

class _CarouselImageState extends State<CarouselImage> {
  List<Movie> movies = [];

  List<Widget> images = [];

  List<String> keywords = [];

  List<bool> likes = [];

  int _currentPage = 0;

  late String _currentKeyword;

  @override
  void initState() {
    super.initState();

    movies = widget.movies;

    images = movies.map((m) => Image.asset('./images/' + m.poster)).toList();

    keywords = movies.map((m) => m.keyword).cast<String>().toList();

    likes = movies.map((m) => m.like).cast<bool>().toList();

    _currentKeyword = keywords[0];
  }

homeScreen에서 관리하는 더미데이터(movies)를 생성자로 받아온뒤 map을 돌면서 각각 데이터들을 List안에 넣어준다.

CarouselSlider(
            items: images,
            options: CarouselOptions(
              onPageChanged: (index, reason) {
                setState(
                  () {
                    _currentPage = index;
                    _currentKeyword = keywords[_currentPage];
                  },
                );
              },
            ),
          ),
  • items : 슬라이드하면서 보여줄 이미지들을 넣어준다
  • onPageChanged : 화면을 슬라이드하여 페이지가 변경되면 실행, setState로 현재 페이지값과 키워드값을 변경된 페이지값으로 리빌드 시켜준다.

인디케이터 만들기

List<Widget> makeIndicator(List list, int _currentPage) {
  List<Widget> results = [];
  for (var i = 0; i < list.length; i++) {
    results.add(Container(
      width: 8,
      height: 8,
      margin: EdgeInsets.symmetric(vertical: 10, horizontal: 2),
      decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: _currentPage == i
              ? Color.fromRGBO(255, 255, 255, 0.9)
              : Color.fromRGBO(255, 255, 255, 0.4)),
    ));
  }

  return results;
}

현재 보고있는 페이지위치를 알려주는 인디케이터를 만들어주는 makeIndicator함수 생성

파라미터로 들어온 list의 갯수만큼 반복문을 돌면서 인디케이터를 생성해준다. 만약 현재 스크린에서 보고있는 페이지인경우 Color를 다르게줘서 현재 보고있는 페이지가 몇번째인지 알게해준다.

FireBase 연동하기

FireBase가 Flutter와 연동하기를 원하더라도 Flutter의 runApp() 이 실행되어 플러터 엔진이 초기화 되기 전까지는 접근을 할 수 없다. 따라서 플러터 코어 엔진을 먼저 실행 시켜주어야 하는데 그 코드가 바로 WidgetsFlutterBinding.ensureInitialized()

메인 메소드에서 비동기메소드를 실행하려면 WidgetsFlutterBinding.ensureInitialized()를 무조건 먼저 불러와줘야함

그다음에 Firebase 초기화 메소드 Firebase.initializeApp()실행

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

안해주면 에러발생

데이터 모델링 수정

import 'package:cloud_firestore/cloud_firestore.dart';

class Movie {
  final String title;
  final String keyword;
  final String poster;
  final bool like;
  final DocumentReference? reference;

  Movie.fromMap(Map<String, dynamic> map, {this.reference})
      : title = map['title'],
        keyword = map['keyword'],
        poster = map['poster'],
        like = map['like'] as bool;

  Movie.fromSnapshot(DocumentSnapshot snapshot)
    : this.fromMap(snapshot.data as Map<String, dynamic> , reference : snapshot.reference);

  @override
  String toString() => "Movie<$title:$keyword>";
}
  • snapshot으로 데이터를 가져올수 있도록 fromShapshot 함수 생성
  • DocumentReference : 실제 firebase firesotre에 있는 데이터 칼럼을 참조할 수 있는 링크 → CRUD기능을 간단하게 처리 가능

데이터 받아오기

class _HomeScreenState extends State<HomeScreen> {
  FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
  late Stream<QuerySnapshot> streamData;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    streamData = firebaseFirestore
        .collection('movie')
        .snapshots(); // 파이어 베이스 콘솔에서 입력한 컬렉션 이름
  }
}

FireBase에 생성해둔 movie라는 이름의 컬렉션을 불러와 Stream 변수에 저장

Widget _fetchData(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
        stream: FirebaseFirestore.instance.collection('movie').snapshots(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return LinearProgressIndicator();
          }
          return _buildBody(context, snapshot.data!.docs);
        });
  }

  Widget _buildBody(BuildContext context, List<DocumentSnapshot> snapshot) {
    List<Movie> movies = snapshot.map((e) => Movie.fromSnapshot(e)).toList();
    return ListView(
      children: [
        Stack(
          children: [
            CarouselImage(
              movies: movies,
            ),
            TopBar(),
          ],
        ),
        CircleSlider(
          movies: movies,
        ),
        BoxSlider(
          movies: movies,
        )
      ],
    );
  }

StreamBuilder를 활용하여 데이터가 들어오면 _buildBody위젯을 실행해서 Body 그려줌

_buidBody 위젯은 받아온 데이터를 List형태로 변환시킨뒤 각 위젯들에 넘겨준다

DB 데이터 업데이트 하기

InkWell(
	onTap: () {
	    setState(() {
	      like = !like;
	      widget.movie.reference!.update({'like': like});
	    });
  },

찜하기 버튼 위젯을 눌러 onTap()이 실행되면 setState()를 통해 like상태를 반대로 바꿔주고 바뀐 like값을 movie컬랙션의 like값으로 업데이트 시켜준다.

찜하기 버튼을 누르면 like 상태가 실시간으로 업데이트되는것을 볼 수 있음

링크 달기

import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';

Linkify(
  onOpen: (link) async {
    if (await canLaunchUrl(Uri.parse(link.url))) {
      await launchUrl(Uri.parse(link.url));
    }
      },
   text: "<https://github.com/qoridhc/>",
)

linkify + urlLauncher를 활용해서 깃허브 링크 구현

 

코드 출처

https://www.inflearn.com/course/flutter-netflix-clone-app