Flutterで始めるアプリ開発
https://www.flutter-study.dev/
大変参考になるサイトで、勉強に使わせてもらっているが、
Riverpodで状態管理
https://www.flutter-study.dev/firebase-app/riverpod
上記ページのサンプルが、
flutter_riverpodの更新等で、色々エラーが出て動かなかった。
初心者が2023.03時点でとりあえず動くように修正してみたので、誰かの参考になるかもと思い共有してみる。
flutter –version
Flutter 3.7.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision b4bce91dd0 (3 weeks ago) • 2023-02-21 09:50:50 +0800
Engine • revision 248290d6d5
Tools • Dart 2.19.2 • DevTools 2.20.1
pubspec.yamlは下記の状態
firebase_core: ^2.8.0
firebase_auth: ^4.3.0
cloud_firestore: ^4.4.5
flutter_riverpod: ^2.3.2
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ユーザー情報の受け渡しを行うためのProvider
final userProvider = StateProvider((ref) {
return FirebaseAuth.instance.currentUser;
});
// エラー情報の受け渡しを行うためのProvider
// ※ autoDisposeを付けることで自動的に値をリセットできます
final infoTextProvider = StateProvider.autoDispose((ref) {
return '';
});
// メールアドレスの受け渡しを行うためのProvider
// ※ autoDisposeを付けることで自動的に値をリセットできます
final emailProvider = StateProvider.autoDispose((ref) {
return '';
});
// パスワードの受け渡しを行うためのProvider
// ※ autoDisposeを付けることで自動的に値をリセットできます
final passwordProvider = StateProvider.autoDispose((ref) {
return '';
});
// メッセージの受け渡しを行うためのProvider
// ※ autoDisposeを付けることで自動的に値をリセットできます
final messageTextProvider = StateProvider.autoDispose((ref) {
return '';
});
// StreamProviderを使うことでStreamも扱うことができる
// ※ autoDisposeを付けることで自動的に値をリセットできます
final postsQueryProvider = StreamProvider.autoDispose((ref) {
return FirebaseFirestore.instance
.collection('posts')
.orderBy('date')
.snapshots();
});
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
// Replace with actual values
options: const FirebaseOptions(
apiKey: "****",
appId: "****",
messagingSenderId: "****",
projectId: "****",
),
);
runApp(
const ProviderScope(
child: ChatApp(),
),
);
}
class ChatApp extends StatelessWidget {
const ChatApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// アプリ名
title: 'ChatApp',
theme: ThemeData(
// テーマカラー
primarySwatch: Colors.blue,
),
// ログイン画面を表示
home: const LoginPage(),
);
}
}
// ログイン画面用Widget
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final infoText = ref.watch(infoTextProvider);
final email = ref.watch(emailProvider);
final password = ref.watch(passwordProvider);
return Scaffold(
body: Center(
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// メールアドレス入力
TextFormField(
decoration: const InputDecoration(labelText: 'メールアドレス'),
onChanged: (String value) {
ref.read(emailProvider.notifier).state = value;
},
),
// パスワード入力
TextFormField(
decoration: const InputDecoration(labelText: 'パスワード'),
obscureText: true,
onChanged: (String value) {
ref.read(passwordProvider.notifier).state = value;
},
),
Container(
padding: const EdgeInsets.all(8),
// メッセージ表示
child: Text(infoText),
),
SizedBox(
width: double.infinity,
// ユーザー登録ボタン
child: ElevatedButton(
child: const Text('ユーザー登録'),
onPressed: () async {
try {
// メール/パスワードでユーザー登録
final FirebaseAuth auth = FirebaseAuth.instance;
final result = await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// ユーザー情報を更新
ref.read(userProvider.notifier).state = result.user;
// ユーザー登録に成功した場合
// チャット画面に遷移+ログイン画面を破棄
await Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) {
return ChatPage();
}),
);
} catch (e) {
// ユーザー登録に失敗した場合
ref.read(infoTextProvider.notifier).state = "登録に失敗しました:${e.toString()}";
}
},
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
// ログイン登録ボタン
child: OutlinedButton(
child: const Text('ログイン'),
onPressed: () async {
try {
// メール/パスワードでログイン
final FirebaseAuth auth = FirebaseAuth.instance;
await auth.signInWithEmailAndPassword(
email: email,
password: password,
);
// ログインに成功した場合
// チャット画面に遷移+ログイン画面を破棄
await Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) {
return ChatPage();
}),
);
} catch (e) {
// ログインに失敗した場合
ref.read(infoTextProvider.notifier).state = "ログインに失敗しました:${e.toString()}";
}
},
),
),
],
),
),
),
);
}
}
// チャット画面用Widget
class ChatPage extends ConsumerWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ユーザー情報を受け取る
final user = ref.watch(userProvider);
final AsyncValue<QuerySnapshot> asyncPostsQuery = ref.watch(postsQueryProvider);
return Scaffold(
appBar: AppBar(
title: const Text('チャット'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
// ログアウト処理
// 内部で保持しているログイン情報等が初期化される
// (現時点ではログアウト時はこの処理を呼び出せばOKと、思うぐらいで大丈夫です)
await FirebaseAuth.instance.signOut();
// ログイン画面に遷移+チャット画面を破棄
await Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) {
return LoginPage();
}),
);
},
),
],
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
child: Text('ログイン情報:${user!.email}'),
),
Expanded(
// StreamBuilder
// 非同期処理の結果を元にWidgetを作れる
child: StreamBuilder<QuerySnapshot>(
// 投稿メッセージ一覧を取得(非同期処理)
// 投稿日時でソート
stream: FirebaseFirestore.instance
.collection('posts')
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
// データが取得できた場合
if (snapshot.hasData) {
final List<DocumentSnapshot> documents = snapshot.data!.docs;
// 取得した投稿メッセージ一覧を元にリスト表示
return ListView(
children: documents.map((document) {
return Card(
child: ListTile(
title: Text(document['text']),
subtitle: Text(document['email']),
// 自分の投稿メッセージの場合は削除ボタンを表示
trailing: document['email'] == user.email
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
// 投稿メッセージのドキュメントを削除
await FirebaseFirestore.instance
.collection('posts')
.doc(document.id)
.delete();
},
)
: null,
),
);
}).toList(),
);
}
// データが読込中の場合
return const Center(
child: Text('読込中...'),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () async {
// 投稿画面に遷移
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return AddPostPage();
}),
);
},
),
);
}
}
// 投稿画面用Widget
class AddPostPage extends ConsumerWidget {
const AddPostPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ユーザー情報を受け取る
final user = ref.watch(userProvider);
final messageText = ref.watch(messageTextProvider);
return Scaffold(
appBar: AppBar(
title: const Text('チャット投稿'),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 投稿メッセージ入力
TextFormField(
decoration: const InputDecoration(labelText: '投稿メッセージ'),
// 複数行のテキスト入力
keyboardType: TextInputType.multiline,
// 最大3行
maxLines: 3,
onChanged: (String value) {
ref.read(messageTextProvider.notifier).state = value;
},
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
child: const Text('投稿'),
onPressed: () async {
final date =
DateTime.now().toLocal().toIso8601String(); // 現在の日時
final email = user!.email; // AddPostPage のデータを参照
// 投稿メッセージ用ドキュメント作成
await FirebaseFirestore.instance
.collection('posts') // コレクションID指定
.doc() // ドキュメントID自動生成
.set({
'text': messageText,
'email': email,
'date': date
});
// 1つ前の画面に戻る
Navigator.of(context).pop();
},
),
)
],
),
),
),
);
}
}