Must Have 코드팩토리의 플러터 프로그래밍 - 22 ~ 23
book
Must Have 코드팩토리의 플러터 프로그래밍 22 ~ 23
22장 [Project #6] 소셜 로그인과 파이어베이스 인증하기
P.690 (22.5)
테스트하기 : 최종 완성본 결과
// ... 소셜로그인 적용 부분 기록 ...
// lib/const/colors.dart
import 'package:flutter/material.dart';
const PRIMARY_COLOR = Color(0xFF0DB2B2);
const SECONDARY_COLOR = Color(0xFF335CB0);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXT_FIELD_FILL_COLOR = Colors.grey[300]!;
// lib/screen/auth_screen.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
class AuthScreen extends StatelessWidget {
const AuthScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: FractionallySizedBox(
widthFactor: 0.7,
child: Image.asset(
'assets/img/logo.png',
),
),
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: () => onGoogleLoginPress(context),
style: ElevatedButton.styleFrom(
backgroundColor: SECONDARY_COLOR,
),
child: Text('구글로 로그인'),
),
],
),
),
);
}
onGoogleLoginPress(BuildContext context) async {
GoogleSignIn googleSignIn = GoogleSignIn(
scopes: [
'email',
],
);
try {
GoogleSignInAccount? account = await googleSignIn.signIn();
final GoogleSignInAuthentication? googleAuth = await account?.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
final result = await FirebaseAuth.instance.signInWithCredential(credential);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => HomeScreen(),
),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('로그인 실패')),
);
}
}
}
// lib/main.dart
import 'package:calendar_scheduler/screen/auth_screen.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:get_it/get_it.dart';
import 'package:calendar_scheduler/provider/schedule_provider.dart';
import 'package:calendar_scheduler/repository/schedule_repository.dart';
import 'package:provider/provider.dart';
import 'package:calendar_scheduler/firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await initializeDateFormatting();
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: AuthScreen(),
),
);
}
// lib/component/schedule_bottom_sheet.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/model/schedule_model.dart';
import 'package:uuid/uuid.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ScheduleBottomSheet extends StatefulWidget {
final DateTime selectedDate;
const ScheduleBottomSheet({
required this.selectedDate,
Key? key,
}) : super(key: key);
@override
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
final GlobalKey<FormState> formKey = GlobalKey();
int? startTime;
int? endTime;
String? content;
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Form(
key: formKey,
child: 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,
onSaved: (String? val) {
startTime = int.parse(val!);
},
validator: timeValidator,
),
),
const SizedBox(width: 16.0),
Expanded(
child: CustomTextField(
label: '종료 시간',
isTime: true,
onSaved: (String? val) {
endTime = int.parse(val!);
},
validator: timeValidator,
),
),
],
),
SizedBox(height: 8.0),
Expanded(
child: CustomTextField(
label: '내용',
isTime: false,
onSaved: (String? val) {
content = val;
},
validator: contentValidator,
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => onSavePressed(context),
style: ElevatedButton.styleFrom(
primary: PRIMARY_COLOR,
),
child: Text('저장'),
),
),
],
),
),
),
),
);
}
void onSavePressed(BuildContext context) async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
final schedule = ScheduleModel(
id: Uuid().v4(),
content: content!,
date: widget.selectedDate,
startTime: startTime!,
endTime: endTime!,
);
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('다시 로그인을 해주세요.'),
),
);
Navigator.of(context).pop();
return;
}
await FirebaseFirestore.instance
.collection(
'schedule',
)
.doc(schedule.id)
.set(
{
...schedule.toJson(),
'author': user.email,
},
);
Navigator.of(context).pop();
}
}
String? timeValidator(String? val) {
if (val == null) {
return '값을 입력해주세요';
}
int? number;
try {
number = int.parse(val);
} catch (e) {
return '숫자를 입력해주세요';
}
if (number < 0 || number > 24) {
return '0시부터 24시 사이를 입력해주세요';
}
return null;
}
String? contentValidator(String? val) {
if (val == null || val.length == 0) {
return '값을 입력해주세요';
}
return null;
}
}
// lib/screen/home_screen.dart
import 'package:calendar_scheduler/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.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';
import 'package:get_it/get_it.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:provider/provider.dart';
import 'package:calendar_scheduler/provider/schedule_provider.dart';
class HomeScreen extends StatefulWidget {
@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(
selectedDate: selectedDate,
),
);
},
child: Icon(
Icons.add,
),
),
body: SafeArea(
child: Column(
children: [
MainCalendar(
selectedDate: selectedDate,
onDaySelected: (selectedDate, focusedDate) => onDaySelected(selectedDate, focusedDate, context),
),
SizedBox(height: 8.0),
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection(
'schedule',
)
.where(
'date',
isEqualTo: '${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}',
)
.where('author', isEqualTo: FirebaseAuth.instance.currentUser!.email)
.snapshots(),
builder: (context, snapshot) {
return TodayBanner(
selectedDate: selectedDate,
count: snapshot.data?.docs.length ?? 0,
);
},
),
SizedBox(height: 8.0),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection(
'schedule',
)
.where(
'date',
isEqualTo: '${selectedDate.year}${selectedDate.month.toString().padLeft(2, "0")}${selectedDate.day.toString().padLeft(2, "0")}',
)
.where('author', isEqualTo: FirebaseAuth.instance.currentUser!.email)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('일정 정보를 가져오지 못했습니다.'),
);
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
final schedules = snapshot.data!.docs
.map(
(QueryDocumentSnapshot e) => ScheduleModel.fromJson(json: (e.data() as Map<String, dynamic>)),
)
.toList();
return ListView.builder(
itemCount: schedules.length,
itemBuilder: (context, index) {
final schedule = schedules[index];
return Dismissible(
key: ObjectKey(schedule.id),
direction: DismissDirection.startToEnd,
onDismissed: (DismissDirection direction) {
FirebaseFirestore.instance.collection('schedule').doc(schedule.id).delete();
},
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
child: ScheduleCard(
startTime: schedule.startTime,
endTime: schedule.endTime,
content: schedule.content,
),
),
);
},
);
},
),
),
],
),
),
);
}
void onDaySelected(
DateTime selectedDate,
DateTime focusedDate,
BuildContext context,
) {
setState(() {
this.selectedDate = selectedDate;
});
}
}
// lib/component/today_banner.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.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: [
Expanded(
child: Text(
'${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일',
style: textStyle,
),
),
Text(
'$count개',
style: textStyle,
),
const SizedBox(width: 8.0),
GestureDetector(
onTap: () async {
await GoogleSignIn().signOut();
await FirebaseAuth.instance.signOut();
Navigator.of(context).pop();
},
child: Icon(
Icons.logout,
size: 16.0,
color: Colors.white,
),
),
],
),
),
);
}
}
23장 [Project #7] 슈파베이스 연동하기
P.726 (23.5)
테스트하기 : 최종 완성본 결과
// ... 슈파베이스 적용 부분 기록 ...
// lib/main.dart
import 'package:calendar_scheduler/firebase_options.dart';
import 'package:calendar_scheduler/screen/auth_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await Supabase.initialize(
url: 'url주소 입력',
anonKey:'키 입력',
);
await initializeDateFormatting();
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: AuthScreen(),
),
);
}
// lib/screen/auth_screen.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({Key? key}) : super(key: key);
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
String email = '';
String password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: FractionallySizedBox(
widthFactor: 0.7,
child: Image.asset(
'assets/img/logo.png',
),
),
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: () => onGoogleLoginPress(context),
style: ElevatedButton.styleFrom(
backgroundColor: SECONDARY_COLOR,
),
child: Text('구글로 로그인'),
),
],
),
),
);
}
onGoogleLoginPress(BuildContext context) async {
GoogleSignIn googleSignIn = GoogleSignIn(
scopes: [
'email',
],
clientId: '클라이언트 ID',
serverClientId: '서버 ID',
);
try {
GoogleSignInAccount? account = await googleSignIn.signIn();
final GoogleSignInAuthentication? googleAuth = await account?.authentication;
if (googleAuth == null || googleAuth.idToken == null || googleAuth.accessToken == null) {
throw Exception('로그인 실패');
}
await Supabase.instance.client.auth.signInWithIdToken(
provider: Provider.google,
idToken: googleAuth.idToken!,
accessToken: googleAuth.accessToken!,
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => HomeScreen(),
),
);
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('로그인 실패')),
);
}
}
}
// lib/component/schedule_bottom_sheet.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/model/schedule_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ScheduleBottomSheet extends StatefulWidget {
final DateTime selectedDate;
const ScheduleBottomSheet({
required this.selectedDate,
Key? key,
}) : super(key: key);
@override
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
final GlobalKey<FormState> formKey = GlobalKey();
int? startTime;
int? endTime;
String? content;
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Form(
key: formKey,
child: 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,
onSaved: (String? val) {
startTime = int.parse(val!);
},
validator: timeValidator,
),
),
const SizedBox(width: 16.0),
Expanded(
child: CustomTextField(
label: '종료 시간',
isTime: true,
onSaved: (String? val) {
endTime = int.parse(val!);
},
validator: timeValidator,
),
),
],
),
SizedBox(height: 8.0),
Expanded(
child: CustomTextField(
label: '내용',
isTime: false,
onSaved: (String? val) {
content = val;
},
validator: contentValidator,
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => onSavePressed(context),
style: ElevatedButton.styleFrom(
primary: PRIMARY_COLOR,
),
child: Text('저장'),
),
),
],
),
),
),
),
);
}
void onSavePressed(BuildContext context) async {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
final schedule = ScheduleModel(
id: Uuid().v4(),
content: content!,
date: widget.selectedDate,
startTime: startTime!,
endTime: endTime!,
);
final supabase = Supabase.instance.client;
await supabase.from('schedule').insert(
schedule.toJson(),
);
Navigator.of(context).pop();
}
}
String? timeValidator(String? val) {
if (val == null) {
return '값을 입력해주세요';
}
int? number;
try {
number = int.parse(val);
} catch (e) {
return '숫자를 입력해주세요';
}
if (number < 0 || number > 24) {
return '0시부터 24시 사이를 입력해주세요';
}
return null;
}
String? contentValidator(String? val) {
if (val == null || val.length == 0) {
return '값을 입력해주세요';
}
return null;
}
}
// lib/model/schedule_model.dart
class ScheduleModel {
final String id;
final String content;
final DateTime date;
final int startTime;
final int endTime;
ScheduleModel({
required this.id,
required this.content,
required this.date,
required this.startTime,
required this.endTime,
});
ScheduleModel.fromJson({
required Map<String, dynamic> json,
}) : id = json['id'],
content = json['content'],
date = DateTime.parse(json['date']),
startTime = json['start_time'],
endTime = json['end_time'];
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'date':
'${date.year}${date.month.toString().padLeft(2, '0')}${date.day.toString().padLeft(2, '0')}',
'start_time': startTime,
'end_time': endTime,
};
}
ScheduleModel copyWith({
String? id,
String? content,
DateTime? date,
int? startTime,
int? endTime,
}) {
return ScheduleModel(
id: id ?? this.id,
content: content ?? this.content,
date: date ?? this.date,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
);
}
}
// lib/screen/home_screen.dart
import 'package:calendar_scheduler/model/schedule_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.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';
import 'package:get_it/get_it.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:provider/provider.dart';
import 'package:calendar_scheduler/provider/schedule_provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class HomeScreen extends StatefulWidget {
@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) {
final future = Supabase.instance.client.from('schedule').select<List<Map<String, dynamic>>>().eq('date',
'${selectedDate.year}${selectedDate.month.toString().padLeft(2, '0')}${selectedDate.day.toString().padLeft(2, '0')}');
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: PRIMARY_COLOR,
onPressed: () async {
await showModalBottomSheet(
context: context,
isDismissible: true,
isScrollControlled: true,
builder: (_) => ScheduleBottomSheet(
selectedDate: selectedDate,
),
);
setState(() {});
},
child: Icon(
Icons.add,
),
),
body: SafeArea(
child: Column(
children: [
MainCalendar(
selectedDate: selectedDate,
onDaySelected: (selectedDate, focusedDate) => onDaySelected(selectedDate, focusedDate, context),
),
SizedBox(height: 8.0),
FutureBuilder<List<Map<String, dynamic>>>(
future: future,
builder: (context, snapshot) {
return TodayBanner(
selectedDate: selectedDate,
count: snapshot.data?.length ?? 0,
);
},
),
SizedBox(height: 8.0),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('일정 정보를 가져오지 못했습니다.'),
);
}
if (snapshot.connectionState == ConnectionState.waiting || !snapshot.hasData) {
return Container();
}
final schedules = snapshot.data!
.map(
(e) => ScheduleModel.fromJson(json: e),
)
.toList();
return ListView.builder(
itemCount: schedules.length,
itemBuilder: (context, index) {
final schedule = schedules[index];
return Dismissible(
key: ObjectKey(schedule.id),
direction: DismissDirection.startToEnd,
onDismissed: (DismissDirection direction) async{
await Supabase.instance.client.from('schedule').delete().match({
'id': schedule.id,
});
setState(() {});
},
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
child: ScheduleCard(
startTime: schedule.startTime,
endTime: schedule.endTime,
content: schedule.content,
),
),
);
},
);
},
),
),
],
),
),
);
}
void onDaySelected(
DateTime selectedDate,
DateTime focusedDate,
BuildContext context,
) {
setState(() {
this.selectedDate = selectedDate;
});
}
}
// lib/component/today_banner.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.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: [
Expanded(
child: Text(
'${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일',
style: textStyle,
),
),
Text(
'$count개',
style: textStyle,
),
const SizedBox(width: 8.0),
Row(
children: [
GestureDetector(
onTap: () async {
await Supabase.instance.client.auth.signOut();
Navigator.of(context).pop();
},
child: Icon(
Icons.logout,
color: Colors.white,
size: 16.0,
),
),
],
),
],
),
),
);
}
}
참고
이 글은 골든래빗 《Must Have 코드팩토리의 플러터 프로그래밍 2판》의 스터디 내용 입니다.
스터디
Q.
A.