본문 바로가기
프로그래밍/Flutter & Dart

Flutter & Firebase - 인스타그램 클론 (2) - 기본 화면 만들기 (OAuth 구글 로그인)

by 어느덧중반 2020. 9. 2.
반응형

오늘의 목표

앞서 추가한 기본 화면에 구글 SignIn 버튼을 실제로 동작하도록 구현해보자.

#0. 구글 SignIn 관련 variable 설명

#1. 앱 시작시 build 전 initState

#2. 구글 SignIn 버튼 클릭시 동작

 


#0. 구글 SignIn 관련 variable 설명

 - GoogleSignIn : 구글 로그인의 기능을 담당할 변수

 - GoogleSignInAccount : 구글 로그인시 계정을 담을 변수

 - userReference : 로그인시 user정보 db(firestore)에 저장할 때 사용할 변수

 - isSignedIn : 앱 시작시 기 로그인여부를 확인할 변수

 - DocumentSnapshot : Firestore에서 실시간 데이터를 주고받을 때 사용하는 용도

// variable for google sign in (very easy to use)
final GoogleSignIn googleSignIn = new GoogleSignIn();
// variable for firestore collection 'users'
final userReference = FirebaseFirestore.instance.collection('users');
...
bool isSignedIn = false;
...
final GoogleSignInAccount gCurrentUser = googleSignIn.currentUser;
...
DocumentSnapshot documentSnapshot = await userReference.doc(gCurrentUser.id).get();

#1. 앱 시작시 build 전 initState

 - Firebase 앱 초기화

 - 앱 실행시 기존에 로그인했었는지 체크

   기존 로그인했었다면) 사용자 정보 저장

@override
void initState() {
  super.initState();
  Firebase.initializeApp().whenComplete(() {
    print("completed");
    setState(() {});
  });
  pageController = PageController();

  // 앱 실행시 구글 사용자의 변경여부를 확인함
  googleSignIn.onCurrentUserChanged.listen((gSignInAccount) {
    controlSignIn(gSignInAccount); // 사용자가 있다면 로그인
  }, onError: (gError) {
    print("Error Message : " + gError);
  });

  googleSignIn.signInSilently();
  // suppressErrors: false).then((gSignInAccount) {
  //   controlSignIn(gSignInAccount);
  // }).catchError((gError) {
  //   print("Error Message : " + gError);
  // });
}

// 로그인 상태 여부에 따라 isSignedIn flag값을 변경해줌
controlSignIn(GoogleSignInAccount signInAccount) async {
  if(signInAccount != null) {
    await saveUserInfoToFirestore();
    setState(() {
      isSignedIn = true;
    });
  } else {
    setState(() {
      isSignedIn = false;
    });
  }
}

saveUserInfoToFirestore() async {
  // 현재 구글 로그인된 사용자 정보 가져오기
  final GoogleSignInAccount gCurrentUser = googleSignIn.currentUser;
  // 해당 유저의 db정보 가져오기
  DocumentSnapshot documentSnapshot = await userReference.doc(gCurrentUser.id).get();

  // 해당 유저의 db정보가 없다면
  if(!documentSnapshot.exists) {
    // 유저정보를 셋팅하는 페이지로 이동
    final username = await Navigator.push(context, MaterialPageRoute(builder: (context) => CreateAccountPage()));

    // 유저정보 셋팅된 값으로 db에 set
    userReference.doc(gCurrentUser.id).set({
      'id' : gCurrentUser.id,
      'profileName' : gCurrentUser.displayName,
      'username' : username,
      'url' : gCurrentUser.photoUrl,
      'email' : gCurrentUser.email,
      'bio' : '',
      'timestamp' : timestamp
    });

    // 해당 정보 다시 가져오기
    documentSnapshot = await userReference.doc(gCurrentUser.id).get();
  }

  // 현재 유저정보에 값 셋팅하기
  currentUser = User.fromDocument(documentSnapshot);
}

#2. 구글 SignIn 버튼 클릭시 동작

 - 최초 SignIn 버튼 클릭을 하는 경우 구글 로그인 계정 추가 페이지가 뜬다.

GestureDetector(
  onTap: loginUser,
  child: Container(
    width: 200,
    height: 50,
    decoration: BoxDecoration(
      image: DecorationImage(
        image: AssetImage('assets/images/google_signin_button.png'),
        fit: BoxFit.cover
      )
    ),
  ),
)

 - 계정을 선택하면 사용자 정보를 입력하는 Setting 창이 뜬다. (pop up)

 - 유저네임을 입력하고 Submit 버튼 클릭시 유저 정보가 db에 저장되며 해당 유저정보를 가지고
   홈 화면(feed page)으로 돌아온다.


