Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
Tags
- changenotifierprovider
- user
- divider
- multiprovider
- snackbar
- transform
- globalkey
- enum
- Firebase
- permission
- datetime
- Camera
- platformexception
- reference
- signout
- changenotifier
- switch
- controller
- Snapshot
- setstate
- borderRadius
- swift 문법
- Stream
- consumer
- runTransaction
- Navigator
- provider
- Swift
- ListView.builder
- 문법
Archives
- Today
- Total
코딩하는 제리
[Flutter/MiniProject] (구) Netflix Clone 본문
소스코드 및 pubspec.yaml
// main.dart
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/screens/home_screen.dart';
import 'package:netflix_clone_jerry/screens/more_screen.dart';
import 'package:netflix_clone_jerry/screens/search_screen.dart';
import 'package:netflix_clone_jerry/widgets/bottom_bar.dart';
import 'screens/like_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
TabController controller;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Netflix',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.black,
accentColor: Colors.white,
),
home: DefaultTabController(
length: 4,
child: Scaffold(
body: TabBarView(
physics: NeverScrollableScrollPhysics() /* 사용자 스크롤 방지 */,
children: [
HomeScreen(),
SearchScreen(),
LikeScreen(),
MoreScreen(),
],
),
bottomNavigationBar: BottomBar(),
),
),
);
}
}
// widgets/bottom_bar.dart
import 'package:flutter/material.dart';
class BottomBar extends StatelessWidget {
@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),
),
),
],
),
),
);
}
}
// widgets/carousel_slider.dart
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/screens/detail_screen.dart';
class CarouselImage extends StatefulWidget {
final List<Movie> movies;
const CarouselImage({Key key, this.movies}) : super(key: key);
@override
_CarouselImageState createState() => _CarouselImageState();
}
class _CarouselImageState extends State<CarouselImage> {
List<Movie> movies;
List<Widget> images;
List<String> keywords;
List<bool> likes;
int _currentPage = 0;
String _currentKeyword;
@override
void initState() {
super.initState();
movies = widget.movies;
images = movies.map((e) => Image.network(e.poster)).toList();
keywords = movies.map((e) => e.keyword).toList();
likes = movies.map((e) => e.like).toList();
_currentKeyword = keywords[0];
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.all(20),
),
CarouselSlider(
items: images,
options: CarouselOptions(onPageChanged: (index, reason) {
setState(() {
_currentPage = index;
_currentKeyword = keywords[_currentPage];
});
}),
),
Container(
padding: EdgeInsets.only(top: 10, bottom: 3),
child: Text(
_currentKeyword,
style: TextStyle(fontSize: 11),
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
child: Column(
children: <Widget>[
likes[_currentPage]
? IconButton(
icon: Icon(Icons.check),
onPressed: () {
setState(() {
likes[_currentPage] = !likes[_currentPage];
movies[_currentPage].reference.updateData(
{'like': likes[_currentPage]});
});
},
)
: IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
likes[_currentPage] = !likes[_currentPage];
movies[_currentPage].reference.updateData(
{'like': likes[_currentPage]});
});
},
),
Text(
'내가 찜한 콘텐츠',
style: TextStyle(fontSize: 11),
)
],
),
),
Container(
width: 100,
padding: EdgeInsets.only(right: 10),
child: TextButton(
style: TextButton.styleFrom(backgroundColor: Colors.white),
onPressed: () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.play_arrow,
color: Colors.black,
),
Padding(
padding: EdgeInsets.all(3),
),
Text(
'재생',
style: TextStyle(color: Colors.black),
),
],
),
),
),
Container(
child: Column(
children: <Widget>[
IconButton(
icon: Icon(Icons.info_outline),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return DetailScreen(
movie: movies[_currentPage],
);
},
),
);
},
padding: EdgeInsets.only(bottom: 0),
),
Text(
'정보',
style: TextStyle(fontSize: 11),
),
],
),
),
],
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: makeIndicator(likes, _currentPage),
),
)
],
),
);
}
}
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;
}
// widgets/circle_slider.dart
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/screens/detail_screen.dart';
class CircleSlider extends StatelessWidget {
final List<Movie> movies;
const CircleSlider({Key key, this.movies}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(7),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('미리보기'),
Container(
height: 120,
child: ListView(
scrollDirection: Axis.horizontal /*좌우 스크롤*/,
children: makeCircleImages(movies, context),
),
)
],
),
);
}
}
List<Widget> makeCircleImages(List<Movie> movies, BuildContext context) {
List<Widget> results = [];
for (var i = 0; i < movies.length; i++) {
results.add(InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return DetailScreen(
movie: movies[i],
);
},
),
);
},
child: Container(
padding: EdgeInsets.only(right: 10),
child: Align(
alignment: Alignment.centerLeft,
child: CircleAvatar(
backgroundImage: NetworkImage(movies[i].poster),
radius: 48,
),
),
),
));
}
return results;
}
// widgets/box_slider.dart
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/screens/detail_screen.dart';
class BoxSlider extends StatelessWidget {
final List<Movie> movies;
const BoxSlider({Key key, this.movies}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(7),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('지금 뜨는 콘텐츠'),
Container(
height: 120,
child: ListView(
scrollDirection: Axis.horizontal,
children: makeBoxImage(movies, context),
),
)
],
),
);
}
}
List<Widget> makeBoxImage(List<Movie> movies, BuildContext context) {
List<Widget> results = [];
for (var i = 0; i < movies.length; i++) {
results.add(InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return DetailScreen(
movie: movies[i],
);
},
),
);
},
child: Container(
padding: EdgeInsets.only(right: 10),
child: Align(
alignment: Alignment.centerLeft,
child: Image.network(movies[i].poster),
),
),
));
}
return results;
}
// models/model_movie.dart
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'];
Movie.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
@override
String toString() => "Movies<$title:$keyword>";
}
// screens/home_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/widgets/box_slider.dart';
import 'package:netflix_clone_jerry/widgets/carousel_slider.dart';
import 'package:netflix_clone_jerry/widgets/circle_slider.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Firestore firestore = Firestore.instance;
Stream<QuerySnapshot> streamData;
@override
void initState() {
super.initState();
streamData = firestore.collection('movie').snapshots();
}
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('movie').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildBody(context, snapshot.data.documents);
},
);
}
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)
],
);
}
@override
Widget build(BuildContext context) {
return _fetchData(context);
}
}
class TopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 7, horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.asset(
'images/logo.png',
fit: BoxFit.contain,
height: 25,
),
Container(
padding: EdgeInsets.only(right: 1),
child: Text(
'TV 프로그램',
style: TextStyle(fontSize: 14),
),
),
Container(
padding: EdgeInsets.only(right: 1),
child: Text(
'영화',
style: TextStyle(fontSize: 14),
),
),
Container(
padding: EdgeInsets.only(right: 1),
child: Text(
'내가 찜한 콘텐츠',
style: TextStyle(fontSize: 14),
),
),
],
),
);
}
}
// screens/search_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/screens/detail_screen.dart';
class SearchScreen extends StatefulWidget {
@override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController _controller = TextEditingController();
// 검색 위젯에 커서가 있는지에 대한 상태를 가지는 위젯
FocusNode focusNode = FocusNode();
String _searchText = "";
_SearchScreenState() {
_controller.addListener(() {
setState(() {
_searchText = _controller.text;
});
});
}
// 스트림 데이터를 가져와 _buildList 호출
Widget _buildBody(BuildContext context) {
return StreamBuilder(
stream: Firestore.instance.collection('movie').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
});
}
// 검색 결과 데이터 필터링, buildListItem 호출
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
List<DocumentSnapshot> searchResults = [];
for (DocumentSnapshot d in snapshot) {
if (d.data.toString().contains(_searchText)) {
searchResults.add(d);
}
}
return Expanded(
child: GridView.count(
crossAxisCount: 3,
childAspectRatio: 1 / 1.5,
padding: EdgeInsets.all(3),
children:
searchResults.map((data) => _buildListItem(context, data)).toList(),
),
);
}
// Detail Screen 화면 생성
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final movie = Movie.fromSnapshot(data);
return InkWell(
child: Image.network(movie.poster),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return DetailScreen(
movie: movie,
);
}));
},
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
child: Column(
children: [
Container(
color: Colors.black,
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 10),
child: Row(
children: [
Expanded(
flex: 6,
child: TextField(
focusNode: focusNode,
style: TextStyle(fontSize: 15),
autofocus: true,
controller: _controller,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white12,
prefixIcon: Icon(
Icons.search,
color: Colors.white60,
size: 20,
),
suffixIcon: focusNode.hasFocus
? IconButton(
icon: Icon(
Icons.cancel,
size: 20,
),
onPressed: () {
setState(() {
_controller.clear();
_searchText = "";
});
},
)
: Container(),
hintText: '검색',
labelStyle: TextStyle(color: Colors.white),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius:
BorderRadius.all(Radius.circular(10))),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius:
BorderRadius.all(Radius.circular(10))),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius:
BorderRadius.all(Radius.circular(10))),
),
),
),
focusNode.hasFocus
? Expanded(
child: TextButton(
child: Text(
'취소',
style: TextStyle(color: Colors.white),
),
onPressed: () {
setState(() {
_controller.clear();
_searchText = "";
focusNode.unfocus();
});
},
),
)
: Expanded(
flex: 0,
child: Container(),
),
],
),
),
_buildBody(context),
],
),
),
);
}
}
// screens/like_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
import 'package:netflix_clone_jerry/screens/detail_screen.dart';
class LikeScreen extends StatefulWidget {
@override
_LikeScreenState createState() => _LikeScreenState();
}
class _LikeScreenState extends State<LikeScreen> {
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('movie')
.where('like', isEqualTo: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
});
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return Expanded(
child: GridView.count(
crossAxisCount: 3,
childAspectRatio: 1 / 1.5,
padding: EdgeInsets.all(3),
children:
snapshot.map((data) => _buildListItem(context, data)).toList(),
),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final movie = Movie.fromSnapshot(data);
return InkWell(
child: Image.network(movie.poster),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return DetailScreen(
movie: movie,
);
}));
},
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
child: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(20, 27, 20, 7),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Image.asset(
'images/logo.png',
fit: BoxFit.contain,
height: 25,
),
Container(
padding: EdgeInsets.only(left: 30),
child: Text(
'내가 찜한 콘텐츠',
style: TextStyle(fontSize: 14),
),
)
],
),
),
_buildBody(context),
],
),
),
);
}
}
// screens/more_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
class MoreScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Column(
children: [
Container(
padding: EdgeInsets.only(top: 50),
child: CircleAvatar(
backgroundColor: Colors.white,
radius: 100,
backgroundImage: AssetImage('images/codingJerry.png'),
),
),
Container(
padding: EdgeInsets.only(top: 15),
child: Text(
'JerryCho',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
Container(
// padding: EdgeInsets.all(15),
width: 140,
height: 5,
color: Colors.red,
),
Container(
padding: EdgeInsets.all(10),
child: Linkify(
text: 'https://coding-jerry.tistory.com',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
linkStyle: TextStyle(color: Colors.white),
onOpen: (link) async {
if (await canLaunch(link.url)) {
await launch(link.url);
}
},
),
),
Container(
padding: EdgeInsets.all(10),
child: TextButton(
onPressed: () {},
child: Container(
color: Colors.red,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit,
color: Colors.white,
),
SizedBox(width: 10),
Text(
'프로필 수정하기',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
],
),
),
);
}
}
// screens/detail_screen.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:netflix_clone_jerry/models/model_movie.dart';
class DetailScreen extends StatefulWidget {
final Movie movie;
const DetailScreen({Key key, this.movie}) : super(key: key);
@override
_DetailScreenState createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
bool like = false;
@override
void initState() {
super.initState();
like = widget.movie.like;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SafeArea(
child: ListView(
children: [
Stack(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(widget.movie.poster),
fit: BoxFit.cover,
),
),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
alignment: Alignment.center,
color: Colors.black.withOpacity(0.1),
child: Container(
child: Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(0, 45, 0, 10),
child: Image.network(widget.movie.poster),
height: 300,
),
Container(
padding: EdgeInsets.all(7),
child: Text(
'99% 일치 2019 15+ 시즌 1개',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13),
),
),
Container(
padding: EdgeInsets.all(7),
child: Text(
widget.movie.title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Container(
padding: EdgeInsets.all(3),
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
backgroundColor: Colors.red,
primary: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow),
Text('재생'),
],
),
),
),
Container(
padding: EdgeInsets.all(5),
child: Text(widget.movie.toString()),
),
Container(
padding: EdgeInsets.all(5),
alignment: Alignment.centerLeft,
child: Text(
'출연: 현빈, 손예진, 서지혜\n제작자: 이정효, 박지은',
style: TextStyle(
color: Colors.white60, fontSize: 12),
),
),
],
),
),
),
),
),
),
Positioned(
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0 /*그림자 제거*/,
),
),
],
),
Container(
color: Colors.black26,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
detailScreenButton(
'내가 찜한 콘텐츠', like ? Icon(Icons.check) : Icon(Icons.add)),
detailScreenButton('평가', Icon(Icons.thumb_up)),
detailScreenButton('공유', Icon(Icons.send)),
],
),
),
],
),
),
));
}
Container detailScreenButton(String title, Icon icon) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: InkWell(
onTap: () {
like = !like;
widget.movie.reference.updateData({'like': like});
},
child: Column(
children: [
icon,
Padding(
padding: EdgeInsets.all(5),
),
Text(
title,
style: TextStyle(fontSize: 11, color: Colors.white60),
),
],
),
),
);
}
}
'Flutter > MiniProject' 카테고리의 다른 글
[Flutter/MiniProject] 간단한 가계부 앱 (0) | 2021.03.26 |
---|---|
[Flutter/MiniProject] 퀴즈앱 UI (0) | 2021.03.23 |
[Flutter/MiniProject] 채팅앱 UI (0) | 2021.03.19 |
[Flutter/MiniProject] 간단한 채팅앱 UI (0) | 2021.03.17 |
[Flutter/MiniProject] AuthenticationPage (0) | 2021.03.12 |
Comments