Supabaseで複数のテーブルから必要な情報を取得し、画面に描画する
概要
Supabaseで複数のテーブルからデータを取得するときに昔の自分は下記のようにやっていました。
ただこれじゃ2度Supabaseからデータを取得することになるので非効率です。
それを効率良くしていきたいと思います。
SupabaseのSchema Visualizer
まずSupabaseのSchema Visualizerはこちらになります。
その中でtodo_title,body,category_idを取得して、画面に描画させて行こうと思います。
Flutter側での使用パッケージについて
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_riverpod: 2.4.10
riverpod_annotation: 2.3.4
supabase_flutter: 2.3.4
freezed: 2.4.7
freezed_annotation: 2.4.1
json_annotation: 4.8.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: 2.4.8
riverpod_generator: 2.3.9
json_serializable: 6.7.1
使用するのは
RiverPod,Supabase,freezed(json系も含めて)になります。
Supabase セットアップについて
上記を参照してください
ソースの中身について
今回は AsyncNotifier を使用して、Supabaseからデータを取得し、表示を想定として作成します。
View層
画面側は至ってシンプルで、表示されるときに build()メソッドでtodoの情報を取得しています。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample_project/domain/todo_notifier.dart';
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: ref.watch(todoNotifierProvider.select((value) => value)).when(
data: (data) {
return Padding(
padding: const EdgeInsets.all(16),
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(data.todoModel[index].todoTitle),
Text(data.todoModel[index].categoryModel.categoryTitle),
],
);
},
itemCount: data.todoModel.length,
),
);
},
error: (_, __) {},
loading: () => const Center(
child: CircularProgressIndicator(),
),
),
);
}
}
state層
todo_state
List型のTodoModelを格納します。
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sample_project/infrastructure/todo/model/todo_model.dart';
part 'todo_state.freezed.dart';
part 'todo_state.g.dart';
@freezed
class TodoState with _$TodoState {
const factory TodoState({
required List<TodoModel> todoModel,
}) = _TodoState;
factory TodoState.fromJson(Map<String, dynamic> json) => _$TodoStateFromJson(json);
}
domain層
Notifier,Serviceともに加工などは一切行っていないので、Notifierだけ抜粋します。
単純にService層にtodoModelを取得しに行っています。
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sample_project/domain/todo_service.dart';
import 'package:sample_project/domain/todo_state.dart';
part 'todo_notifier.g.dart';
@riverpod
class TodoNotifier extends _$TodoNotifier {
@override
FutureOr<TodoState> build() async {
return TodoState(
todoModel: await ref.read(todoServiceProvider).fetchTodoList(),
);
}
}
infra層
Supabaseからデータの取得を行います。
今回はこちらを使って作成しました。
それ以外の内容については公式Docを参照してください。
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sample_project/infrastructure/todo/model/todo_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final todoRepositoryProvider = Provider.autoDispose((ref) => TodoRepository());
class TodoRepository {
final SupabaseClient supabase = Supabase.instance.client;
/// Todoの取得
Future<List<TodoModel>> fetchTodoList() async {
final response = await supabase.from('todo').select('todo_title,body,category:category_id(title)');
List<TodoModel> todoList = response.map((e) => TodoModel.fromJson(e)).toList();
return todoList;
}
}
final response = await supabase.from('todo').select('todo_title,body,category!inner(title)');
ちなみにこれでもできます。
各動きに関してはアコーディオンを見ていただければと思います。
Supabase複数テーブルを参照するときどんなDataとして返ってくるのか
今回帰ってきたレスポンスとして
final response = await supabase.from('todo').select('todo_title,body,category:category_id(title)');
上記の返り値が下記になります。
[
{
todo_title: アプリ開発をする。,
body: null,
category: {
title: 必ずやる
}
}
]
categoryだけがネストで格納されている為、fromJSONがそのままだとエラーで弾かれてしまう形になります。
Freezedをうまく使うことによってネストされているやつもそのままfromJSONで取得できないかなと考えました。
単純に入れ込む場合
@freezed
class CategoryModel with _$CategoryModel {
const factory CategoryModel({
required String categoryTitle,
}) = _CategoryModel;
factory CategoryModel.fromJson(Map<String, dynamic> json) => _$CategoryModelFromJson(json);
factory CategoryModel.insert(String title) {
return CategoryModel(categoryTitle: title);
}
}
CategoryModel.insertを呼び出して、格納していくまた、都度都度、別のModel作成時にも作っていって格納していく為、非効率だと考えました。
ネストされているデータもJSONとして取得できるといいのになと考え・・・。
色々探した結果
@JsonSerializableのオプションコマンドexplicitToJsonです。
@JsonSerializable(explicitToJson: true)を設定することによって
TodoModel
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sample_project/infrastructure/todo/model/category_model.dart';
part 'todo_model.freezed.dart';
part 'todo_model.g.dart';
@freezed
class TodoModel with _$TodoModel {
@JsonSerializable(explicitToJson: true)
const factory TodoModel({
// Todoテーブルの todo_title
@JsonKey(name: 'todo_title') required String todoTitle,
// Todoテーブルの body
@JsonKey(name: 'body') required String? body,
// categoryテーブル
@JsonKey(name: 'category') required CategoryModel categoryModel,
}) = _TodoModel;
factory TodoModel.fromJson(Map<String, dynamic> json) => _$TodoModelFromJson(json);
}
CategoryModel
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'category_model.freezed.dart';
part 'category_model.g.dart';
@freezed
class CategoryModel with _$CategoryModel {
const factory CategoryModel({
required String categoryTitle,
}) = _CategoryModel;
factory CategoryModel.fromJson(Map<String, dynamic> json) => _$CategoryModelFromJson(json);
factory CategoryModel.insert(String title) {
return CategoryModel(categoryTitle: title);
}
}
返り値
[
{
todo_title: アプリ開発をする。,
body: null,
category: {
title: 必ずやる
}
}
]
上記のレスポンスを fromJSONとしてModelに格納することができます。
これで外部テーブルがあってもfromJSONに格納できるようになるので、都度都度違うテーブルを叩いて、DBを呼ぶ回数が減りました。
最後に
今回記事を書くのに使用した内容はGitHubに置きましたので、参照してください!
あと是非X(Twitter)をフォローして頂けると嬉しいです。
Discussion