flutter, firebase 인증 관련 provider, 화면 전환

hunnypooh·2022년 10월 31일

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

authState, authProvider

  • firebase는 유저의 상태가 변할때마다 유저의 정보를 Stream 데이터로 보내줌.

  • 두번째 과정은 ChangeNotifier를 이용해서 유저값을 읽어서 유저가 null이면 unauthenticated, null이 아니면 authenticated state를 만들어서 Listener들에게 알려줌.
    • 이 상태로 로그인 페이지, 홈페이지로 분기함.
  • auth_provider는 사용자가 signin, signout, token expired 등 이벤트가 생길때마다 stream을 이용해서 앱에게 사용자의 상태를 알려줌
    • 이미 auth repository에서 그런 stream을 읽어서 user 변수에 저장하는 getter 만듬.
    • 따라서 auth_provider는 user Stream을 읽어서 유저값이 변할때마다 null인지를 체크해서 unauthenticated나 authenticated state를 생성함.
      • ←그래서 auth_provider는 읽어오는 StreamProivder의 proxyProvider 형태가 되야함. (user stream이 변할때마다 update 해야하므로)
      • 이때 StreamProvider가 제공해주는 값은 AuthRepository에 저장된 user getter임.

auth state

  1. status enum 만들고 클래스 생성
    • 작성 후 generate constructor, equatable, tostring, copywith 순서대로 생성
    • user가 nullable 타입이라서 Object 타입을 Object?로 변경
    • status enum 만들고 클래스 생성
      • 작성 후 generate constructor, equatable, tostring, copywith 순서대로 생성
      • user가 nullable 타입이라서 Object 타입을 Object?로 변경
  2. 초기 state로 활용될 factory 코드 작성
    • 전체 코드
      // 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,
          );
        }
      }

auth provider

  1. auth_provider는 변할때마다 리스너들에게 알려줘야 해서 ChangeNotifier와 믹스인.
  2. state의 초기값은 AuthState.unknown(); , 외부에서 state에 접근 가능하게 get 작성
  3. signout은 signin, signup을 처리하는 프로바이더에서 처리하기 힘들어서 auth_proivder에서 작성.
    1. signout이 AuthRepository에 정의되어 있어서 AuthRepository 인스턴스가 필요해서 만들고, generate constructor.
    2. ChangeNotifierProxyProvider 사용하고, update 함수 작성.
      • 이때 Stream provider로부터 제공받는 데이터는 AuthRepository에 저장된 user getter.
      • ChangeNotifierProxyProvider 는 MyModel에 변화가 있을 때 동일한 instance가 계속 사용되고 다시 create 되지 않아서 업데이트가 계속 될 때 사용하기 좋음.
    • 전체 코드
      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();
        }
      }

injecting

  • main에 provider 주입.
    ...
    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(),
            },
          ),
        );
      }
    }

splash 화면 (사용자 상태에 따라서 화면 전환)

  • build 함수 안에서 auth provider의 state를 watch해서 값을 authState 변수에 저장.
    • build 안에서 navigation 하니까 safe operation을 위해 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(),
      ),
    );
  }
}

signin state, provider

  • 사용자가 입력한 이메일, password 정보를 가지고 auth repository에서 제공하는 signin 함수를 호출해야 함.
    • 성공하면 home page로 이동, 실패하면 ui 상으로 사용자에게 실패했다고 알림. 진행중일때는 ui 상으로 사용자에게 진행중이라고 알림
    • ⇒ signin 페이지에 여러 state 변화가 있어서, 따로 관리해야함!

signin state

  • status enum 만들고 클래스 생성
    • 작성 후 generate constructor, equatable, tostring, copywith 순서대로 생성
  • 초기화 코드인 factory 작성
  • 전체 코드
    // 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'),
                ),
              ],
            );
          },
        );
      }
    }

signin provider

  • 초기값으로 SigninState.initial을 가지고, signin getter를 만듬.

  • auth repository의 sign in 함수를 호출해서 AuthRepository 타입의 인스턴스가 필요함

    • ← 생성하고, 생성자 생성
  • signin 함수는 form에서 입력받고 void 타입의 Future를 리턴하는 async 함수.

    • 시간이 걸리는 작업이라서 state의 signinStatus를 submitting으로 변경시켯 뭔가가 진행중이라고 알려줌
    • 필요한 작업은 전부 try-catch에서 수행.
      • catch
        • 에러는 auth repository에서 전부 CustomRrror 타입으로 throw 해서, CustomError타입으로 catch함.
        • 에러 받으면 리스너들에게 알리고, rethrow 해서 ui에서 처리하도록 함.
      • try
        • auth repository의 signin 함수 호출.
        • 여기서는 성공적인 경우라서 signin status를 success인 새로운 state를 생성하고, 리스너에게 알림.
  • main에 추가

    • signin provider는 auth repository를 필요로 하는데, 위에서 이미 만들어서 extension method 인 read를 통해 읽을 수 있음.
      
      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 로 상태 보면서 각 상태에 맞는 화면으로 구성하면 됨!

profile
간단한것들 정리

0개의 댓글