2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutterでログイン状態を保持しようとしたら苦戦した話

Posted at

毎回ログインするのめんどくさい。

ログイン画面を作って、認証できるようにした結果、デバッグを再起動する際に毎回ログインするのがめんどくさい…

そういえば、普通のアプリは毎回ログインしないよなと思ってログイン状態を保持する方法を調べてみました。

とりあえずここを見とけば間違いない

調べた結果、Firebaseの機能でできるみたい。
こちらのサイトこちらの記事
でほぼやりたいことはできていました。
大変助かりました。ありがとうございます。

やってること

firebase_authを使う

元々Firebaseの認証を使っていたので、ここはそのままでOK。
なければpubspec.yamlに追加。

dependencies:
  firebase_auth: ^4.17.4

FirebaseAuth.instance.authStateChanges()でログイン状態を確認

Firebaseのドキュメントを確認すると、

Firebase Auth では、Stream を使用して、この状態をリアルタイムで取得できます。Stream が呼び出されると、ユーザーの現在の認証状態を即時に提供します。また、認証状態が変更されるたびに、後続のイベントを提供します。

公式ドキュメント

で、authStateChanges ()メソッドを使用すると、ユーザーがログインしてるかどうかわかるということでした。

StreamBuilder を使って上記のメソッドを呼び出す

とりあえず参考先と同じように。

main.dart


class App extends StatelessWidget { 
  @override
    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 Home();
    }
    // User が null である、つまり未サインインのサインイン画面へ
    return LoginPage();
     },
   ),
  );
}

なんとなくできた。けど…

ユーザーの認証状態に応じて、ログインページとログイン後のページを切り替えることはできました。
ただ、ログイン時にユーザーを取得して、ログイン後のページに反映させたい場合、上記のユーザーがnullじゃない状態のときにユーザーの情報を取得する内容を追加しなければなりませんでした。

とりあえず追加してみる

もとからあるgetUser関数

class UserFirestore{
  static Future<dynamic> getUser(String uid) async {  
  try {  
    DocumentSnapshot documentSnapshot = await users.doc(uid).get();  
    if (documentSnapshot.data() != null) {  
      Map<String, dynamic> data = documentSnapshot.data() as Map<String, dynamic>;  
      Account myAccount = Account(  
        id: uid,  
        name: data['name'],  
        email: data['email'],  
        imagePath: data['image_path'],  
        createdTime: data['created_time'],  
      );  
      Authentication.myAccount = myAccount;  
      print('ユーザー取得完了');  
      return true;  
    } else {  
      return false;  
    }  
  } on FirebaseException catch (e) {  
    print('ユーザー取得エラー: $e');  
    return false;  
  }  
}
}

snapshotがデータを持っていた時に動くようにする

main.dart 


if (snapshot.hasdata) {
  UserFirestore .getUser(snapshot.data!.uid);
  return const Home();
  
}

だめでした。到達はしますが、一回Null check operator used on a null value.のエラー画面を挟んでしまいます。

nullを許容してみますが、ユーザーが取得できていない状態でもログイン後のページに遷移してしまうようになり断念。

というか、よくよく観察していると、どうもStreamBuilderのstreamが2回走っている??

StreamBuilder を学ぶ

なんでかよくわからないので、一旦StreamBuilder の公式を読み、サンプルを取得して動かしてみました。

class StreamBuilderExample extends StatefulWidget {  
  const StreamBuilderExample({super.key});  
  
  @override  
  State<StreamBuilderExample> createState() => _StreamBuilderExampleState();  
}  
  
class _StreamBuilderExampleState extends State<StreamBuilderExample> {  
  final Stream<int> _bids = (() {  
    late final StreamController<int> controller;  
    controller = StreamController<int>(  
      onListen: () async {  
        await Future<void>.delayed(const Duration(seconds: 3));  
        print('seconds 3');  
        controller.add(3);  
        await Future<void>.delayed(const Duration(seconds: 1));  
        print('seconds 1');  
        await controller.close();  
        print('close');  
      },  
    );  
    return controller.stream;  
  })();  
  
