Open4
Firestoreでフォルダとファイルを作る
Taskで考えてみた
Firestoreで特定のフォルダごとにデータを管理しているようなデータ設計をしたい。
イメージは、別々のフォルダがありそのの中にファイルが複数ある。
Firestoreの設計:
Firestore
|
└─── tasks (コレクション)
|
└─── task1 (ドキュメント)
| |
| ├─── id (フィールド)
| ├─── title (フィールド)
| └─── contents (サブコレクション)
| |
| └─── content1 (ドキュメント)
| └─── content2 (ドキュメント)
| └─── content3 (ドキュメント)
|
└─── task2 (ドキュメント)
|
├─── id (フィールド)
├─── title (フィールド)
└─── contents (サブコレクション)
|
└─── content1 (ドキュメント)
└─── content2 (ドキュメント)
└─── content3 (ドキュメント)
データのモデルはこんな感じか?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:todo_fire/core/timestamp_converter.dart';
part 'task_state.freezed.dart';
part 'task_state.g.dart';
class TaskState with _$TaskState {
const factory TaskState({
('') String id,// 保存したdocuentIdを指定する
('') String title,
([]) List<TaskStateContent> contents,
}) = _TaskState;
factory TaskState.fromJson(Map<String, dynamic> json) => _$TaskStateFromJson(json);
}
class TaskStateContent with _$TaskStateContent {
const factory TaskStateContent({
('') String id,
('') String content,
() DateTime? createdAt,
}) = _TaskStateContent;
factory TaskStateContent.fromJson(Map<String, dynamic> json) => _$TaskStateContentFromJson(json);
}
List型は使ってなかった💦
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:todo_fire/core/timestamp_converter.dart';
part 'task_state.freezed.dart';
part 'task_state.g.dart';
class TaskState with _$TaskState {
const factory TaskState({
('') String id,// 保存したdocuentIdを指定する
('') String title,
}) = _TaskState;
factory TaskState.fromJson(Map<String, dynamic> json) => _$TaskStateFromJson(json);
}
class TaskStateContent with _$TaskStateContent {
const factory TaskStateContent({
('') String id,
('') String content,
() DateTime? createdAt,
}) = _TaskStateContent;
factory TaskStateContent.fromJson(Map<String, dynamic> json) => _$TaskStateContentFromJson(json);
}
どのようなプロバイダーを作成する???
フォルダを作るのは簡単だろう。特定するためのidは保存しておいた方が良い。問題は、📁フォルダ1, 📁フォルダ2を作ったときだ。
📁フォルダ1, に情報が📄ファイル1, 📄ファイル2って感じで入ってる。ファイルにもidは必要だろうと思う。
Providerでどのフォルダか特定するには、次のページへ画面遷移したときに、.family
でdocumentIdを渡す必要があった。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:todo_fire/model/task_state.dart';
final firebaseProvider =
Provider<FirebaseFirestore>((ref) => FirebaseFirestore.instance);
// タスク用のwithConverterを作成
final taskConverterProvider = Provider((ref) {
final firebase = ref.watch(firebaseProvider);
return firebase.collection('tasks').withConverter<TaskState>(
fromFirestore: (snapshot, _) {
final data = snapshot.data()!;
data['id'] = snapshot.id;
return TaskState.fromJson(data).copyWith(id: snapshot.id);
},
toFirestore: (value, _) => value.toJson(),
);
});
final taskContentConverterProvider = Provider.family((ref, String taskId) {
final firebase = ref.watch(firebaseProvider);
return firebase.collection('tasks').doc(taskId).collection('content').withConverter<TaskStateContent>(
fromFirestore: (snapshot, _) {
final data = snapshot.data()!;
data['id'] = snapshot.id;
return TaskStateContent.fromJson(data);
},
toFirestore: (value, _) => value.toJson(),
);
});
// タスク用のStreamProviderを作成
final taskStreamProvider = StreamProvider.autoDispose<List<TaskState>>((ref) {
final todoConverter = ref.watch(taskConverterProvider);
return todoConverter
.snapshots()
.map((e) => e.docs.map((e) => e.data()).toList());
});
// サブコレクションのcontent用のwithConverterを作成
final contentConverterProvider = Provider.family((ref, String taskId) {
final firebase = ref.watch(firebaseProvider);
return firebase.collection('tasks').doc(taskId).collection('contents').withConverter<TaskStateContent>(
fromFirestore: (snapshot, _) => TaskStateContent.fromJson(snapshot.data()!),
toFirestore: (value, _) => value.toJson(),
);
});
// サブコレクションのcontent用のStreamProviderを作成
final contentStreamProvider = StreamProvider.autoDispose.family<List<TaskStateContent>, String>((ref, String taskId) {
final contentConverter = ref.watch(contentConverterProvider(taskId));
return contentConverter
.snapshots()
.map((e) => e.docs.map((e) => e.data()).toList());
});
📁フォルダが2個ある:
task1の場合はサブコレクションにデータがあるので、ファイルが2個表示されるイメージ
task2は、サブコレクションにデータがないので、何も表示されない。フォルダごとにデータを分けることができている。
UIのコード
View側のコードを書いてなかった💦
フォルダのページ:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/provider/firebase_provider.dart';
import 'package:todo_fire/view/conten_view.dart';
class TaskView extends ConsumerWidget {
const TaskView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final taskState = ref.watch(taskStreamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Task'),
),
body: taskState.when(
data: (tasks) {
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
// 画面左にフォルダのアイコンを表示
leading: const Icon(Icons.folder),
title: Text(task.title),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ContentView(task: task),
),
);
},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, s) => Center(child: Text(e.toString())),
),
);
}
}
ファイルのページ:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/model/task_state.dart';
import 'package:todo_fire/provider/firebase_provider.dart';
class ContentView extends ConsumerWidget {
const ContentView({super.key, required this.task});
final TaskState task;
Widget build(BuildContext context, WidgetRef ref) {
final content = ref.watch(contentStreamProvider(task.id));
return Scaffold(
appBar: AppBar(
title: Text(task.title),
),
body: content.when(
data: (contents) {
return ListView.builder(
itemCount: contents.length,
itemBuilder: (context, index) {
final content = contents[index];
return ListTile(
// 左にファイルのiconを表示
leading: const Icon(Icons.file_copy),
title: Text(content.content),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, s) => Center(child: Text(e.toString())),
),
);
}
}
フォルダを作るコード
メソッドじゃないから、かっこよくないがメモ用ならこれでいいか。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/model/task_state.dart';
import 'package:todo_fire/provider/firebase_provider.dart';
class CreateTaskView extends ConsumerWidget {
const CreateTaskView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final titleController = TextEditingController();
return Scaffold(
appBar: AppBar(
title: const Text('Create Task'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
hintText: 'Input title',
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () async {
// doumentIDを取得する
final id = ref.read(taskConverterProvider).doc().id;
final task = TaskState(
id: id,
title: titleController.text,
);
// taskを作成する
await ref.read(taskConverterProvider).add(task);
if (context.mounted) {
Navigator.of(context).pop();
}
},
child: const Text('task作成')),
],
),
),
);
}
}
フォルダにファイルを作る
指定したフォルダの中にファイルを作るコード。ここは詳細ページですね。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/model/task_state.dart';
import 'package:todo_fire/provider/firebase_provider.dart';
class ContentView extends ConsumerWidget {
const ContentView({super.key, required this.task});
final TaskState task;
Widget build(BuildContext context, WidgetRef ref) {
final content = ref.watch(contentStreamProvider(task.id));
final contentController = TextEditingController();
return Scaffold(
appBar: AppBar(
title: Text(task.title),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: contentController,
decoration: const InputDecoration(
labelText: 'Content',
hintText: 'Input content',
),
),
),
Expanded(
child: content.when(
data: (contents) {
return ListView.builder(
itemCount: contents.length,
itemBuilder: (context, index) {
final content = contents[index];
return ListTile(
// 左にファイルのiconを表示
leading: const Icon(Icons.file_copy),
title: Text(content.content),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, s) => Center(child: Text(e.toString())),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// doumentIDを取得する
final id = ref.read(contentConverterProvider(task.id)).doc().id;
final content = TaskStateContent(
id: id,
content: contentController.text,
createdAt: Timestamp.now().toDate(),
);
// contentを作成する
ref.read(contentConverterProvider(task.id)).add(content);
},
child: const Icon(Icons.add),
),
);
}
}