GoRouterのパスパラメーターと詳細ページへ値を渡す方法
パラメーターってそもそもなんなのか?
パスパラメータとクエリパラメータは、URLを通じてサーバーに情報を送信するための2つの方法です。
パスパラメータ: URLの一部として送信されます。例えば、https://example.com/users/123
の123はパスパラメータです。この場合、123は特定のユーザーのIDを表しています。パスパラメータは、リソースの特定のインスタンスやアイテムを識別するのによく使われます。
クエリパラメータ: URLの末尾に?記号の後に追加され、key=valueの形式で表されます。例えば、https://example.com/users?sort=descのsort=desc
はクエリパラメータです。この場合、sort=descはユーザーを降順でソートすることを指示しています。クエリパラメータは、リソースのフィルタリングやソート、ページネーションなどによく使われます。
これらのパラメータは、HTTPリクエストの一部としてサーバーに送信され、サーバーはこれらのパラメータを使用してリクエストを処理します。
🧑🎓こちらに実験用のサンプルがございます
パスの定数
Dart3のfinal
keywordをつけると、継承できないクラスになる。これでどこかで継承されて、中のコードに変更が加えられることはなくなる。
final class RouterPath {
static const HOME = '/';
static const DETAIL = 'detail';
static const PATH_PARAM = 'path_param';
}
ルートの定義
RouterPathクラスのインスタンス化しないで使える変数を使いルートにパスを指定する。パスパラメーターのところは書き方がちょっと違います。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_builder_ui/common/router_path.dart';
import 'package:page_builder_ui/view/category.dart';
import 'package:page_builder_ui/view/detail_page.dart';
import 'package:page_builder_ui/view/parameter_test.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../domain/entity/category.dart';
part 'router.g.dart';
GoRouter router(RouterRef ref) {
return GoRouter(
routes: [
GoRoute(
path: RouterPath.HOME,
name: RouterPath.HOME,
builder: (context, state) => const CategoryPage(),
routes: [
// 詳細ページへのルーティング
GoRoute(
path: RouterPath.DETAIL,
name: RouterPath.DETAIL,
builder: (context, state) {
final category = state.extra as CategoryModel;
return DetailPage(category);
},
),
// パスパラメーターのテスト
GoRoute(
path: '${RouterPath.PATH_PARAM}/:id',
name: RouterPath.PATH_PARAM,
builder: (context, state) {
final id = state.pathParameters['id']!;
return ParameterTest(id: id);
},
),
]
),
],
// 404ページを指定
errorPageBuilder: (context, state) {
return const MaterialPage(
child: Scaffold(
body: Center(
child: Text('Page not found'),
),
));
});
}
モデルの値を渡す例
domainとなるentity(モデルって言った方がわかりやすいですね)
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'category.freezed.dart';
part 'category.g.dart';
class CategoryModel with _$CategoryModel {
const factory CategoryModel({
(0) int id,
('') String name,
}) = _CategoryModel;
factory CategoryModel.fromJson(Map<String, dynamic> json) =>
_$CategoryModelFromJson(json);
}
entityから値を受け取るページ
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:page_builder_ui/domain/entity/category.dart';
class DetailPage extends ConsumerWidget {
const DetailPage(this.category, {super.key});
final CategoryModel category;
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text(category.id.toString()),
),
body: Center(
child: Text(category.name),
),
);
}
}
entityの値を渡すページ
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:page_builder_ui/common/router_path.dart';
import 'package:page_builder_ui/provider/category_provider.dart';
class CategoryPage extends ConsumerWidget {
const CategoryPage({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final categories = ref.watch(categoryListStreamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('categories'),
),
body: categories.when(
data: (data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
context.goNamed(RouterPath.DETAIL, extra: data[index]);
},
title: Text(data[index].name),
);
},
);
},
error: (e, s) => Center(child: Text(e.toString())),
loading: () => const Center(child: CircularProgressIndicator())),
);
}
}
こんな感じです
パスでidを渡す場合
idを受け取れるように引数が必須のコンストラクターを定義しておく。後は、前のページからパスを渡すだけ。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ParameterTest extends ConsumerWidget {
const ParameterTest({Key? key, required this.id}) : super(key: key);
final String id;
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('パスパラメーターのテスト'),
),
body: Center(
child: Text('idは: $id'),
),
);
}
}
idを渡せるように、前のページを修正
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:page_builder_ui/common/router_path.dart';
import 'package:page_builder_ui/provider/category_provider.dart';
class CategoryPage extends ConsumerWidget {
const CategoryPage({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final categories = ref.watch(categoryListStreamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('categories'),
),
body: categories.when(
data: (data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
context.goNamed(RouterPath.PATH_PARAM,
pathParameters: {'id': data[index].id.toString()});
},
title: Text(data[index].name),
);
},
);
},
error: (e, s) => Center(child: Text(e.toString())),
loading: () => const Center(child: CircularProgressIndicator())),
);
}
}
こんな感じです
プロバイダー
Supabase使ってるので、プロバイダーで呼び出してます。詳しくはGithubのリポジトリを見て色々試して欲しいです。
import 'package:page_builder_ui/domain/entity/category.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'category_provider.g.dart';
/// TODO: Providerが使えない?
final supabaseInstance = Supabase.instance.client;
/// [全てのデータを取得する]
// final categoryStreamProvider =
// StreamProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
// final supabase = Supabase.instance.client;
// return supabase.from('categories').stream(primaryKey: ['id']);
// });
/// [モデルクラスを使用してデータを取得する]
// final categoryListStreamProvider =
// StreamProvider.autoDispose<List<Category>>((ref) {
// final supabase = Supabase.instance.client;
// return supabase.from('categories').stream(primaryKey: ['id']).map((event) => event.map((e) => Category.fromJson(e)).toList());
// });
// 自動生成するコマンド
// flutter pub run build_runner watch --delete-conflicting-outputs
/// [ジェネレーターを使用してデータを取得する]
Stream<List<Map<String, dynamic>>> categoryStream(CategoryStreamRef ref) {
return supabaseInstance.from('categories').stream(primaryKey: ['id']);
}
/// [モデルクラスを使用して、ジェネレーターでデータを取得する]
Stream<List<CategoryModel>> categoryListStream(CategoryListStreamRef ref) {
return supabaseInstance.from('categories').stream(primaryKey: ['id']).map(
(event) => event.map((e) => CategoryModel.fromJson(e)).toList());
}
最後に
今回は、普段使わないGoRouterのパスパラメーターなるものを使ってみました。過去に一応記事は書いていたみたいなんですけどね。
一応Supabaseに貢献してる人なんで、Zennで本を出してます。
Discussion