💾

Flutterでログイン後の状態を維持する

2022/07/12に公開

どうやってアプリを停止してログイン後の状態を維持するのか?

Firebaseが元々持っている機能を使えば、shared_preferencesというDartパッケージを使わなくてもログイン後の状態を維持できるらしい?

公式ドキュメント
https://firebase.google.com/docs/auth/flutter/start#auth-state

とはいえ、これだけでは分かりませんでしたのMENTAでコーチングをしてくれたモバイルエンジニアのTOKUYAMAさんより、こちらの記事をご紹介いただきました!

https://qiita.com/KosukeSaigusa/items/b19ec19379c5a2e4ceb2

Kosukeさん、「ほほ〜、イケメンですね🥰」...
Kosukeさんの記事を参考にFirebaseでログインした後、ログイン後の状態を維持する機能を作ってみました。

こちらが、完成したサンプルでございます😇

動画を加工したGifの状態が悪かったので、今回は動く画像はなしです🙇‍♂️

pubspec.yaml

name: takuma_sample
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: ">=2.16.1 <3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  firebase_core: ^1.19.1
  firebase_auth: ^3.4.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^1.0.0

# For information on the generic Dart part of this file, see the
# following time_line: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware.

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

ログイン判定を行うファイル

main.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:takuma_sample/login.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(App());
}

class App extends StatelessWidget {
  
  Widget build(BuildContext context) => MaterialApp(
        title: 'Flutter app',
        home: StreamBuilder<User?>(
          stream: FirebaseAuth.instance.authStateChanges(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              // スプラッシュ画面などに書き換えても良い
              return const SizedBox();
            }
            if (snapshot.hasData) {
              // User が null でなない、つまりサインイン済みのホーム画面へ
              return MainContent();
            }
            // User が null である、つまり未サインインのサインイン画面へ
            return UserLogin();
          },
        ),
      );
}

ログインページ

login.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:takuma_sample/sign_up.dart';

class UserLogin extends StatefulWidget {
  const UserLogin({Key? key}) : super(key: key);

  
  _UserLogin createState() => _UserLogin();
}

class _UserLogin extends State<UserLogin> {
  final _auth = FirebaseAuth.instance;

  String email = '';
  String password = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ログイン'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                email = value;
              },
              decoration: const InputDecoration(
                hintText: 'メールアドレスを入力',
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                password = value;
              },
              obscureText: true,
              decoration: const InputDecoration(
                hintText: 'パスワードを入力',
              ),
            ),
          ),
          ElevatedButton(
            child: const Text('ログイン'),
            onPressed: () async {
              try {
                final newUser = await _auth.signInWithEmailAndPassword(
                    email: email, password: password);
                if (newUser != null) {
                  Navigator.pushReplacement(context,
                      MaterialPageRoute(builder: (context) => MainContent()));
                }
              } on FirebaseAuthException catch (e) {
                if (e.code == 'invalid-email') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(''),
                    ),
                  );
                  print('メールアドレスのフォーマットが正しくありません');
                } else if (e.code == 'user-disabled') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('現在指定したメールアドレスは使用できません'),
                    ),
                  );
                  print('現在指定したメールアドレスは使用できません');
                } else if (e.code == 'user-not-found') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('指定したメールアドレスは登録されていません'),
                    ),
                  );
                  print('指定したメールアドレスは登録されていません');
                } else if (e.code == 'wrong-password') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('パスワードが間違っています'),
                    ),
                  );
                  print('パスワードが間違っています');
                }
              }
            },
          ),
          TextButton(
              onPressed: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => Register()));
              },
              child: Text('新規登録はこちらから'))
        ],
      ),
    );
  }
}

class MainContent extends StatefulWidget {
  const MainContent({Key? key}) : super(key: key);

  
  _MainContentState createState() => _MainContentState();
}

