FirestoreでSNSぽい設計をしてみる①
二つのコレクションのデータを同じ場所に表示したい
タイトルが、SNSぽい設計と書きましたが、実際のところは実験で作ったので、それぽいものではないですね。
今回やること
- 二つのコレクションのデータを取得して、同じカードの中に表示.
- 一つのコレクションに全てのデータを保存しないので、正規化している.
- Firestoreは、非正規化するものですけど、分けないと必要ないデータをuserコレクションは持ってしまう.
🥏こんな設計にした
userコレクションIDは、nameフィールドしか今回ありません。画像も欲しいですが、今回は省きます。ダミーのデータを作成するときは、userコレクションもtaskコレクションもドキュメントIDは同じものを使用してください。
簡単ですけど、DBの設計図です。
userコレクションIDは、String型のnameフィールド1個だけです。
taskコレクションIDは、taskはString型、likesは、array型で、このなかに今回は仮の0x1111
というString型のダミーのIDを入れていますが、実際には、UIDを保存して、これを数えていいねしてくれた数を数えます。
commentsは、array型で、他のユーザーのテキスト情報をString型で保存して、数えてコメント数を数えます。
userより先に、taskを作成したら、画面に表示されて変なUIになるのではと思ったのですが、userコレクションのデータが表示されていないと、taskは表示されないようです?
これであってるか疑問ですけど???
🖊️ソースコード
今回は、riverpod generatorとFreezedを使用しています。全体のコードはGithubのレポジトリを見ていただいた方が良いと思われます。
userモデル
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
class User with _$User {
const factory User({
('') String name,
}) = _User;
factory User.fromJson(Map<String, Object?> json)
=> _$UserFromJson(json);
}
taskモデル
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'task.freezed.dart';
part 'task.g.dart';
class Task with _$Task {
const factory Task({
('') String task,
([]) List<String> likes,
([]) List<String> comments,
}) = _Task;
factory Task.fromJson(Map<String, Object?> json)
=> _$TaskFromJson(json);
}
Firebaseと接続するプロバイダー
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:task_app/model/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../model/task.dart';
part 'firebase_provider.g.dart';
FirebaseFirestore firebaseFirestore(FirebaseFirestoreRef ref) {
return FirebaseFirestore.instance;
}
Stream<List<User>> userStream(UserStreamRef ref) {
final firestore = ref.watch(firebaseFirestoreProvider);
return firestore
.collection('user').limit(5)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => User.fromJson(doc.data())).toList());
}
Stream<List<Task>> taskStream(TaskStreamRef ref) {
final firestore = ref.watch(firebaseFirestoreProvider);
return firestore
.collection('task').limit(5)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => Task.fromJson(doc.data())).toList());
}
SNSぽいUIを表示するページ
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:task_app/provider/firebase_provider.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
);
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(userStreamProvider);
final tasks = ref.watch(taskStreamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Task App'),
),
body: users.when(
data: (users) => tasks.when(
data: (tasks) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
final task = tasks[index];
return SizedBox(
width: 200,
height: 150,
child: Card(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(user.name)),
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: Text(task.task)),
const SizedBox(height: 10),
Row(
children: [
Text('いいね ${task.likes.length.toString()}'),
Text('コメント ${task.comments.length.toString()}'),
],
),
],
),
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text(error.toString())),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text(error.toString())),
),
);
}
}
こんな感じのUIになります
最後に
今回は、SNSのUIを見ていて、userコレクションIDに全部データ入ってるはずないなと思い、taskコレクションIDを作成して、そこからデータを取得して、whenメソッドをネストさせて、同じCart Widgetに、2つのコレクションのデータを表示できるようにしました。
Firestoreには、参照型というものもあるのですが、これはなんども読み取ると、読み取りコストがかかるらしいです?
TOPレベルのコレクション(別々のコレクション)ってことですね。サブコレクションとかにしないで、データを取得して表示してます。
やったとしてもwhenメソッドをネストさせてる気がします?
完成品のソースコード
Discussion