前書き
モバイルアプリにおいて「特定のページに直接遷移させる」DeepLinkは、使い方によってはユーザー体験を向上させる機能となり得ます。
Flutterにおいて、このDeepLinkを実装するにはgo_routerパッケージを用いるのが一般的ではありますが、既存プロジェクトの構成やプロジェクトの方針(純粋なFlutter APIに依存したいなど)により、標準の Navigator 2.0 (Router API) を使わなければならないケースもあります。
本記事では、両方の実装パターンを比較しながら、どのようにDeepLinkに対応させるかを解説します。
本記事で記載すること(対象となる人)
・これからDeepLinkを実装する上で、画面遷移処理の仕組みを知りたい人
・サードパーティ製のSDK(AdjustやAppsflyer等)を用いてDeepLinkを実現する場合には、ブラックボックス化しがちな部分でもあるので理解を深めたい人
本記事で記載しないこと
・DeepLinkそのものの実装方法
以下のようなページ構成のサンプルを記載します。
・HomeScreen:起点となるページ
・ProductScreen:指定した「id」の商品ページを表示する
1.go_routerを使った実装
go_routerは、Navigator 2.0 の構造をラップし、URLベースのルーティングを直感的に記述できるようにしたパッケージです。DeepLinkのパス解析も自動で行ってくれます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(const GoRouterApp());
// ルーティングの設定
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
// ネストさせたパス: /product/:id
GoRoute(
path: 'product/:id',
builder: (context, state) {
final id = state.pathParameters['id'] ?? '0';
return ProductScreen(id: id);
},
),
],
),
],
);
class GoRouterApp extends StatelessWidget {
const GoRouterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'Go Router Sample',
);
}
}
2.Navigator 2.0 (Router API)を使った実装
外部パッケージを使わず、Flutter標準の機能だけで実装する場合、URLとアプリの状態を同期させるための「中継役」を自作する必要があります。
これは「どのURL(文字列)」が「どの画面の状態」に対応するかを自分で定義する作業です。
実装の概要
RoutePath: アプリ内の「画面の状態」を表すクラス。
RouteInformationParser: URL文字列をRoutePathに変換する。
RouterDelegate: RoutePathに基づいてページスタックを構築する。
import 'package:flutter/material.dart';
void main() {
runApp(const NavigatorApp());
}
class AppRoutePath {
final String? productId;
AppRoutePath.home() : productId = null;
AppRoutePath.product(this.productId);
AppRoutePath.unknown() : productId = null;
bool get isHomePage => productId == null;
bool get isProductPage => productId != null;
}
class AppRouteInformationParser extends RouteInformationParser<AppRoutePath> {
@override
Future<AppRoutePath> parseRouteInformation(RouteInformation routeInformation) async {
final uri = routeInformation.uri;
// パスが '/' の場合
if (uri.pathSegments.isEmpty) {
return AppRoutePath.home();
}
// パスが '/product/:id' の場合
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'product') {
return AppRoutePath.product(uri.pathSegments[1]);
}
// それ以外
return AppRoutePath.unknown();
}
@override
RouteInformation? restoreRouteInformation(AppRoutePath configuration) {
if (configuration.isHomePage) return RouteInformation(uri: Uri.parse('/'));
if (configuration.isProductPage) {
return RouteInformation(uri: Uri.parse('/product/${configuration.productId}'));
}
return null;
}
}
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
String? _selectedProductId;
@override
AppRoutePath get currentConfiguration {
return _selectedProductId == null
? AppRoutePath.home()
: AppRoutePath.product(_selectedProductId);
}
@override
Future<void> setNewRoutePath(AppRoutePath configuration) async {
_selectedProductId = configuration.productId;
}
void _handleProductTapped(String id) {
_selectedProductId = id;
notifyListeners();
}
void _handleHomeTapped() {
_selectedProductId = null;
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: const ValueKey('HomePage'),
child: HomeScreen(onProductTap: _handleProductTapped),
),
if (_selectedProductId != null)
MaterialPage(
key: ValueKey('ProductPage-$_selectedProductId'),
child: ProductScreen(id: _selectedProductId!, onHomeTap: _handleHomeTapped),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
_selectedProductId = null;
notifyListeners();
return true;
},
);
}
}
class NavigatorApp extends StatelessWidget {
const NavigatorApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigator 2.0 DeepLink Sample',
routeInformationParser: AppRouteInformationParser(),
routerDelegate: AppRouterDelegate(),
);
}
}
3. go_router と Navigator 2.0 の差分
ページ遷移処理
go_router は context.go('/product/1') と呼び出すだけで遷移が完結します。Navigator 2.0 は、「今の状態ならこのページのスタックを表示する」というページの状態の構築を自前で行う必要があります。
パス解析の自動化
go_router は :id のような変数抽出を自動で行います。
Navigator 2.0 では Uri.pathSegments を自力で分解し、自分で型変換する必要があります。
また上述したコードのようにそのためのボイラープレートコードを記載する必要があります。
4. サードパーティSDK利用時のDeepLink実装のポイント
AdjustやAppsFlyerなどのSDKを用いてDeepLinkを実現する場合、多くはSDKのリスナーが「URL」をキャッチし、それをアプリ側に通知します。
SDKは、URLを受け取った後に「どの画面を開くか」を開発者に委ねます。
この時に多くのSDKでも go_router を推奨していますが、もう少し高度なルーティングを行いたい場合もあると思います。
この時に、Navigator 2.0 を使用し、構造を理解していれば、内部で独自処理を行うことができます。
また go_router や Navigator 2.0 の画面遷移処理を行う前に、何か処理を行いたい場合にも、SDKが実態としてどのようにアプリに「URL」を通知し、画面遷移を行うか理解することで、独自の実装を迷わずに入れることができるのではないかと思います。
終わりに
手軽に導入するならgo_router一択ですが、依存関係を最小限にしたいプロジェクトではNavigator 2.0 の知識が役に立つと思います。
また、サードパーティ製のSDK(AdjustやAppsflyer等)を用いてDeepLinkを実現する場合にも、ブラックボックス化しがちな動作の理解に役立つかと思います。
参考
Flutter でディープリンクを設定する
go_routerパッケージ
問い合わせ先
案件のご依頼・ご相談は、以下までご連絡ください。
info@lightcafe.co.jp
We are hiring!
ライトカフェはエンジニアを積極採用中です。
常にお客様に寄り添い課題を認識し、解決の手助けをさせていただく。業務SIerにとって当たり前の事ですが、わたしたちはそれを一番大切にしています。
この会社コンセプトに共感して頂ける方、世の中のWEBサービス開発に関わりたい方をお待ちしています。
#イツモトナリニライトカフェ
ライトカフェ採用ページはこちら
ライトカフェクリエイション採用ページはこちら