해당 강의를 학습하면서 정리한 글입니다. (강력추천)
https://www.udemy.com/course/flutter-provider-essential-korean/


ChangeNotifier를 이용해서 유저값을 읽어서 유저가 null이면 unauthenticated, null이 아니면 authenticated state를 만들어서 Listener들에게 알려줌.proxyProvider 형태가 되야함. (user stream이 변할때마다 update 해야하므로)Object 타입을 Object?로 변경Object 타입을 Object?로 변경// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:equatable/equatable.dart';
import 'package:firebase_auth/firebase_auth.dart' as fbAuth;
enum AuthStatus {
unknown, //인증상태 알 수 없음. 앱을 처음 시작했거나 리부팅했을때.
authenticated,
unauthenticated,
}
class AuthState extends Equatable {
final AuthStatus authStatus;
final fbAuth.User? user;
AuthState({
required this.authStatus,
this.user,
});
factory AuthState.unknown() {
return AuthState(authStatus: AuthStatus.unknown);
}
List<Object?> get props => [authStatus, user];
bool get stringify => true;
AuthState copyWith({
AuthStatus? authStatus,
fbAuth.User? user,
}) {
return AuthState(
authStatus: authStatus ?? this.authStatus,
user: user ?? this.user,
);
}
}AuthState.unknown(); , 외부에서 state에 접근 가능하게 get 작성import 'package:firebase_auth/firebase_auth.dart' as fbAuth;
import 'package:flutter/material.dart';
import '../repositories/auth_repository.dart';
import 'auth_state.dart';
class AuthProvider with ChangeNotifier {
AuthState _state = AuthState.unknown();
AuthState get state => _state;
final AuthRepository authRepository;
AuthProvider({
required this.authRepository,
});
void update(fbAuth.User? user) {
if (user != null) {
_state = _state.copyWith(
authStatus: AuthStatus.authenticated,
user: user,
);
} else {
_state = _state.copyWith(authStatus: AuthStatus.unauthenticated);
}
print('authState: $_state');
notifyListeners();
}
void signout() async {
await authRepository.signout();
}
}...
import 'package:firebase_auth/firebase_auth.dart' as fbAuth;
...
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthRepository>(
create: (context) => AuthRepository(
firebaseFirestore: FirebaseFirestore.instance,
firebaseAuth: fbAuth.FirebaseAuth.instance,
),
),
StreamProvider<fbAuth.User?>(
create: (context) => context.read<AuthRepository>().user,
initialData: null,
),
**ChangeNotifierProxyProvider**<fbAuth.User?, AuthProvider>(
create: (context) => AuthProvider(
authRepository: context.read<AuthRepository>(),
),
update: (BuildContext context, fbAuth.User? userStream,
AuthProvider? authProvider) =>
authProvider!..update(userStream),
),
],
child: MaterialApp(
title: 'Auth Provider',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashPage(),
routes: {
SignupPage.routeName: (context) => SignupPage(),
SigninPage.routeName: (context) => SigninPage(),
HomePage.routeName: (context) => HomePage(),
},
),
);
}
}addPostFrameCallback을 이용해서 현재 진행되는 빌드 작업이 다 끝난 후 호출되도록 스케줄링.addPostFrameCallback : UI에 영향을 미치는 액션의 실행을 현재 프레임 이후로 지연시킴.import 'package:fb_auth_provider/pages/home_page.dart';
import 'package:fb_auth_provider/pages/signin_page.dart';
import 'package:fb_auth_provider/providers/auth/auth_state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth/auth_provider.dart';
class SplashPage extends StatelessWidget {
const SplashPage({Key? key}) : super(key: key);
static const String routeName = '/';
Widget build(BuildContext context) {
final authState = context.watch<AuthProvider>().state;
if (authState.authStatus == AuthStatus.authenticated) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushNamed(context, HomePage.routeName);
});
} else if (authState.authStatus == AuthStatus.unauthenticated) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushNamed(context, SigninPage.routeName);
});
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:equatable/equatable.dart';
import 'package:fb_auth_provider/models/custom_error.dart';
enum SigninStatus {
initial,
submitting,
success,
error,
}
class SigninState extends Equatable {
final SigninStatus signinStatus;
final CustomError error;
SigninState({
required this.signinStatus,
required this.error,
});
factory SigninState.initial() {
return SigninState(
signinStatus: SigninStatus.initial,
error: CustomError(),
);
}
List<Object> get props => [signinStatus, error];
bool get stringify => true;
SigninState copyWith({
SigninStatus? signinStatus,
CustomError? error,
}) {
return SigninState(
signinStatus: signinStatus ?? this.signinStatus,
error: error ?? this.error,
);
}
}class _SigninPageState extends State<SigninPage> {
final _formKey = GlobalKey<FormState>();
AutovalidateMode _autovalidateMode = AutovalidateMode.disabled;
String? _email, _password;
void _submit() async {
setState(() {
_autovalidateMode = AutovalidateMode.always;
});
final form = _formKey.currentState;
if (form == null || !form.validate()) return;
print('email: $_email, password: $_password');
context.read<SigninProvider>().signin(email: _email!, password: _password!);
try {
await context
.read<SigninProvider>()
.signin(email: _email!, password: _password!);
} on CustomError catch (e) {
errorDialog(context, e);
}
}
Widget build(BuildContext context) {
...import 'dart:io'; //플랫폼 체크
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
//showDialog, showCupertionDialog 위젯에 필요
import '../models/custom_error.dart'; //에러 내용을 담음
void errorDialog(BuildContext context, CustomError e) {
if (Platform.isIOS) {
//ios면 쿠퍼티노 다이얼로그 띄움
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: Text(e.code),
content: Text(e.plugin + '\n' + e.message),
actions: [
CupertinoDialogAction(
child: Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
} else {
showDialog(
//안드로이드면 showDialog 호출
context: context,
builder: (context) {
return AlertDialog(
title: Text(e.code),
content: Text(e.plugin + '\n' + e.message),
actions: [
TextButton(
//ios면 CupertinoAlertDialog 부른다는 점만 다름
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
);
},
);
}
}초기값으로 SigninState.initial을 가지고, signin getter를 만듬.
auth repository의 sign in 함수를 호출해서 AuthRepository 타입의 인스턴스가 필요함
signin 함수는 form에서 입력받고 void 타입의 Future를 리턴하는 async 함수.
rethrow 해서 ui에서 처리하도록 함.main에 추가
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<**AuthRepository**>(
create: (context) => AuthRepository(
firebaseFirestore: FirebaseFirestore.instance,
firebaseAuth: fbAuth.FirebaseAuth.instance,
),
),
StreamProvider<fbAuth.User?>(
create: (context) => context.read<AuthRepository>().user,
initialData: null,
),
ChangeNotifierProxyProvider<fbAuth.User?, AuthProvider>(
create: (context) => AuthProvider(
authRepository: context.read<AuthRepository>(),
),
update: (BuildContext context, fbAuth.User? userStream,
AuthProvider? authProvider) =>
authProvider!..update(userStream),
),
ChangeNotifierProvider<SigninProvider>(
create: (context) => SigninProvider(
authRepository: context.read<AuthRepository>(),
),
),
],전체 코드
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:flutter/material.dart';
import '../../models/custom_error.dart';
import '../repositories/auth_repository.dart';
import 'signin_state.dart';
class SigninProvider with ChangeNotifier {
SigninState _state = SigninState.initial();
SigninState get state => _state;
final AuthRepository authRepository;
SigninProvider({
required this.authRepository,
});
Future<void> signin({
required String email,
required String password,
}) async {
_state = _state.copyWith(signinStatus: SigninStatus.submitting);
notifyListeners();
try {
await authRepository.signin(email: email, password: password);
_state = _state.copyWith(signinStatus: SigninStatus.success);
notifyListeners();
} on CustomError catch (e) {
_state = _state.copyWith(signinStatus: SigninStatus.error, error: e);
notifyListeners();
rethrow;
}
}
}
signup은 signin과 진짜 비슷해서 약간만 수정하면 됨.
provider에서 만든 함수들은 각 페이지에서 .read 로 호출하고, .state 로 상태 보면서 각 상태에 맞는 화면으로 구성하면 됨!