Must Have 코드팩토리의 플러터 프로그래밍 - 16 ~ 17
book
Must Have 코드팩토리의 플러터 프로그래밍 16 ~ 17
16장 [Project] 코팩튜브
P.464 (16.4.5)
새로고침 기능 구현하기 : RefreshIndicator 위젯을 사용하면 리스트를 당길 때, 새로고침이 가능
P.466 (16.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'package:cf_tube/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:cf_tube/component/custom_youtube_player.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:cf_tube/repository/youtube_repository.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
centerTitle: true,
title: Text(
'코팩튜브',
),
backgroundColor: Colors.black,
),
body: FutureBuilder<List<VideoModel>>(
future: YoutubeRepository.getVideos(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
),
);
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return RefreshIndicator(
onRefresh: () async {
setState(() {});
},
child: ListView(
physics: BouncingScrollPhysics(),
children: snapshot.data!
.map((e) => CustomYoutubePlayer(videoModel: e))
.toList(),
),
);
},
),
);
}
}
// lib/component/custom_youtube_player.dart
import 'package:flutter/material.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class CustomYoutubePlayer extends StatefulWidget {
final VideoModel videoModel;
const CustomYoutubePlayer({
required this.videoModel,
Key? key,
}) : super(key: key);
@override
State<CustomYoutubePlayer> createState() => _CustomYoutubePlayerState();
}
class _CustomYoutubePlayerState extends State<CustomYoutubePlayer> {
YoutubePlayerController? controller;
@override
void initState() {
super.initState();
controller = YoutubePlayerController(
initialVideoId: widget.videoModel.id,
flags: YoutubePlayerFlags(
autoPlay: false,
),
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
YoutubePlayer(
controller: controller!,
showVideoProgressIndicator: true,
),
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.videoModel.title,
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(height: 16.0),
],
);
}
@override
void dispose() {
super.dispose();
controller!.dispose();
}
}
// lib/const/api.dart
const API_KEY = '개인키입력';
const YOUTUBE_API_BASE_URL = 'https://youtube.googleapis.com/youtube/v3/search';
const CF_CHANNEL_ID = 'UCxZ2AlaT0hOmxzZVbF_j_Sw';
// lib/model/video_model.dart
class VideoModel {
final String id;
final String title;
VideoModel({
required this.id,
required this.title,
});
}
// lib/repository/youtube_repository.dart
import 'package:cf_tube/const/api.dart';
import 'package:dio/dio.dart';
import 'package:cf_tube/model/video_model.dart';
class YoutubeRepository {
static Future<List<VideoModel>> getVideos() async {
final response = await Dio().get(
YOUTUBE_API_BASE_URL,
queryParameters: {
'channelId': CF_CHANNEL_ID,
'maxResults': 50,
'key': API_KEY,
'part': 'snippet',
'order': 'date',
},
);
final listWithData = response.data['items'].where(
(item) =>
item?['id']?['videoId'] != null && item?['snippet']?['title'] != null,
);
return listWithData
.map<VideoModel>(
(item) => VideoModel(
id: item['id']['videoId'],
title: item['snippet']['title'],
),
)
.toList();
}
}
17장 [Project #1] 일정 관리 앱 만들기
P.505 (17.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting();
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:calendar_scheduler/component/schedule_card.dart';
import 'package:calendar_scheduler/component/today_banner.dart';
import 'package:calendar_scheduler/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler/const/colors.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime selectedDate = DateTime.utc(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: PRIMARY_COLOR,
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true,
isScrollControlled: true,
builder: (_) => ScheduleBottomSheet(),
);
},
child: Icon(
Icons.add,
),
),
body: SafeArea(
child: Column(
children: [
MainCalendar(
selectedDate: selectedDate,
onDaySelected: onDaySelected,
),
SizedBox(height: 8.0),
TodayBanner(
selectedDate: selectedDate,
count: 0,
),
SizedBox(height: 8.0),
ScheduleCard(
startTime: 12,
endTime: 14,
content: '프로그래밍 공부',
),
],
),
),
);
}
void onDaySelected(DateTime selectedDate, DateTime focusedDate){
setState(() {
this.selectedDate = selectedDate;
});
}
}
// lib/const/color.dart
import 'package:flutter/material.dart';
const PRIMARY_COLOR = Color(0xff0DB2B2);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXT_FIELD_FILL_COLOR = Colors.grey[300]!;
// lib/component/custom_text_field.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextField extends StatelessWidget {
final String label;
final bool isTime;
const CustomTextField({
required this.label,
required this.isTime,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: PRIMARY_COLOR,
fontWeight: FontWeight.w600,
),
),
Expanded(
flex: isTime ? 0 : 1,
child: TextFormField(
cursorColor: Colors.grey,
maxLines: isTime ? 1 : null,
expands: !isTime,
keyboardType: isTime ? TextInputType.number : TextInputType.multiline,
inputFormatters: isTime
? [
FilteringTextInputFormatter.digitsOnly,
]
: [],
decoration: InputDecoration(
border: InputBorder.none,
filled: true,
fillColor: Colors.grey[300],
suffixText: isTime ? '시' : null,
),
),
),
],
);
}
}
// lib/component/main_calendar.dart
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:calendar_scheduler/const/colors.dart';
class MainCalendar extends StatelessWidget {
final OnDaySelected onDaySelected;
final DateTime selectedDate;
MainCalendar({
required this.onDaySelected,
required this.selectedDate,
});
@override
Widget build(BuildContext context) {
return TableCalendar(
locale: 'ko_kr',
onDaySelected: onDaySelected,
selectedDayPredicate: (date) =>
date.year == selectedDate.year &&
date.month == selectedDate.month &&
date.day == selectedDate.day,
firstDay: DateTime(1800, 1, 1),
lastDay: DateTime(3000, 1, 1),
focusedDay: DateTime.now(),
headerStyle: HeaderStyle(
titleCentered: true,
formatButtonVisible: false,
titleTextStyle: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16.0,
),
),
calendarStyle: CalendarStyle(
isTodayHighlighted: false,
defaultDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: LIGHT_GREY_COLOR,
),
weekendDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: LIGHT_GREY_COLOR,
),
selectedDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
border: Border.all(
color: PRIMARY_COLOR,
width: 1.0,
),
),
defaultTextStyle: TextStyle(
fontWeight: FontWeight.w600,
color: DARK_GREY_COLOR,
),
weekendTextStyle: TextStyle(
fontWeight: FontWeight.w600,
color: DARK_GREY_COLOR,
),
selectedTextStyle: TextStyle(
fontWeight: FontWeight.w600,
color: PRIMARY_COLOR,
),
),
);
}
}
// lib/component/schedule_bottom_sheet.dart
import 'package:flutter/material.dart';
import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
class ScheduleBottomSheet extends StatefulWidget {
const ScheduleBottomSheet({Key? key}) : super(key: key);
@override
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return SafeArea(
child: Container(
height: MediaQuery.of(context).size.height / 2 +
bottomInset,
color: Colors.white,
child: Padding(
padding:
EdgeInsets.only(left: 8, right: 8, top: 8, bottom: bottomInset),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomTextField(
label: '시작 시간',
isTime: true,
),
),
const SizedBox(width: 16.0),
Expanded(
child: CustomTextField(
label: '종료 시간',
isTime: true,
),
),
],
),
SizedBox(height: 8.0),
Expanded(
child: CustomTextField(
label: '내용',
isTime: false,
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onSavePressed,
style: ElevatedButton.styleFrom(
primary: PRIMARY_COLOR,
),
child: Text('저장'),
),
),
],
),
),
),
);
}
void onSavePressed() {}
}
// lib/component/schedule_card.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
class ScheduleCard extends StatelessWidget {
final int startTime;
final int endTime;
final String content;
const ScheduleCard({
required this.startTime,
required this.endTime,
required this.content,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(
width: 1.0,
color: PRIMARY_COLOR,
),
borderRadius: BorderRadius.circular(8.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Time(
startTime: startTime,
endTime: endTime,
),
SizedBox(width: 16.0),
_Content(
content: content,
),
SizedBox(width: 16.0),
],
),
),
),
);
}
}
class _Time extends StatelessWidget {
final int startTime;
final int endTime;
const _Time({
required this.startTime,
required this.endTime,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontWeight: FontWeight.w600,
color: PRIMARY_COLOR,
fontSize: 16.0,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${startTime.toString().padLeft(2, '0')}:00',
style: textStyle,
),
Text(
'${endTime.toString().padLeft(2, '0')}:00',
style: textStyle.copyWith(
fontSize: 10.0,
),
),
],
);
}
}
class _Content extends StatelessWidget {
final String content;
const _Content({
required this.content,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
content,
),
);
}
}
// lib/component/today_banner.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
class TodayBanner extends StatelessWidget {
final DateTime selectedDate;
final int count;
const TodayBanner({
required this.selectedDate,
required this.count,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white,
);
return Container(
color: PRIMARY_COLOR,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일',
style: textStyle,
),
Text(
'$count개',
style: textStyle,
),
],
),
),
);
}
}
참고
이 글은 골든래빗 《Must Have 코드팩토리의 플러터 프로그래밍 2판》의 스터디 내용 입니다.
스터디
Q.
A.