새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.

바로가기


Flutter와 Firebase로 Android iOS 둘 다 만들기 17 router 실제 페이지에 적용하기

3 분 소요

지금까지 얻은 지식을 토대로 실제 만들어야할 대상을 라우터를 통해서 전환해봅니다.

개요

먼저 간단한 계획을 그려봤습니다.

alt plan

  • 스플래시 페이지 3초
  • 인증 대기페이지(라우팅 역할)
  • 인증되어 있으면 메인페이지와 프로필페이지 로그아웃
  • 인증되어 있지 않으면 로그인페이지와 회원가입페이지
  • 로그인 후에 바로 메인으로 가지 않고 인증체크 페이지로 이동

지난번처럼 StreamBuilder로 처리하기에는 Splash 페이지에 너무 많은 코드가 뒤죽박죽이 될 것 같기 때문에.. 역할별로 조금씩 나눠봅니다.

사실 하기 나름인데 실무에 적용해보니 이메일 인증 체크등 거추장스러운 것들이 많이 있었기 때문입니다.

파일 정리

  • lib/
    • main.dart
    • pages/
      • splash.dart
      • auth.dart
      • signin.dart
      • signup.dart
      • home.dart
      • profile.dart

위의 그림처럼 6개의 페이지를 분리해서 만들었습니다.

main.dart

라우팅 정의를 합니다.

import 'package:flutter/material.dart';
import 'pages/splash.dart';
import 'pages/auth.dart';
import 'pages/signin.dart';
import 'pages/signup.dart';
import 'pages/home.dart';
import 'pages/profile.dart';

void main() => runApp(FFApp());

class FFApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      initialRoute: SplashPage.routeName,
      routes: {
        SplashPage.routeName: (context) => SplashPage(),
        AuthPage.routeName: (context) => AuthPage(),
        SignInPage.routeName: (context) => SignInPage(),
        SignUpPage.routeName: (context) => SignUpPage(),
        HomePage.routeName: (context) => HomePage(),
        ProfilePage.routeName: (context) => ProfilePage(),
      },
    );
  }
}

각 파일에는 routeName이 정의되어 있기 때문에 x.routeName 형태로 작성했습니다.

splash.dart

import 'package:flutter/material.dart';
import 'dart:async';

class SplashPage extends StatefulWidget {
  SplashPage({Key key}) : super(key: key);
  static const routeName = '/';

  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() { 
    super.initState();
    _routePage();
  }

  _routePage () async {
    await Future.delayed(Duration(seconds: 4));
    return Navigator.pushReplacementNamed(context, '/auth');
  }

  Widget _buildBody(message) {
    return Scaffold(
      body: Container(
        color: Theme.of(context).primaryColor,
        padding: EdgeInsets.all(10),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: <Widget>[
              Image.asset('assets/icon.png', width: 200),
              Text(message, style: Theme.of(context).textTheme.body1.copyWith(color: Colors.white),),
              Text('Copyright © fkkmemi.', style: Theme.of(context).textTheme.caption.copyWith(color: Colors.white),)
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return _buildBody('Internet checking...');
  }
}

코드만 괜히 길 뿐.. 4초후 /auth페이지로 이동합니다.

auth.dart

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';

class AuthPage extends StatefulWidget {
  AuthPage({Key key}) : super(key: key);
  static const routeName = '/auth';

  @override
  _AuthPageState createState() => _AuthPageState();
}

class _AuthPageState extends State<AuthPage> {
  StreamSubscription<FirebaseUser> _subscriptionAuth;
  String _message = 'loading...';

  @override
  void initState() { 
    super.initState();
    _streamOpen();
  }

  @override
  void dispose() {
    _subscriptionAuth.cancel();
    super.dispose();
  }

  _streamOpen() {
    _subscriptionAuth = FirebaseAuth.instance.onAuthStateChanged.listen((fu) {
      if (fu == null) return Navigator.pushReplacementNamed(context, '/signin');
      return Navigator.pushReplacementNamed(context, '/home');
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Account check')
      ),
      body: Container(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(_message),
              CircularProgressIndicator(),
            ],
          ),
        ),
      ),
    );
  }
}

