ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 코딩마스터하기|서치 페이지 대신 팔로/언팔로 페이지 만들기, 파이어베이스
    개발/Flutter 2021. 11. 8. 21:11
    반응형

    팔로 언팔로 페이지 만들기

    우선 screens 폴더에 search_screen.dart를 만든다. 그리고 이번에 팔로/언팔로를 위해서 ListView.separated를 사용해 볼 것이다. 우선 SafeArea를 해주고 그안에 ListView.seperated를 넣어준다. 그 안에는 itemBuilder과 separatorBuilder, itemCount를 넣어주면 된다. itemBuilder은 칸의 아이템을 넣을 수 있는 부분이고 separatorBuilder는 각 item을 나눠주는 부분이다. 우선 itemBuilder와 separatorBuilder에 (context, index){}를 해주고 itemBuilder에는 return ListTile을 해준다. leading에는 RoundedAvatar를 하고 title에는 Text로 username $index를 해주고 subtitle에도 Text로 user bio number $index를 해준다. trailing에는 버튼을 위해서 Container로 우선 해주고 width 80, height는 30 alignmnet는 center로 해준다. borderRadius에는 circular에 8을 준다. decoration에는 Boxdecoration으로 color에 red[50] boder는 Border.all()에 red와 width 0.5를 준다. Container child는 Text로 following에 textAlign은 center, bold를 주면 각 리스트 칸의 아이템부분의 틀은 완성이다. separatorBuilder의 return에는 Divider로 color에 grey를 주기만 하면 각 칸 사이가 회색선으로 나뉜다. 아이템갯수는 30개정도로 우선 해보자.


    class _SearchScreenState extends State<SearchScreen> {
      @override
      Widget build(BuildContext context) {
        return SafeArea(
          child: ListView.separated(
            itemBuilder: (context, index) {
              return ListTile(
                leading: RoundedAvatar(index: index,),
                title: Text('username $index'),
                subtitle: Text('user bio number $index'),
                trailing: Container(
                  height: 30,
                  width: 80,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.red[50],
                    border: Border.all(
                      color: Colors.red,
                      width: 0.5,
                    ),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    'Following',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              );
            }, //(context, index)
            separatorBuilder: (context, index) {
              return Divider(color: Colors.grey,);
            }, //아이템 사이사이 부분
            itemCount: 30, //아이템 갯수
          ),
        );
      }
    }

    버튼 클릭시 follow unfollow 와 색 바꾸기

    우선 follow를 구분하기 위해서 List<bool>을 만들어 전체 리스트의 상태를 언팔로우로 해준다. state class의 상단에 List<bool> followings =  List.generate(30, (index) => false)로 30개의 false를 만들어 주고, itemCount를 30에서 followings.length로 바꿔준다. 그리고 ListTile의 onTap에 누르면 팔로에서 언팔로, 언팔로에서 팔로로 바뀌어야 하므로 setState안에 followings[index] = !followings[index]를 해주면 된다. BoxDecoration과 Border.all에 색부분에 followings[index]?red:blue로 해주고 실행하면 바뀌는 것을 알 수 있다.


    class _SearchScreenState extends State<SearchScreen> {
      List<bool> followings = List.generate(30, (index) => false);
    
      @override
      Widget build(BuildContext context) {
        return SafeArea(
          child: ListView.separated(
            itemBuilder: (context, index) {
              return ListTile(
                onTap: () {
                  setState(() {
                    followings[index] = !followings[index];
                  });
                },
                leading: RoundedAvatar(
                  index: index,
                ),
                title: Text('username $index'),
                subtitle: Text('user bio number $index'),
                trailing: Container(
                  height: 30,
                  width: 80,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: followings[index] ? Colors.blue[50] : Colors.red[50],
                    border: Border.all(
                      color: followings[index] ? Colors.blue : Colors.red,
                      width: 0.5,
                    ),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    followings[index] ?'Following' : 'Unfollowing',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              );
            }, //(context, index)
            separatorBuilder: (context, index) {
              return Divider(
                color: Colors.grey,
              );
            }, //아이템 사이사이 부분
            itemCount: followings.length, //아이템 갯수
          ),
        );
      }
    }

    파이어베이스

    우선 프로젝트를 만든다.  그리고 그 프로젝트에서 Authentication에서 이메일과 암호를 켜준다. 그리고 프로젝트 설정에서 안드로이드 버튼을 눌러준다. andoriod 폴더에 app - src - main - androidmanifest에서 패키지 네임을 복사해서 넣어준다. 그리고 google-services.json파일을 다운받아서 app 폴더에 넣어준다. 그리고 andorid 폴더의 build.gradle에 classpath 'com.google.gms:google-services:4.3.3' 를 buildscript - dependencies에 넣어준다. pubspec.yaml에 firebase_auth: ^0.16.1를 추가해준다. 그 후 build.gradle에서 dependencies에서 가장 위의 tools.build:gradle를 4.0.1로 변경해준다. 에러가 나오면 읽고 경로를 따라서 해당부분을 변경해주면 된다.


    firebase auth 상태 모델 생성.

    firebase_auth: ^0.18.0부터는 변경 전에서 변경 후 값 사용

    변경전 => 변경후

    FirebaseUser => User

    AuthResult => UserCredential

    currentUser() => currentUser

    onAuthStateChanged => authStateChanges()

    models에 firebase_auth_state.dart를 만들어 준다. class FirebaseAuthState extends ChangeNotifier{}로 만들어 준다. 상태에 대한 부분을 위해 enum으로 FirebaseAuthStatus를 만들어 주고 signout, progress, signin을 생성한다. 그리고 상태에 대한 부분인 FirebaseAuthStatus _firebaseAuthStatus = FirebaseAuthStatus.progress; FirebaseUser _firebaseUse;을 만들어 준다. 그리고 메소드를 만들어 주는데 로그인에 대한 부분으로 다음과 같이 생성해준다.


    class FirebaseAuthState extends ChangeNotifier {
      FirebaseAuthStatus _firebaseAuthStatus = FirebaseAuthStatus.progress;
      FirebaseUser _firebaseUse;
    
      void changeFirebaseAuthStatus([FirebaseAuthStatus firebaseAuthStatus]) {
        if (firebaseAuthStatus != null) {
          _firebaseAuthStatus = firebaseAuthStatus;
        } else {
          if (_firebaseUse != null) {
            _firebaseAuthStatus = FirebaseAuthStatus.signin;
          } else {
            _firebaseAuthStatus = FirebaseAuthStatus.signout;
          }
        }
        notifyListeners();
      }
    }
    
    enum FirebaseAuthStatus { signout, progress, signin }

    inject FirebaseAuthState via provider

    상태에서 firebase에서 상태를 받아왔을때 변화시키는 부분을 만들어 주자. void로 watchAuthChange(){}를 만들어 준다. 우선 클래스에 FirebaseAuth _firebaseAuth = FirebaseAuth.instance를 만들어 주고 FirebaseUser _firebaseUser;도 만들어 준다. 그리고 _firebaseAuth.onAuthStateChanged를 이용해서 상태가 바뀔때마다 값을 받아온다. .listen(firebaseUser)를 해주고{} 안에 if문에 firebaseUser와 _firebaseUser가 둘다 null이면 그냥 return;을 해주고 두 값이 다르면 _firebaseUser = firebaseUser로 해준다. 그리고 changeFirebaseAUthStatus를 해서 아래의 메소드를 실행시켜주면 된다. 그리고 main에서도 각 값에 따라서 switch를 통해 아웃이면 인증페이지 인이면 메인페이지 그외는  로딩페이지로 가게 만들어 준다.


    main.dart
    class MyApp extends StatelessWidget {
      FirebaseAuthState _firebaseAuthState = FirebaseAuthState();
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider<FirebaseAuthState>.value(
          value: _firebaseAuthState,
          child: MaterialApp(
            home: Consumer<FirebaseAuthState>(
              builder: (BuildContext context, FirebaseAuthState firebaseAuthState,
                  Widget widget) {
                switch (firebaseAuthState.firebaseAuthStatus) {
                  case FirebaseAuthStatus.signout:
                    return AuthScreen();
                  case FirebaseAuthStatus.progress:
                    return MyProgressIndicator();
                  case FirebaseAuthStatus.signin:
                    return HomePage();
                  default:
                    return MyProgressIndicator();
                }
              },
            ),
            theme: ThemeData(
              primarySwatch: white,
            ),
          ),
        );
      }
    }
    firebase_auth_state.dart
    class FirebaseAuthState extends ChangeNotifier {
      FirebaseAuthStatus _firebaseAuthStatus = FirebaseAuthStatus.progress;
      FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
      FirebaseUser _firebaseUser;
    
      void watchAuthChange() {
        _firebaseAuth.onAuthStateChanged.listen((firebaseUser) {
          if (firebaseUser == null && _firebaseUser == null) {
            return;
          } else if (firebaseUser != _firebaseUser) {
            _firebaseUser = firebaseUser;
            changeFirebaseAuthStatus();
          }
        });
      }
    
      void changeFirebaseAuthStatus([FirebaseAuthStatus firebaseAuthStatus]) {
        if (firebaseAuthStatus != null) {
          _firebaseAuthStatus = firebaseAuthStatus;
        } else {
          if (_firebaseUser != null) {
            _firebaseAuthStatus = FirebaseAuthStatus.signin;
          } else {
            _firebaseAuthStatus = FirebaseAuthStatus.signout;
          }
        }
        notifyListeners();
      }
    
      FirebaseAuthStatus get firebaseAuthStatus => _firebaseAuthStatus;
    }
    
    enum FirebaseAuthStatus { signout, progress, signin }

    testing sign in and sign out flow

    기존 pushReplacement로 페이지를 변경하였지만 firebase를 통해서 sign in sign up sign out등의 기능을 작동시키려고 한다. Provider를 통해서 할 수 있다. 우선 sign up과 sign in 의 _submitButton의 onpressed에 페이지 이동부분을 삭제하고 Provider를 통해 이동한다. Provider.of<FirebaseAuthState>(context, listen: false,)를 해준다. 여기서는 listen을 사용하지 않기때문에 안쓰는것이 아닌 false를 해줘야 에러가 나지않고 작동한다. 그리고 .changeFirebaseAuthStatus(FirebaseAuthStatus.signin)을 해줘서 signin으로 바꿔준다. 실행하면 로그인이나 가입시 정상 기입하고 버튼을 누르면 작동하는 것을 알 수 있다. profile_side_menu에서 sign out에 해당하는 부분에서 위와같이 해주고 FirebaseAuthStatus.signout로 해주면 로그아웃 되는 것을 알 수 있다.


    sign_in_fomr.dart, sign_up_form.dart
    FlatButton _submitButton(BuildContext context) {
        return FlatButton(
          onPressed: () {
            if (_formKey.currentState.validate()) {
              //3개다 true면 서버에 저장하거나 값 읽어오거나 하면됨.
              // Navigator.of(context).pushReplacement(
              //   MaterialPageRoute(
              //     builder: (context) => HomePage(),
              //   ),
              // );
              Provider.of<FirebaseAuthState>(context, listen: false,) //listen : false로 안해주면 에러남.
                  .changeFirebaseAuthStatus(FirebaseAuthStatus.signin);
            }
          },
          child: Text(
            'Sign In',
            style: TextStyle(color: Colors.white),
          ),
          color: Colors.blue,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(common_xxs_gap),
          ),
        );
      }
    profile_side_menu.dart
                ListTile(
                  leading: Icon(
                    Icons.exit_to_app,
                    color: Colors.black87,
                  ),
                  title: Text('Log out'),
                  onTap: () {
                    Provider.of<FirebaseAuthState>(context, listen: false,)
                        .changeFirebaseAuthStatus(FirebaseAuthStatus.signout);
                  },
                ),

    using animated switcher for sign in-out transition

    각 부분이 로그인 이나 아웃을 할때 화면의 효과를 위해 AnimatedSwitcher을 줄것이다. 우선 MyApp 클래스 내 상단에 현재 위젯을 정해줄 Widget _currentWidget;를 만들어 주고 switch를 _currentWidget = AuthScreen();과 같이 각 부분에 맞게 변경 해준다. 그리고 switch 밖 하단에 return AnimatedSwitcher()을 주고 duration에 duration을 주고, child에 _currentWidget를 해주면 정상 작동한다.


    class MyApp extends StatelessWidget {
      FirebaseAuthState _firebaseAuthState = FirebaseAuthState();
    
      Widget _currnetWidget;
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider<FirebaseAuthState>.value(
          value: _firebaseAuthState,
          child: MaterialApp(
            home: Consumer<FirebaseAuthState>(
              builder: (BuildContext context, FirebaseAuthState firebaseAuthState,
                  Widget widget) {
                switch (firebaseAuthState.firebaseAuthStatus) {
                  case FirebaseAuthStatus.signout:
                    _currnetWidget = AuthScreen();
                    break;
                  case FirebaseAuthStatus.signin:
                    _currnetWidget = HomePage();
                    break;
                  default:
                    _currnetWidget = MyProgressIndicator();
                    break;
                }
                return AnimatedSwitcher(
                  duration: duration,
                  child: _currnetWidget,
                );
              },
            ),
            theme: ThemeData(
              primarySwatch: white,
            ),
          ),
        );
      }
    }

    sign out method 생성 후 사용

    기존 sign out은 상태만 바꿔줬을뿐 값은 비워주지 않았기 때문에 firebase_auth_state.dart로 가서 void의 메소드를 만들어준다. 이름은 signOut로 해주고 _firebaseAuthStatus = FirebaseAuthStatus.signout;로 해준다. 또 if(_firebaseUser != null)로 유저값이 안비워져있다면 _firebaseUser = null로 해주고 _firebaseAuth.signOut를 해준뒤 if문 밖에 notifyListeners();를 해주면 된다. profile_side_menu에서 changeFirebaseAuthStatus 부분을 signOut()로 바꿔주고 실행하면 정상 작동 하는 것을 알 수있다.


      void signOut(){
        _firebaseAuthStatus = FirebaseAuthStatus.signout;
        if(_firebaseUser != null){
          _firebaseUser = null;
          _firebaseAuth.signOut();
        }
        notifyListeners();
      }

    wire sign in-up with firebase auth

    firebase에 등록을 해서 register와 login이 되게 해보자. firebase_auth_state에 register와 login 메소드를 만든다. 그리고 그 안에 registerUser는 _firebaseAuth.createUserWithEmailAndPassword로()에 email에 email password에 password를 해준다. login은 _firebaseAuth.signInWithEmailAndPassword를 해주고 email,password를 넣어준다. sign_in_form에 provider에 .changeFirebaseAuthStatus(FirebaseAuthStatus.signin); 를 .login(email: _emailController.text,password: _pwController.text);로 해주고 sign_up_form도 .registerUser(email: _emailController.text, password: _pwController.text); 로 변경해준다. 이상태에서 실행하면 가입은 되지만 화면이 안바뀌는데 main에 _firebaseAuthState.watchAuthChange();를 추가해서 상태를 변경해줘야 페이지가 변경된다.


    sign_up_form.dart
    
      FlatButton _submitButton(BuildContext context) {
        return FlatButton(
          onPressed: () {
            if (_formKey.currentState.validate()) {
              //3개다 true면 서버에 저장하거나 값 읽어오거나 하면됨.
              // Navigator.of(context).pushReplacement(
              //   MaterialPageRoute(
              //     builder: (context) => HomePage(),
              //   ),
              // );
              Provider.of<FirebaseAuthState>(context, listen: false,) //listen : false로 안해주면 에러남.
                  .registerUser(email: _emailController.text, password: _pwController.text);
            }
          },
          child: Text(
            'Join',
            style: TextStyle(color: Colors.white),
          ),
          color: Colors.blue,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(common_xxs_gap),
          ),
        );
    sing_in_form.dart
    
      FlatButton _submitButton(BuildContext context) {
        return FlatButton(
          onPressed: () {
            if (_formKey.currentState.validate()) {
              //3개다 true면 서버에 저장하거나 값 읽어오거나 하면됨.
              // Navigator.of(context).pushReplacement(
              //   MaterialPageRoute(
              //     builder: (context) => HomePage(),
              //   ),
              // );
              Provider.of<FirebaseAuthState>(context, listen: false,) //listen : false로 안해주면 에러남.
                  .login(email: _emailController.text,password: _pwController.text);
            }
          },
          child: Text(
            'Sign In',
            style: TextStyle(color: Colors.white),
          ),
          color: Colors.blue,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(common_xxs_gap),
          ),
        );
    firebase_auth_state.dart
      
      void registerUser({@required String email, @required String password}){
        _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
    
      }
    
      void login({@required String email, @required String password}){
        _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
      }

    error handling for sign up

    에러를 잡아보자. 에러의 코드는 가입에는 ERROR_WEAK_PASSWORD, ERROR_WEAK_PASSWORD, ERROR_EMAIL_ALREADY_IN_USE 가 있다. 그래서 createUserWithEmailAndPassword 뒤에 .catchError((error){}) 의 {}안에 print로 알수 있다. 우선 메시지로 보여주기 위해서 String _message = '';을 만들어 주고 switch(error.code)로 각 에러별로 나눌수 있다. 각 case에 에러별로 알맞은 메시지를 넣어준 후, 하단에 SnackBar를 생성하고 SnackBar(content: Text(_message))를 담아준다. 그리고 Scaffold.of(context).showSnackBar(snackBar);로 스낵바를 보여준다. 이때 context를 받아와야 하므로 메소드에 BuildContext context를 추가해 context를 받아오면 된다. Scaffold안에 있어야 SnackBar를 사용할 수 있는데 sign_up_form은 AuthScreen에 있어서 Scaffold에 들어가 있어서 받아올수 있어 동작한다. 그리고 sign_up_form에 registerUser에 context도 추가해 준다.


      void registerUser(BuildContext context,{@required String email, @required String password}) {
        _firebaseAuth
            .createUserWithEmailAndPassword(email: email.trim(), password: password.trim())
            .catchError((error) {
              print(error);
              String _message = '';
          switch (error.code) {
            case 'ERROR_WEAK_PASSWORD':
              _message = '패스워드의 보안이 약합니다.';
              break;
            case 'ERROR_WEAK_PASSWORD':
              _message = '올바른 이메일을 넣어주세요';
              break;
            case 'ERROR_EMAIL_ALREADY_IN_USE':
              _message = '이미 사용중인 아이디 입니다.';
              break;
          }
    
          SnackBar snackBar = SnackBar(content: Text(_message,),);
          Scaffold.of(context).showSnackBar(snackBar);
        });
      }

    error handling on login

    로그인 부분도 똑같이 에러를 핸들링 해주자. 에러코드를 찾아서 똑같이 해주면 된다. 그리고 sign_in_form에도 context를 추가해주고 실행하면 잘 작동하는것을 볼 수 있다.


      void login(BuildContext context,
          {@required String email, @required String password}) {
        _firebaseAuth
            .signInWithEmailAndPassword(
                email: email.trim(), password: password.trim())
            .catchError((error) {
          String _message = '';
          switch (error.code) {
            case 'ERROR_INVALID_EMAIL':
              _message = '메일 주소가 유효하지 않습니다.';
              break;
            case 'ERROR_WRONG_PASSWORD':
              _message = '암호가 틀렸습니다.';
              break;
            case 'ERROR_USER_NOT_FOUND':
              _message = '가입하지 않은 아이디입니다.';
              break;
            case 'ERROR_USER_DISABLED':
              _message = '해당유저는 금지되어 있습니다.';
              break;
            case 'ERROR_TOO_MANY_REQUESTS':
              _message = '너무 많이 시도하였습니다. 나중에 다시 시도해주세요.';
              break;
            case 'ERROR_OPERATION_NOT_ALLOWED':
              _message = '허용되지 않았습니다.';
              break;
          }
    
          SnackBar snackBar = SnackBar(
            content: Text(
              _message,
            ),
          );
          Scaffold.of(context).showSnackBar(snackBar);
        });
      }

     

    반응형

    댓글

Designed by Tistory.