💽

StreamProviderでログインを維持する

2022/12/27に公開

最近流行りの技術構成で作った!

RiverpodのStreamProviderでログイン状態を維持する方法があったので、興味があって今回demoアプリを作ってみました。
画面遷移にはgo_routerを使用しました。versionが新しすぎると不具合があるので、2個手前のちょっと古いpackageを使用しました。

今回参考にした情報はFlutter界隈では有名なアンドレアさんWebサイトです。
https://codewithandrea.com/articles/flutter-state-management-riverpod/

完成したコード

https://github.com/sakurakotubaki/RIverpodAuthApp

  • アプリのディレクトリ構成
    • serviceにproviderとgo_routerの設定ファイルを作成.
    • uiに認証関係のauthとログイン後のページを表示するpageを作成.
lib
├── firebase_options.dart
├── main.dart
├── service
│   ├── firebase_provider.dart
│   └── router.dart
└── ui
    ├── auth
    │   └── signup_page.dart
    └── page
        └── my_page.dart

アプリの実行とログインしているかしていないかで、画面遷移先を設定するmain.dart

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_auth/firebase_options.dart';
import 'package:riverpod_auth/service/firebase_provider.dart';
import 'package:riverpod_auth/service/router.dart';
import 'package:riverpod_auth/ui/auth/signup_page.dart';
import 'package:riverpod_auth/ui/page/my_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

// go_routerで最初に呼び出されるページ。何も表示されることはない.
// ログインしていなければ認証のページに移動し、ログインして入れば、
// ログイン後のページへ移動する.
class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // StreamProvider を監視し、AsyncValue<User?> を取得する。
    final authStateAsync = ref.watch(authStateChangesProvider);
    // パターンマッチングを使用して、状態をUIにマッピングする
    return authStateAsync.when(
      data: (user) => user != null ? MyPage() : SignUpPage(),
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

Providerとgo_routerの設定をするserviceディレクトリのファイル

FirebaseとTextEditingControllerの設定

service/provider.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
  // 以下のプロバイダからFirebaseAuthを取得します。
  final firebaseAuth = ref.watch(firebaseAuthProvider);
  // Stream<User?> を返すメソッドを呼び出す。
  return firebaseAuth.authStateChanges();
});

// プロバイダを使用して、FirebaseAuth インスタンスにアクセスします。
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
  return FirebaseAuth.instance;
});
// メールアドレスのテキストを保存するProvider
final emailProvider = StateProvider.autoDispose((ref) {
  return TextEditingController(text: '');
});

final passwordProvider = StateProvider.autoDispose((ref) {
  // パスワードのテキストを保存するProvider
  return TextEditingController(text: '');
});

go_routerでルーティングの設定。
変数を_routerと書くとPrivateな変数になるからなのか、他のファイルで呼び出せなくなる!

service
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_auth/ui/auth/signup_page.dart';
import 'package:riverpod_auth/ui/page/my_page.dart';

import '../main.dart';

/// GoRouterの画面遷移の設定
/// 変数は、_routerとすると、
/// privateになるからか、他のファイルで呼び出せなかった!
final GoRouter router = GoRouter(
  routes: <GoRoute>[
    /// 最初のページのページへ移動する設定
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) => const HomePage(),
      routes: <GoRoute>[
        /// パスワードをリセットするページへ画面遷移する設定
        GoRoute(
          path: 'auth',
          builder: (BuildContext context, GoRouterState state) =>
              const SignUpPage(),
        ),

        /// ログイン後のページへ画面遷移する設定
        GoRoute(
          path: 'mypage',
          builder: (BuildContext context, GoRouterState state) =>
              const MyPage(),
        ),
      ],
    ),
  ],
);

アプリの画面のuiディレクトリ
認証機能を使用するページ。
今回は、新規登録とログインページ分けてません!
すいません🙇‍♂️

page/auth/signup_dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_auth/service/firebase_provider.dart';

/// 認証のページ.
/// 今回は新規登録とログインは同じページにしました.
class SignUpPage extends ConsumerWidget {
  const SignUpPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final emailController = ref.watch(emailProvider);
    final passwordlController = ref.watch(passwordProvider);
    return Scaffold(
      appBar: AppBar(
        centerTitle: true, // AndroidのAppBarの文字を中央寄せ.
        automaticallyImplyLeading: false, //戻るボタンを消す.
        title: Text('新規登録'),
      ),
      body: Center(
        child: Container(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                controller: emailController,
                decoration: const InputDecoration(labelText: 'メールアドレス'),
              ),
              TextField(
                controller: passwordlController,
                decoration: const InputDecoration(labelText: 'パスワード'),
                obscureText: true,
              ),
              ElevatedButton(
                child: const Text('ユーザ登録'),
                onPressed: () async {
                  try {
                    final User? user = (await FirebaseAuth.instance
                            .createUserWithEmailAndPassword(
                                email: emailController.text,
                                password: passwordlController.text))
                        .user;
                    if (user != null) {}
                  } catch (e) {
                    print(e);
                  }
                },
              ),
              ElevatedButton(
                child: const Text('ログイン'),
                onPressed: () async {
                  try {
                    // メール/パスワードでログイン
                    final User? user = (await FirebaseAuth.instance
                            .signInWithEmailAndPassword(
                                email: emailController.text,
                                password: passwordlController.text))
                        .user;
                    if (user != null) context.go('/mypage');
                  } catch (e) {
                    print(e);
                  }
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

ログイン後の画面を表示するmypage

ui/page/my_page.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

// Login後のページ
class MyPage extends ConsumerWidget {
  const MyPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () async {
              // ログアウト処理
              // 内部で保持しているログイン情報等が初期化される
              await FirebaseAuth.instance.signOut();
              context.go('/auth');
            },
          ),
        ],
        title: Text('HomePage'),
      ),
      body: Text('Welcome! Login状態!'),
    );
  }
}

スクリーンショット

  • 最初の画面
  • 新規登録してログインする

ログイン後の画面

アプリを停止して、Runして再起動すると、ログインの状態を維持できていた!

最後に

技術のキャッチアップが遅れていたので、Riverpod + go_routerを組み合わせた技術構成でアプリが最近まで作れませんでした!
というより、新しいものばかりに手を出しすぎるとサポートされなくなって、代替手段を探さないといけないので、リスクを考えると怖くて個人開発では使わなかったですね。

Discussion