Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterオープンソースプロジェクト・Patapataの紹介

gree_tech
October 12, 2023

 Flutterオープンソースプロジェクト・Patapataの紹介

GREE Tech Conference 2023で発表された資料です。
https://techcon.gree.jp/2023/session/TrackA-7

https://github.com/gree/patapata

gree_tech

October 12, 2023
Tweet

More Decks by gree_tech

Other Decks in Technology

Transcript

  1. 概要 • Patapata ◦ Flutterのアプリ開発を楽にするためのフレームワーク ◦ 毎回アプリを開発する時に必要な汎用的処理をほぼ自動的 に作成してくれるフレームワーク (コードの自動生成とは別) ◦

    アナリティクス、ログ、多言語対応などの機能を簡単に構築 する ◦ オープンソースとして公開予定 • Flutter ◦ Googleが開発したマルチプラットフォームのアプリを開発で きるオープンソースフレームワーク ◦ UI機能が豊富、UI実装の負担を軽減 3 Flutter + Patapata = 商用レベルのアプリ開発の負担を減らす
  2. 事例2 • dTV ◦ ドラマ・アニメ・映画などを視聴できる動画配 信サービス ◦ Android / iOS

    対応 ◦ 初期のバージョンからの Flutterを使用して開 発したスマホアプリ 5
  3. Patapataの開発背景 • Flutterのみでアプリ開発 ◦ UIや画面の実装の負担は減った ◦ 画面遷移や初期化、ディープリンクなどの機能はアプリごとに実装が必要になった ◦ あるいは、pub.devに公開されてる外部パッケージで補うなどが必要 •

    課題 ◦ プラグインの追加や設定情報を管理しづらい ◦ 画面遷移時に受け取るデータの型がわかりにくい ◦ ログやアナリティクス、ディープリンクといった商用アプリでは(ほとんどの場合は)必須 になる機能をアプリごとに用意するのは手間がかかる ◦ 初期化処理のような毎回同じになる処理は転用したい など 6 Patapataを開発 Flutterコミュニティへの貢献のためOSS化
  4. Patapataの機能 • Pluginアーキテクチャ • 環境の概念 • StandardApp(アプリの画面管理シ ステム) • 起動シーケンス

    • ローカライゼーション(多言語対応) • ScreenLayout(画面サイズによって UIのサイズを調整) 7 • リモートメッセージシステム • ローカルコンフィグ • リモートコンフィグ • ログ・エラー • アナリティクス • Deep Link • リポジトリシステム など 今後も機能を追加予定
  5. 画面遷移とページ(1) • Flutterのみで実装する場合 ◦ Flutterの画面遷移(NavigatorやRouter)は複雑で学習コストが高い ◦ go_routerを使う場合が多い import 'package:go_router/go_router.dart'; …

    GoRouter.of(context).go('/pageA'); GoRouter.of(context).go('/pageB', extra: {'data': 'hello'}); … データの受け渡しがない場合 データの受け渡しがある場合
  6. 画面遷移とページ(2) • 問題点 ◦ データの受け渡しをするときにデータ型が Object?(Null許容)型 ▪ →様々な型のデータを受け渡しできるため、 データ不整合が起きうる ◦

    受け取ったデータを遷移先の画面で参照できるようにするために、一度 builderを経由す る必要がある GoRoute( path: 'pageB', builder: (BuildContext context, GoRouterState state) { final map = state.extra! as Map<String, dynamic>; final data = map['data'] as String?; return PageB(message: data); }, ),
  7. 画面遷移とページ(3) • Patapataで実装する場合 ◦ context.goというシンプルな方法で画面遷移 ◦ ページにデータを渡す場合は、ページデータのクラスを用意し、受け渡し時の データの 型の不整合を防ぐ import

    'package:patapata_core/patapata_core.dart'; … context.go<PageA, void>(null); context.go<PageB, PageData>(PageData(hello: 'Patapata')); … データの受け渡しがない場合 データの受け渡しがある場合
  8. 画面遷移とページ(4) 12 class PageB extends StandardPage<PageData> { @override Widget buildPage(BuildContext

    context) { return Scaffold( appBar: AppBar( title: Text(l(context, 'title')), ), body: Center( child: Text(pageData.hello), ), ); } } Pageには任意のデータクラスを渡すことが可能 class PageData { PageData({ this.hello = 'hi!', }); final String hello; } データへのアクセスは pageData.helloで直にアクセスが可能
  9. 環境設定とプラグイン(1) • Flutterのみで実装する場合 ◦ 多言語対応 ▪ MaterialAppのプロパティ • localizationsDelegates •

    supportedLocale ◦ Firebase CrashlyticsなどのFirebase関連の機能 ▪ Firebase.initializeAppを実行 ▪ 必要なオプションなどを引数に渡す • 問題点 ◦ 複数の箇所に環境設定を書く必要があり管理しにくい ◦ 初期化するタイミングがプラグインごとに異なる
  10. 環境設定とプラグイン(2) MaterialApp.router( routerConfig: _router, // routerの中身は省略 localizationsDelegates: const [ AppLocalizations.delegate,

    GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en'), Locale('ja'), ], ); 多言語対応の場合 MaterialAppのlocalizationsDelegatesと supportedLocalesに言語の設定をする
  11. 環境設定とプラグイン(3) void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform,

    ); runApp(const MyApp()); } Firebaseの場合 設定値はoptionsで設定 初期化はmainなどrunAppが始まる前に実行する
  12. 環境設定とプラグイン(4) • Patapataで実装する場合 ◦ アプリに必要な多言語対応などの設定は Environmentで一元管理 ◦ プラグインの追加はpluginsのプロパティで設定 ◦ プラグインの初期化はPatapataが自動でやってくれる

    import 'package:patapata_core/patapata_core.dart'; … void main() { App( environment: const Environment(), … ).run(); } environment : アプリケーションの環境をカスタ マイズするために渡す引数 ※Patapataのアプリで実装する場合は App(...).run()で開始
  13. 環境設定とプラグイン(5) 17 class Environment with I18nEnvironment, FirebaseCorePluginEnvironment… { @override final

    List<String>? l10nPaths = const ['l10n']; @override final List<Locale>? supportedL10ns = const [Locale('ja'), Locale('en'),]; @override final Map<TargetPlatform, FirebaseOptions>? firebaseOptions = const { TargetPlatform.android: DefaultFirebaseOptions.android, TargetPlatform.iOS: DefaultFirebaseOptions.ios, }; … const Environment(); } Patapataで使う機能の環境設定 ローカライズ、Firebaseなどの初期 設定を、このEnviromentというクラス で設定し管理 Enviromentのプロパティで多言語対 応、Firebaseのオプション設定がまと めて記載できる
  14. 環境設定とプラグイン(6) 18 App( … plugins: [ SentryPlugin(), FirebaseCorePlugin(), FirebaseRemoteConfigPlugin(), FirebaseAnalyticsPlugin(),

    FirebaseCrashlyticsPlugin(), FirebaseMessagingPlugin(), FirebaseDynamicLinksPlugin(), ] ).run(); アプリに様々な機能を追加したい場合 例えば • Sentry(エラー監視) • Firebase関連 • アドフリくん(広告収益)など Appクラスのpluginの引数に追加していく Patapataの提供してるプラグインなら、初期化 を呼び出す必要はなく、Patapataが裏で初期 化処理を実行 リモートコンフィグを使っていれば、 プラグインを 個別に有効・無効にできる
  15. Deep link(1) • Patapataで実装する利点の1つ ◦ アプリの特定のページに遷移させたり、ストアに飛ばすなどの機能 ◦ Patapataでは画面ごとにDeep linkの設定が可能 ▪

    受け取り • 正規表現で記述することが可能 • 取得後は自動で画面に遷移する ▪ 作成 • 画面ごとのDeep linkの作成が可能
  16. Deep link(2) 20 StandardPageFactory<PageC, DeepLinkData>( create: (_) => PageC(), links:

    { r'pageC/(\d+)' : (match, uri) => DeepLinkData( id: int.parse(uri.queryParameters([‘id’])!), message: 'this is message', ), }, linkGenerator: (pageC) => 'pageC/${pageC.id}', ), … StandardPageFactoryはPatapataの画面 (StandardPage)を作るために必要なクラス
  17. Deep link(3) 21 StandardPageFactory<PageC, DeepLinkData>( create: (_) => PageC(), links:

    { r'pageC/(\d+)' : (match, uri) => DeepLinkData( id: int.parse(uri.queryParameters([‘id’])!), message: 'this is message', ), }, linkGenerator: (pageC) => 'pageC/${pageC.id}', ), … class DeepLinkData { DeepLinkData({ required this.id, required this.message, }); final int id; final String? message; } linksはアプリからdeep linkを受け取る設定 linkGeneratorはアプリからdeep linkを作成 画面ごとに設定可能
  18. 具体的な機能 • Pluginアーキテクチャ • 環境の概念 • StandardApp(アプリの画面管理シ ステム) • 起動シーケンス

    • ローカライゼーション(多言語対応) • ScreenLayout(画面サイズによって UIのサイズを調整) 24 • リモートメッセージシステム • ローカルコンフィグ • リモートコンフィグ • ログ・エラー • アナリティクス • Deep Link • リポジトリシステム など
  19. 起動シーケンス 27 • Flutterのみで実装する場合 ◦ スプラッシュ画面に直接処理を書く ◦ アプリ起動時に行う処理を制御するためのクラスを実装する • 問題点

    ◦ コードが複雑になりバグが出やすい ◦ そのためだけのクラスを1から実装する必要がある → ルールに沿ったシーケンシャルな実装で、起動時の処理を安全に定義
  20. 起動シーケンス 28 • 3つのクラス ◦ StartupSequence ▪ 起動シーケンス全体を管理する ◦ StartupState

    ▪ 各ステップごとの処理を定義する ◦ StartupStateFactory<T extends StartupState> ▪ ステップ同士の繋がりを定義する
  21. 起動シーケンス 29 規約同意 済み? class Agreements extends StartupState { Agreements(

    StartupSequence startupSequence ) : super(startupSequence); @override Future<void> process(Object? data) async { if (!agree()) { await navigateToPage( AgreementsPage,(result) {}, ); } // 次のステップへ } } Yes 規約同意画面 No
  22. 起動シーケンス 30 class Agreements extends StartupState { Agreements( StartupSequence startupSequence

    ) : super(startupSequence); } @override Future<void> process(Object? data) async { if (!agree()) { await navigateToPage( AgreementsPage,(result) {}, ); } // 次のステップへ } 規約同意 済み? Yes 規約同意画面 No
  23. 起動シーケンス 31 StartupSequence( // 起動シーケンスを定義するためのクラス startupStateFactories: [ StartupStateFactory<Agreements>( // 規約同意確認

    (seq) => Agreements(seq), [ LogicStateTransition<Notification>() ], ), StartupStateFactory<Notification>( // 通知設定確認 (seq) => Notification(seq), [], ), ], ) 規約同意済み? (Agreements) 通知設定済み? (Notification)
  24. 起動シーケンス 32 StartupSequence( startupStateFactories: [ ], ) StartupStateFactory<Agreements>( (seq) =>

    Agreements(seq), [ LogicStateTransition<Notification>() ], ), StartupStateFactory<Notification>( (seq) => Notification(seq), [], ), 規約同意済み? (Agreements) 通知設定済み? (Notification)
  25. ローカライゼーション • Flutterのみで実装する場合 (標準機能: flutter_localizations) ◦ arbファイルに翻訳情報を記述 ◦ flutter gen-l10nコマンドを実行

    • 問題点 ◦ キーをネームスペースで区切ることができず、 UIとテキストを紐付けにくい ◦ 追加・変更するたびにコマンドを実行し コードを再生成する必要がある → Yamlファイルを直接読み込むことで、ストレスフリーな多言語対応 • ネームスペースでUIとテキストを紐付けて管理する • ファイルを直接読み込むためコードを再生成する必要がない 35
  26. ローカライゼーション 36 home: title: Flutter Demo Home Page message: "You

    have pushed the button this many times:" count: "{count, plural, 1{# time} other{# times}}" home: title: Flutterデモホームページ message: "あなたは何度もボタンを押しました :" count: "{count} 回" 日本語 英語
  27. Text( l( context, 'home.title' ) ) ローカライゼーション 37 message: "あなたは何度もボタンを押しました

    :" count: "{count} 回" home: title: Flutterデモホームページ → Yamlのキーをカンマ区切りで渡す
  28. Text( l( context, 'home.title' ) ) ローカライゼーション 38 message: "You

    have pushed the button... count: "{count, plural, 1{# time}... home: title: Flutter Demo Home Page → 英語の出しわけ
  29. ローカライゼーション 39 title: Flutterデモホームページ Text(l(context, 'home.message')) → Yamlのキーをカンマ区切りで渡す home: message:

    "あなたは何度もボタンを押しました :" count: "{count} 回" Text(l( context, 'home.count', {'count': '$_counter'}, )), → パラメータ指定可能
  30. Scaffold( appBar: AppBar( title: Text(l(context, 'home.title')), ), body: Column( children:

    <Widget>[ Text(l(context, 'home.message')), Text( l( context, 'home.count', {'count': '$_counter'}, ), ), ], ), ); // 一部コードを省略しています ローカライゼーション 40
  31. リモートコンフィグ • 実例 ◦ リリース後に機能を有効にする ◦ 季節ごとのイベントの情報を変更する ◦ 障害対応のためにAPIやCDNのエンドポイントを変更する ◦

    一時的にAPMで検知するエラーレベルの下限 / 上限を変更したい → 設定情報をリモートで管理することで、 リリースなしで変更することを可能に   (実際の例はログ・エラーのパートで解説 ) 42
  32. Get • String, int, double, bool型をサポート リモートコンフィグ 44 // get

    final int logLevel = app.remoteConfig.getInt( 'patapata_log_level', ); debugPrint(logLevel.toString()); // '500' が出力される
  33. フィルタリング②: ログの情報 • アプリ起動時に行う処理を定義 メリット • 煩雑になりがちな実装を簡単に行うことができる → patapata_log_level でレベルによるフィルタリング

    ログ・エラー 48 // フィルタを追加 app.log.addFilter( (record) => record.message.startsWith('ignore') ? null : record, ); // 出力される _logger.severe('logged'); // 出力されない _logger.severe('ignore: no log');
  34. • エラー検知サービスの導入をFlutterのみで行う場合 ログ・エラー 49 利用するサービスを選定する Firebase Crashlytics? Sentry? etc… アプリで使用可能にする

    ライブラリを使う?自前で実装する? エラーを送信する 各サービス固有の処理を呼び出す ステップ1 ステップ2 ステップ3 → サービスを1つ導入するにもやることが多い
  35. • エラー検知サービスの導入をPatapataで行う場合 ※PatapataでPlugin化されているサービスに限ります ログ・エラー 50 利用するサービスを選定する Firebase Crashlytics? Sentry? etc…

    アプリで使用可能にする ライブラリを使う?自前で実装する? エラーを送信する 共通化されている処理を呼び出す ステップ1 ステップ2 ステップ2 設定後すぐ利用可能! → サービスを導入するための作業が簡単!!
  36. ログ・エラー 51 final _logger = Logger('patapata.example'); try { // 処理を記述

    } catch (e, stackTrace) { _logger.severe( e.toString(), e, stackTrace ); } ログと連携した エラー送信
  37. // フィルタを追加 app.log.addFilter( (record) => record.message.startsWith('ignore') ? null : record,

    ); // 出力される _logger.severe('logged'); // 出力されない _logger.severe('ignore: no log'); ログ・エラー 52 Firebase Crashlytics Sentry ※開発環境で発生したエラー 各サービスに 送信されたエラー
  38. 例) Impression → 連携しているサービスにアナリティクスイベントが送信される アナリティクス 56 AnalyticsImpressionWidget( // イベントを付与するためのコード ・・・・

    name: 'featureSliderItemImpression', // イベントの名前 child: tWidget, // イベントを付与させたいUI ・・・・ ) Widgetで囲むことで Impression機能付与
  39. 例) Event • String, int, double, bool型をサポート アナリティクス 57 //

    set app.localConfig.setString( 'chapterTitle', 'オフラインデータの永続化 ', ); // get final String tTitle = app.localConfig.getString( 'chapterTitle', ); debugPrint(tTitle); // 'Key-Valueデータの永続化' が出力される AnalyticsEventWidget ( name: "ExampleEventName.viewPage" , child: ElevatedButton ( onPressed : () async { context.go< Page, PageData>( PageData (), ); }, child: Text( "ページを開く", ), ), ) イベントが 送信される
  40. • アナリティクスイベントを送信する処理をPatapataで実装する場合 ※PatapataでPlugin化されているサービスに限ります アナリティクス 58 利用するサービスを選定する Firebase Analytics? Adjust?Karte? …

    アプリで使用可能にする ライブラリを使う?自前で実装する? アナリティクスイベントを送信する 共通化されている処理を呼び出す ステップ1 ステップ2 ステップ2 設定後すぐ利用可能! → サービスを導入するための作業が簡単!!
  41. 後半パートのまとめ • 起動シーケンス … ルールに沿ったシーケンシャルな実装で、安全に定義 • ローカライゼーション … yaml形式でストレスフリーな多言語対応 •

    リモートコンフィグ … アプリの機能や設定をリリースなしで変更 • ログ・エラー … 2つのシステムが連携して障害検知をサポート • アナリティクス … Widgetで囲むだけでアナリティクスイベントを送信 59
  42. 61