  @override  
  Widget build(BuildContext context) {  
    return DefaultTextStyle(  
      style: Theme.of(context).textTheme.displayMedium!,  
      textAlign: TextAlign.center,  
      child: Container(  
        alignment: FractionalOffset.center,  
        color: Colors.white,  
        child: StreamBuilder<int>(  
          stream: _bids,  
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {  
            List<Widget> children;  
            print('start');  
            if (snapshot.hasError) {  
              children = <Widget>[  
                const Icon(  
                  Icons.error_outline,  
                  color: Colors.red,  
                  size: 60,  
                ),  
                Padding(  
                  padding: const EdgeInsets.only(top: 16),  
                  child: Text('Error: ${snapshot.error}'),  
                ),  
                Padding(  
                  padding: const EdgeInsets.only(top: 8),  
                  child: Text('Stack trace: ${snapshot.stackTrace}'),  
                ),  
              ];  
            } else {  
              switch (snapshot.connectionState) {  
                case ConnectionState.none:  
                  print('none');  
                  children = const <Widget>[  
                    Icon(  
                      Icons.info,  
                      color: Colors.blue,  
                      size: 60,  
                    ),  
                    Padding(  
                      padding: EdgeInsets.only(top: 16),  
                      child: Text('Select a lot'),  
                    ),  
                  ];  
                case ConnectionState.waiting:  
                  print('waiting');  
                  children = const <Widget>[  
                    SizedBox(  
                      width: 60,  
                      height: 60,  
                      child: CircularProgressIndicator(),  
                    ),  
                    Padding(  
                      padding: EdgeInsets.only(top: 16),  
                      child: Text('Awaiting bids...'),  
                    ),  
                  ];  
                case ConnectionState.active:  
                  print('active');  
                  children = <Widget>[  
                    const Icon(  
                      Icons.check_circle_outline,  
                      color: Colors.green,  
                      size: 60,  
                    ),  
                    Padding(  
                      padding: const EdgeInsets.only(top: 16),  
                      child: Text('\$${snapshot.data}'),  
                    ),  
                  ];  
                case ConnectionState.done:  
                  print('done');  
                  children = <Widget>[  
                    const Icon(  
                      Icons.info,  
                      color: Colors.blue,  
                      size: 60,  
                    ),  
                    Padding(  
                      padding: const EdgeInsets.only(top: 16),  
                      child: Text('\$${snapshot.data} (closed)'),  
                    ),  
                  ];  
              }  
            }  
  
            return Column(  
              mainAxisAlignment: MainAxisAlignment.center,  
              children: children,  
            );  
          },  
        ),  
      ),  
    );  
  }  
}

switchを使ってそれぞれのConnectionStateごとに分けてる方法が紹介されていました。

わかりやすいようにそれぞれのcaseごとに何をしているかprintを追加しておきます。

2回走るのはFirebaseAuth のくせなのかと思いましたが、こちらもdoneになるまで繰り返します。

なるほど。streamの処理をこなすごとに最初からビルドされる。で、doneになるまで動いてる、と。

ということは、FirebaseAuthのConnectionState を見ればいいわけか?と仮説をたてました。

修正してみる

StremaBuilderドキュメントのサンプルを参考にswitch文にし、ConnectionState の値を見てdoneになったらログイン後のページに遷移するように修正してみましたが、doneに到達しません。

どうもactiveの状態で止まっているようです。

FirebaseAuth のauthStateChanges は状態を監視しているため、完了せずずっとactiveになっているのではないかと思い、active判定しているcaseに処理を書いたらその通りでした。

ただ、その状態だと結局2回以上処理が走ってしまうのは変わりませんでした。

なので、サンプルを参考に処理の関数を作成します。

final Stream<User?> _stream = (() {  
  late final StreamController<User?> controller;  
  controller = StreamController<User?> (  
    onListen: () async {  
      await Future<void>.delayed(const Duration(seconds: 3));  
      print('duration 3seconds');  
      FirebaseAuth.instance.authStateChanges().listen((User? user) {  
        if (user == null) {  
          print('user is null');  
          controller.close();  
        } else {  
          print('user is signe in');  
          controller.add(user);  
          controller.close();  
        }  
      });  
    }  
  );  
  return controller.stream;  
})();

これでConnectionState がdoneの状態になるとStreamBuilderのsnapshotにuserのデータが入ります。

あとはcasa ConnectionState.doneに以下の処理を追加。

StreamBuilder <User?> (
  stream: _stream,
  builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
    Widget nextPage = const SizedBox();
    if (snapshot.hasError) {
      // errorの画面 
    } else {
      switch (snapshot.connectionState ) {
        // ConnectionState ごとの処理 
        case ConnectionState.done:  
  print('done');  
  if (snapshot.hasData) {  
    nextPage = FutureBuilder(  
        future: UserFirestore.getUser(snapshot.data!.uid),  
        builder: (BuildContext context, futureSnapshot) {  
          if (futureSnapshot.connectionState == ConnectionState.waiting) {  
            return const Center(  
              child: SizedBox(  
                width: 60,  
                height: 60,  
                child: CircularProgressIndicator(color: Colors.deepOrangeAccent, ),  
              ),  
            );  
          }  
          // return const CalendarPage();  
          print(futureSnapshot.connectionState);  
          if (futureSnapshot.hasData) {  
            return const Home();  
          } else {  
            return Container(  
              color: Colors.white,  
            );  
          }  
        });  
  } else {  
    return const LoginPage();  
  }
      }
    }
    return nextPage;
  }
)

これでユーザー取得が何回も動くことなく、かつエラー画面やログイン画面を挟むことなくアカウント情報を持ったログインページに進むことができました。

解決

今回何気なく使っていたStreamBuilder の動きがわからなくて、右往左往してしまいました。まだ全然理解できてませんが。
それでも思った通りの結果が得られたのは嬉しかったです。

ただ、_state関数の中でDurationを外すと処理の順番がごちゃごちゃになって処理が2回動いてしまうのが謎として残っています。

また、Flutter初心者なため、もっといい書き方やここはこうしたほうがいいなどありましたら教えていただけると助かります。
読んでいただいた方、最後までありがとうございました。

参考

2
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?