class _MainContentState extends State<MainContent> {
  //ステップ1
  final _auth = FirebaseAuth.instance;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('成功'),
        actions: [
          IconButton(
            //ステップ2
            onPressed: () async {
              await _auth.signOut();
              if (_auth.currentUser == null) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('ログアウトしました'),
                  ),
                );
                print('ログアウトしました!');
              }
              Navigator.pushReplacement(context,
                  MaterialPageRoute(builder: (context) => UserLogin()));
            },
            icon: Icon(Icons.close),
          ),
        ],
      ),
      body: Center(
        child: Text('ログイン成功!'),
      ),
    );
  }
}

新規登録ページ

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

import 'login.dart';

class Register extends StatefulWidget {
  const Register({Key? key}) : super(key: key);

  
  _RegisterState createState() => _RegisterState();
}

class _RegisterState extends State<Register> {
  //ステップ1
  final _auth = FirebaseAuth.instance;

  String email = '';
  String password = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新規登録'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                email = value;
              },
              decoration: const InputDecoration(
                hintText: 'メールアドレスを入力',
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: TextField(
              onChanged: (value) {
                password = value;
              },
              obscureText: true,
              decoration: const InputDecoration(
                hintText: 'パスワードを入力',
              ),
            ),
          ),
          ElevatedButton(
            child: const Text('新規登録'),
            //ステップ2
            onPressed: () async {
              try {
                final newUser = await _auth.createUserWithEmailAndPassword(
                    email: email, password: password);
                if (newUser != null) {
                  Navigator.pushReplacement(context,
                      MaterialPageRoute(builder: (context) => MainContent()));
                }
              } on FirebaseAuthException catch (e) {
                if (e.code == 'email-already-in-use') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('指定したメールアドレスは登録済みです'),
                    ),
                  );
                  print('指定したメールアドレスは登録済みです');
                } else if (e.code == 'invalid-email') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('メールアドレスのフォーマットが正しくありません'),
                    ),
                  );
                  print('メールアドレスのフォーマットが正しくありません');
                } else if (e.code == 'operation-not-allowed') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('指定したメールアドレス・パスワードは現在使用できません'),
                    ),
                  );
                  print('指定したメールアドレス・パスワードは現在使用できません');
                } else if (e.code == 'weak-password') {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('パスワードは6文字以上にしてください'),
                    ),
                  );
                  print('パスワードは6文字以上にしてください');
                }
              }
            },
          )
        ],
      ),
    );
  }
}

使ってみた感想

普段使っているアプリってユーザー登録したら、ログインしたままの状態が維持されていますよね!
毎回、ログインするとユーザーさんがストレスなので、一度ログインしたら、アプリを再起動した時に、すぐに操作できるようにしたいですよね。

shared_preferencesって、そもそもどんな役割があるのか?
公式によると

Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data.

単純なデータのためのプラットフォーム固有の永続的ストレージをラップします (iOS と macOS の NSUserDefaults、Android の SharedPreferences など)。データは非同期にディスクに永続化される可能性があり、復帰後にディスクに書き込みが永続化される保証はないため、このプラグインは重要なデータの保存には使用しないでください。

スマートフォン本体に一時的にデータを保存するということか🤔

今回ご指導いただいたメンターさんの解説では

素早い対応ありがとうございます。ビルド通りました。
Zennの記事も拝見しました。

ご理解されたとおり、ログイン状態はFirebaseAuthが持っているのため、shared_preferencesで別に持たせる必要はないですね。

ちなみにshared_preferencesはパッケージの説明に「Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.).」とあるとおり、iOS/macOSの「NSUserDefaults」あるいはAndroidの「SharedPreferences」のFlutter版と言えます。

これらは主にアプリの「設定」項目を保持するのに使われてきました。
・ダークモードのON/OFF/自動
・コンテンツを表示する文字サイズ=小/標準/大
といった簡単なものをKey-Valueで記憶するものです。
このような内容はアプリが停止させられて再起動してきたときなどに読み込みたい情報で、大昔はファイルに書いていましたが最近はOSのAPIで簡単に覚えられるようになっています。

そういう意味ではアプリの動作中の「状態」を覚えさせるのは向いていないこともあります。ご参考まで。

目的に応じて使い分けが必要ですね。

今回学んだことは、現代ではAPIが便利な機能をあらかじめ用意しているので、自分達で作らなくてもできることがあるのを学びました😅

Discussion