전체 소스 (HomePage.dart)

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:instargram_clone_by_kyungsnim/models/user.dart';
import 'package:instargram_clone_by_kyungsnim/pages/TimeLinePage.dart';
import 'package:instargram_clone_by_kyungsnim/pages/SearchPage.dart';
import 'package:instargram_clone_by_kyungsnim/pages/ProfilePage.dart';
import 'package:instargram_clone_by_kyungsnim/pages/UploadPage.dart';
import 'package:instargram_clone_by_kyungsnim/pages/NotificationsPage.dart';
import 'package:instargram_clone_by_kyungsnim/pages/CreateAccountPage.dart';

// variable for google sign in (very easy to use)
final GoogleSignIn googleSignIn = new GoogleSignIn();
// variable for firestore collection 'users'
final userReference = FirebaseFirestore.instance.collection('users');

final DateTime timestamp = DateTime.now();
User currentUser;

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool isSignedIn = false;
  // 페이지 컨트롤
  PageController pageController;
  int getPageIndex = 0;

  @override
  void initState() {
    super.initState();
    Firebase.initializeApp().whenComplete(() {
      print("completed");
      setState(() {});
    });
    pageController = PageController();

    // 앱 실행시 구글 사용자의 변경여부를 확인함
    googleSignIn.onCurrentUserChanged.listen((gSignInAccount) {
      controlSignIn(gSignInAccount); // 사용자가 있다면 로그인
    }, onError: (gError) {
      print("Error Message : " + gError);
    });

    googleSignIn.signInSilently();
    // suppressErrors: false).then((gSignInAccount) {
    //   controlSignIn(gSignInAccount);
    // }).catchError((gError) {
    //   print("Error Message : " + gError);
    // });
  }

  // 로그인 상태 여부에 따라 isSignedIn flag값을 변경해줌
  controlSignIn(GoogleSignInAccount signInAccount) async {
    if(signInAccount != null) {
      await saveUserInfoToFirestore();
      setState(() {
        isSignedIn = true;
      });
    } else {
      setState(() {
        isSignedIn = false;
      });
    }
  }

  saveUserInfoToFirestore() async {
    // 현재 구글 로그인된 사용자 정보 가져오기
    final GoogleSignInAccount gCurrentUser = googleSignIn.currentUser;
    // 해당 유저의 db정보 가져오기
    DocumentSnapshot documentSnapshot = await userReference.doc(gCurrentUser.id).get();

    // 해당 유저의 db정보가 없다면
    if(!documentSnapshot.exists) {
      // 유저정보를 셋팅하는 페이지로 이동
      final username = await Navigator.push(context, MaterialPageRoute(builder: (context) => CreateAccountPage()));

      // 유저정보 셋팅된 값으로 db에 set
      userReference.doc(gCurrentUser.id).set({
        'id' : gCurrentUser.id,
        'profileName' : gCurrentUser.displayName,
        'username' : username,
        'url' : gCurrentUser.photoUrl,
        'email' : gCurrentUser.email,
        'bio' : '',
        'timestamp' : timestamp
      });

      // 해당 정보 다시 가져오기
      documentSnapshot = await userReference.doc(gCurrentUser.id).get();
    }

    // 현재 유저정보에 값 셋팅하기
    currentUser = User.fromDocument(documentSnapshot);
  }

  void dispose() {
    pageController.dispose();
    super.dispose();
  }

  loginUser() {
    googleSignIn.signIn();
  }

  logoutUser() {
    googleSignIn.signOut();
  }

  whenPageChanges(int pageIndex) {
    setState(() {
      this.getPageIndex = pageIndex;
    });
  }

  onTapChangePage(int pageIndex) {
    pageController.animateToPage(pageIndex, duration: Duration(milliseconds: 400), curve: Curves.bounceInOut);
  }

  buildHomeScreen() {
    return Scaffold(
      body: PageView(
        children: <Widget>[
          // 정상 로그인시 홈스크린 보인다.
          TimeLinePage(), // 0번 pageIndex
          SearchPage(), // 1번 pageIndex
          UploadPage(), // 2번 pageIndex
          NotificationsPage(), // 3번 pageIndex
          ProfilePage(), // 4번 pageIndex
        ],
        controller: pageController, // controller를 지정해주면 각 페이지별 인덱스로 컨트롤 가능
        onPageChanged: whenPageChanges, // page가 바뀔때마다 whenPageChanges 함수가 호출되고 현재 pageIndex 업데이트해줌
        physics: NeverScrollableScrollPhysics(),
      ),
      bottomNavigationBar: CupertinoTabBar(
        currentIndex: getPageIndex,
        onTap: onTapChangePage,
        activeColor: Colors.white,
        inactiveColor: Colors.grey,
        backgroundColor: Colors.black,
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home)),
          BottomNavigationBarItem(icon: Icon(Icons.search)),
          BottomNavigationBarItem(icon: Icon(Icons.photo_camera, size: 35)),
          BottomNavigationBarItem(icon: Icon(Icons.notifications)),
          BottomNavigationBarItem(icon: Icon(Icons.person)),
        ],
      ),
    );
  }

  buildSignInScreen() {
    return Scaffold(
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topRight,
            end: Alignment.bottomLeft,
            colors: [
              Theme.of(context).accentColor,
              Theme.of(context).primaryColor
            ]
          )
        ),
        alignment: Alignment.center,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(
              'Instargram',
              style: TextStyle(
                fontSize: 70,
                color: Colors.white,
                fontFamily: 'Signatra'
              ),
            ),
            SizedBox(height: 200),
            GestureDetector(
              onTap: loginUser,
              child: Container(
                width: 200,
                height: 50,
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: AssetImage('assets/images/google_signin_button.png'),
                    fit: BoxFit.cover
                  )
                ),
              ),
            )
          ],
        )
      )
    );
  }

  @override
  Widget build(BuildContext context) {
    if(isSignedIn) {
      return buildHomeScreen();
    } else {
      return buildSignInScreen();
    }

  }
}

 

