毎回ログインするのめんどくさい。
ログイン画面を作って、認証できるようにした結果、デバッグを再起動する際に毎回ログインするのがめんどくさい…
そういえば、普通のアプリは毎回ログインしないよなと思ってログイン状態を保持する方法を調べてみました。
とりあえずここを見とけば間違いない
調べた結果、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初心者なため、もっといい書き方やここはこうしたほうがいいなどありましたら教えていただけると助かります。
読んでいただいた方、最後までありがとうございました。
参考