시작할 때(initState) 계정 감시(스트림 리슨)를 시작합니다.

스트림 리슨을 할 때 구독(subscription)을 받아 놓은 이유는 나갈 때를 위함입니다.

리슨하고 있다가 user가 null이면 로그인 페이지로, 있으면 홈페이지로 이동시킵니다.

주의할 점은 페이지 나갈 때(dispose) 구독을 해지해줘야합니다.

구독 해지 없이 나가면 다른페이지로 라우트시 워닝메세지가 뜹니다.

역시 코드만 길 뿐 그다지 하는 일도 없습니다.

signin.dart

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';

class SignInPage extends StatefulWidget {
  SignInPage({Key key}) : super(key: key);
  static const routeName = '/signin';

  @override
  _SignInPageState createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {
  bool _loading = false;

  _googleSignIn () async {
    final bool isSignedIn = await GoogleSignIn().isSignedIn();
    GoogleSignInAccount googleUser;
    if (isSignedIn) googleUser = await GoogleSignIn().signInSilently();
    else googleUser = await GoogleSignIn().signIn();
    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;

    final AuthCredential credential = GoogleAuthProvider.getCredential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );

    final FirebaseUser user = (await FirebaseAuth.instance.signInWithCredential(credential)).user;
    // print("signed in " + user.displayName);
    return user;
  }

  _buildLoading() {
    return Center(child: CircularProgressIndicator(),);
  }

  _buildBody() {
    return Column(
      children: <Widget>[
        RaisedButton(
          child: Text('google login'),
          onPressed: () async {
            setState(() => _loading = true);
            await _googleSignIn();       
            setState(() => _loading = false);
            // Navigator.pushReplacementNamed(context, '/');
            // Navigator.pushReplacementNamed(context, '/auth');
            Navigator.pushReplacementNamed(context, '/home');
          },
        ),
        RaisedButton(
          child: Text('register'),
          onPressed: () {       
            Navigator.pushNamed(context, '/signup');
          },
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SignInPage'), ),
      body: _loading ? _buildLoading() : _buildBody()
    );
  }
}

예전에 썼던 구글로그인 코드에 이미 로그인이 되어있을 경우 조용히 로그인하는 기능만 조금 추가해봤습니다.

로그인이 정상적으로 된 경우 현재는 /, /auth, /home 모두 결국 /home으로 가게 됩니다.

/의 경우 광고차.. 스플래시를 한번 더 보여줄 수도 있습니다. /auth의 경우 이메일 혹은 권한 관련해서 판단을 받아야할 수 있습니다.

마치며

모양은 별로지만 미리 구성을 간단히 해놓으면 추후 각 페이지만 좀 더 근사하게 포장하면 됩니다.

사실 웹도 똑같이 빈페이지 만들어 놓고 작업합니다.

현재 소스의 로그인 시나리오는 완벽하지 않을 것입니다.

플러터 관련 게시물들을 둘러봐도 로그인 시나리오는 너무나도 다양합니다.

10년간 많이도 다양한 플랫폼(웹, 임베디드, 모바일등)에서 여러가지 형태로 구현해본들 만족한 적은 단 한번도 없었던 것 같습니다.

과거에 구현해 놓은 시나리오는 도자기를 깨는 것 처럼 수천번은 갈아 엎었던 것 같습니다.

그런 의미에서 과거 강의에 있던 로그인기능은 참고만 하시고 개선해서 사용하시기 바랍니다. 저는 다 바꿨답니다..(바뀐 내용은 강의 리뉴얼 때..)

절대 자신의 코드를 과신하지 말고, 자신도 모르게 만들어 놓은 복잡하고 의미없는 규칙에 얽히고 있지는 않은 지.. 항상 돌아보는 것이 좋습니다.

굵직한 흐름을 읽어야 도자기를 부수고 다시 구울 수 있습니다.

개발자라면 항상 완벽하진 못해도 완벽해지기 위한 노력이 더 중요한 것이라고 생각합니다.

소스

이제부터 강의 코드가 길어질 것 같아서 깃헙에 등록해두었습니다.

https://github.com/fkkmemi/ff2

영상

댓글남기기