사용자 정보 추가 페이지 소스 (CreateAccountPage.dart)

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:instargram_clone_by_kyungsnim/widgets/HeaderWidget.dart';

class CreateAccountPage extends StatefulWidget {
  @override
  _CreateAccountPageState createState() => _CreateAccountPageState();
}

class _CreateAccountPageState extends State<CreateAccountPage> {
  final _scaffoldKey = GlobalKey<ScaffoldState>();
  final _formKey = GlobalKey<FormState>();
  String username;
  TextEditingController usernameController;

  submitUsername() {
    final form = _formKey.currentState;
    if(form.validate()) {
      form.save();

      SnackBar snackBar = SnackBar(content: Text('Welcome ' + username));
      _scaffoldKey.currentState.showSnackBar(snackBar);
      Timer(Duration(seconds: 4), () {
        Navigator.pop(context, username);
      });
    }
  }

  @override
  Widget build(BuildContext parentContext) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: header(context, title: 'Settings', disappearedBackButton: true),
      body: ListView(
        children: <Widget>[
          Container(
            child: Column(
              children: <Widget>[
                Padding(
                  padding: EdgeInsets.only(top: 25),
                  child: Center(
                    child: Text('Set up a username', style: TextStyle(fontSize: 25)),
                  )
                ),
                Padding(
                  padding: EdgeInsets.all(16),
                  child: Container(
                    child: Form(
                      key: _formKey,
                      autovalidate: true,
                      child: TextFormField(
                        style: TextStyle(color: Colors.white,),
                        validator: (val) {
                          if(val.trim().length < 5 || val.isEmpty) {
                            return 'user name is too short (< 5)';
                          } else if (val.trim().length > 15 || val.isEmpty) {
                            return 'user name is too long (> 15)';
                          } else {
                            return null;
                          }
                        },
                        onSaved: (val) => username = val,
                        decoration: InputDecoration(
                          enabledBorder: UnderlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey,),
                          ),
                          focusedBorder: UnderlineInputBorder(
                            borderSide: BorderSide(color: Colors.white),
                          ),
                          border: OutlineInputBorder(),
                          labelText: 'Username',
                          labelStyle: TextStyle(fontSize: 16),
                          hintText: 'must be at least 5 characters',
                          hintStyle: TextStyle(color: Colors.grey),
                        ),
                      )
                    )
                  )
                ),
                GestureDetector(
                  onTap: submitUsername,
                  child: Container(
                    height: 55,
                    width: 360,
                    decoration: BoxDecoration(
                      color: Colors.lightGreenAccent,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Center(
                      child: Text(
                        'Submit',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        )
                      )
                    )
                  )
                )
              ],
            )
          )
        ],
      )
    );
  }
}

구글 로그인 전 Firestore(DB)
구글 Sign in 클릭 후 계정 선택 및 사용자 정보 입력
정상 로그인 후 홈화면으로 돌아옴
사용자 정보가 users collection에 저장된다.

 


반응형

댓글