Slide 1

Slide 1 text

Flutterオープンソースプロジェクト Patapataの紹介 グリー株式会社 クライアントエンジニア 山田 幸司 / 薩間 俊彰

Slide 2

Slide 2 text

● 山田 幸司 ○ 2013年4月グリー株式会社に新卒 エンジニアとして入社 ○ Webゲーム開発、インフラエンジニ ア、VRアプリの開発などを行う ○ 現在はAndroid・iOS向けのアプリ 開発エンジニアとして在籍中 2

Slide 3

Slide 3 text

概要 ● Patapata ○ Flutterのアプリ開発を楽にするためのフレームワーク ○ 毎回アプリを開発する時に必要な汎用的処理をほぼ自動的 に作成してくれるフレームワーク (コードの自動生成とは別) ○ アナリティクス、ログ、多言語対応などの機能を簡単に構築 する ○ オープンソースとして公開予定 ● Flutter ○ Googleが開発したマルチプラットフォームのアプリを開発で きるオープンソースフレームワーク ○ UI機能が豊富、UI実装の負担を軽減 3 Flutter + Patapata = 商用レベルのアプリ開発の負担を減らす

Slide 4

Slide 4 text

Patapata導入事例 ● DADAN(ダダン) ○ 漫画が楽しめるアプリ ○ 幅広いジャンルの漫画・コミックを掲載 ○ Android / iOS対応 ○ Google Play / App Storeにて配信中 4

Slide 5

Slide 5 text

事例2 ● dTV ○ ドラマ・アニメ・映画などを視聴できる動画配 信サービス ○ Android / iOS 対応 ○ 初期のバージョンからの Flutterを使用して開 発したスマホアプリ 5

Slide 6

Slide 6 text

Patapataの開発背景 ● Flutterのみでアプリ開発 ○ UIや画面の実装の負担は減った ○ 画面遷移や初期化、ディープリンクなどの機能はアプリごとに実装が必要になった ○ あるいは、pub.devに公開されてる外部パッケージで補うなどが必要 ● 課題 ○ プラグインの追加や設定情報を管理しづらい ○ 画面遷移時に受け取るデータの型がわかりにくい ○ ログやアナリティクス、ディープリンクといった商用アプリでは(ほとんどの場合は)必須 になる機能をアプリごとに用意するのは手間がかかる ○ 初期化処理のような毎回同じになる処理は転用したい など 6 Patapataを開発 Flutterコミュニティへの貢献のためOSS化

Slide 7

Slide 7 text

Patapataの機能 ● Pluginアーキテクチャ ● 環境の概念 ● StandardApp(アプリの画面管理シ ステム) ● 起動シーケンス ● ローカライゼーション(多言語対応) ● ScreenLayout(画面サイズによって UIのサイズを調整) 7 ● リモートメッセージシステム ● ローカルコンフィグ ● リモートコンフィグ ● ログ・エラー ● アナリティクス ● Deep Link ● リポジトリシステム など 今後も機能を追加予定

Slide 8

Slide 8 text

比較 8 ● Patapataを使わない場合・使った場合を比較 ● Patapataは機能が多い ● アプリケーション全体に影響する機能に絞って比較 ○ 画面遷移 ○ ページ ○ 環境設定 ○ プラグイン ○ Deep link

Slide 9

Slide 9 text

画面遷移とページ(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'}); … データの受け渡しがない場合 データの受け渡しがある場合

Slide 10

Slide 10 text

画面遷移とページ(2) ● 問題点 ○ データの受け渡しをするときにデータ型が Object?(Null許容)型 ■ →様々な型のデータを受け渡しできるため、 データ不整合が起きうる ○ 受け取ったデータを遷移先の画面で参照できるようにするために、一度 builderを経由す る必要がある GoRoute( path: 'pageB', builder: (BuildContext context, GoRouterState state) { final map = state.extra! as Map; final data = map['data'] as String?; return PageB(message: data); }, ),

Slide 11

Slide 11 text

画面遷移とページ(3) ● Patapataで実装する場合 ○ context.goというシンプルな方法で画面遷移 ○ ページにデータを渡す場合は、ページデータのクラスを用意し、受け渡し時の データの 型の不整合を防ぐ import 'package:patapata_core/patapata_core.dart'; … context.go(null); context.go(PageData(hello: 'Patapata')); … データの受け渡しがない場合 データの受け渡しがある場合

Slide 12

Slide 12 text

画面遷移とページ(4) 12 class PageB extends StandardPage { @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で直にアクセスが可能

Slide 13

Slide 13 text

環境設定とプラグイン(1) ● Flutterのみで実装する場合 ○ 多言語対応 ■ MaterialAppのプロパティ ● localizationsDelegates ● supportedLocale ○ Firebase CrashlyticsなどのFirebase関連の機能 ■ Firebase.initializeAppを実行 ■ 必要なオプションなどを引数に渡す ● 問題点 ○ 複数の箇所に環境設定を書く必要があり管理しにくい ○ 初期化するタイミングがプラグインごとに異なる

Slide 14

Slide 14 text

環境設定とプラグイン(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に言語の設定をする

Slide 15

Slide 15 text

環境設定とプラグイン(3) void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp(const MyApp()); } Firebaseの場合 設定値はoptionsで設定 初期化はmainなどrunAppが始まる前に実行する

Slide 16

Slide 16 text

環境設定とプラグイン(4) ● Patapataで実装する場合 ○ アプリに必要な多言語対応などの設定は Environmentで一元管理 ○ プラグインの追加はpluginsのプロパティで設定 ○ プラグインの初期化はPatapataが自動でやってくれる import 'package:patapata_core/patapata_core.dart'; … void main() { App( environment: const Environment(), … ).run(); } environment : アプリケーションの環境をカスタ マイズするために渡す引数 ※Patapataのアプリで実装する場合は App(...).run()で開始

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

環境設定とプラグイン(6) 18 App( … plugins: [ SentryPlugin(), FirebaseCorePlugin(), FirebaseRemoteConfigPlugin(), FirebaseAnalyticsPlugin(), FirebaseCrashlyticsPlugin(), FirebaseMessagingPlugin(), FirebaseDynamicLinksPlugin(), ] ).run(); アプリに様々な機能を追加したい場合 例えば ● Sentry(エラー監視) ● Firebase関連 ● アドフリくん(広告収益)など Appクラスのpluginの引数に追加していく Patapataの提供してるプラグインなら、初期化 を呼び出す必要はなく、Patapataが裏で初期 化処理を実行 リモートコンフィグを使っていれば、 プラグインを 個別に有効・無効にできる

Slide 19

Slide 19 text

Deep link(1) ● Patapataで実装する利点の1つ ○ アプリの特定のページに遷移させたり、ストアに飛ばすなどの機能 ○ Patapataでは画面ごとにDeep linkの設定が可能 ■ 受け取り ● 正規表現で記述することが可能 ● 取得後は自動で画面に遷移する ■ 作成 ● 画面ごとのDeep linkの作成が可能

Slide 20

Slide 20 text

Deep link(2) 20 StandardPageFactory( 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)を作るために必要なクラス

Slide 21

Slide 21 text

Deep link(3) 21 StandardPageFactory( 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を作成 画面ごとに設定可能

Slide 22

Slide 22 text

前半パートのまとめ ● Patapata ○ Flutterアプリの商用レベルの開発の負担を減らすフレームワーク ○ 画面遷移はシンプルに、遷移時のデータ受け取り時の不整合が起きに くい ○ プラグインや多言語対応などの情報は複数箇所にかかず、一元管理 できる ○ Deep linkの機能が簡単に設定できる ○ オープンソース化して公開予定 22

Slide 23

Slide 23 text

写真 23 ● 薩間 俊彰(さつま としあき) ○ 2022年11月グリー株式会社に Flutterエンジニアとして中途入社。 ○ 現在は漫画アプリ「DADAN」の開発 などを担当。

Slide 24

Slide 24 text

具体的な機能 ● Pluginアーキテクチャ ● 環境の概念 ● StandardApp(アプリの画面管理シ ステム) ● 起動シーケンス ● ローカライゼーション(多言語対応) ● ScreenLayout(画面サイズによって UIのサイズを調整) 24 ● リモートメッセージシステム ● ローカルコンフィグ ● リモートコンフィグ ● ログ・エラー ● アナリティクス ● Deep Link ● リポジトリシステム など

Slide 25

Slide 25 text

起動シーケンス 25

Slide 26

Slide 26 text

起動時に行われるフローの一例 起動シーケンス 26 規約同意 済み? 規約同意画面 ホーム画面 通知設定 済み? 通知設定画面 アプリ起動 Yes Yes No No

Slide 27

Slide 27 text

起動シーケンス 27 ● Flutterのみで実装する場合 ○ スプラッシュ画面に直接処理を書く ○ アプリ起動時に行う処理を制御するためのクラスを実装する ● 問題点 ○ コードが複雑になりバグが出やすい ○ そのためだけのクラスを1から実装する必要がある → ルールに沿ったシーケンシャルな実装で、起動時の処理を安全に定義

Slide 28

Slide 28 text

起動シーケンス 28 ● 3つのクラス ○ StartupSequence ■ 起動シーケンス全体を管理する ○ StartupState ■ 各ステップごとの処理を定義する ○ StartupStateFactory ■ ステップ同士の繋がりを定義する

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

起動シーケンス 31 StartupSequence( // 起動シーケンスを定義するためのクラス startupStateFactories: [ StartupStateFactory( // 規約同意確認 (seq) => Agreements(seq), [ LogicStateTransition() ], ), StartupStateFactory( // 通知設定確認 (seq) => Notification(seq), [], ), ], ) 規約同意済み? (Agreements) 通知設定済み? (Notification)

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

ローカライゼーション 33

Slide 34

Slide 34 text

できること ● Yamlファイルを用いた多言語対応 ローカライゼーション 34 home: title: Flutterデモホームページ message: "あなたは何度もボタンを押しました :" count: "{count} 回" 日本語

Slide 35

Slide 35 text

ローカライゼーション ● Flutterのみで実装する場合 (標準機能: flutter_localizations) ○ arbファイルに翻訳情報を記述 ○ flutter gen-l10nコマンドを実行 ● 問題点 ○ キーをネームスペースで区切ることができず、 UIとテキストを紐付けにくい ○ 追加・変更するたびにコマンドを実行し コードを再生成する必要がある → Yamlファイルを直接読み込むことで、ストレスフリーな多言語対応 ● ネームスペースでUIとテキストを紐付けて管理する ● ファイルを直接読み込むためコードを再生成する必要がない 35

Slide 36

Slide 36 text

ローカライゼーション 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} 回" 日本語 英語

Slide 37

Slide 37 text

Text( l( context, 'home.title' ) ) ローカライゼーション 37 message: "あなたは何度もボタンを押しました :" count: "{count} 回" home: title: Flutterデモホームページ → Yamlのキーをカンマ区切りで渡す

Slide 38

Slide 38 text

Text( l( context, 'home.title' ) ) ローカライゼーション 38 message: "You have pushed the button... count: "{count, plural, 1{# time}... home: title: Flutter Demo Home Page → 英語の出しわけ

Slide 39

Slide 39 text

ローカライゼーション 39 title: Flutterデモホームページ Text(l(context, 'home.message')) → Yamlのキーをカンマ区切りで渡す home: message: "あなたは何度もボタンを押しました :" count: "{count} 回" Text(l( context, 'home.count', {'count': '$_counter'}, )), → パラメータ指定可能

Slide 40

Slide 40 text

Scaffold( appBar: AppBar( title: Text(l(context, 'home.title')), ), body: Column( children: [ Text(l(context, 'home.message')), Text( l( context, 'home.count', {'count': '$_counter'}, ), ), ], ), ); // 一部コードを省略しています ローカライゼーション 40

Slide 41

Slide 41 text

リモートコンフィグ 41

Slide 42

Slide 42 text

リモートコンフィグ ● 実例 ○ リリース後に機能を有効にする ○ 季節ごとのイベントの情報を変更する ○ 障害対応のためにAPIやCDNのエンドポイントを変更する ○ 一時的にAPMで検知するエラーレベルの下限 / 上限を変更したい → 設定情報をリモートで管理することで、 リリースなしで変更することを可能に   (実際の例はログ・エラーのパートで解説 ) 42

Slide 43

Slide 43 text

リモートコンフィグ ● できること ○ Pluginや各種機能をアプリリリースなしで変更可能 ● 機能 ○ オンラインでKey-Valueデータを取得(Get) ○ String, Int, Double, Boolをサポート ※リモートコンフィグは基本的には Firebase Remote Config 43

Slide 44

Slide 44 text

Get ● String, int, double, bool型をサポート リモートコンフィグ 44 // get final int logLevel = app.remoteConfig.getInt( 'patapata_log_level', ); debugPrint(logLevel.toString()); // '500' が出力される

Slide 45

Slide 45 text

ログ・エラー 45

Slide 46

Slide 46 text

ログ・エラー ● メリット ○ 柔軟なログフィルタリング機能 ○ エラー検知のサービスをアプリに簡単に導入可能 ○ 2つのシステムが連携して、障害検知をサポート ● サポートしているサービス ○ Sentry, Firebase Crashlytics 46

Slide 47

Slide 47 text

フィルタリング①: Remote Configと連携したログレベル ● アプリ起動時に行う処理を定義 メリット ● 煩雑になりがちな実装を簡単に行うことができる → patapata_log_level でレベルによるフィルタリング ログ・エラー 47

Slide 48

Slide 48 text

フィルタリング②: ログの情報 ● アプリ起動時に行う処理を定義 メリット ● 煩雑になりがちな実装を簡単に行うことができる → patapata_log_level でレベルによるフィルタリング ログ・エラー 48 // フィルタを追加 app.log.addFilter( (record) => record.message.startsWith('ignore') ? null : record, ); // 出力される _logger.severe('logged'); // 出力されない _logger.severe('ignore: no log');

Slide 49

Slide 49 text

● エラー検知サービスの導入をFlutterのみで行う場合 ログ・エラー 49 利用するサービスを選定する Firebase Crashlytics? Sentry? etc… アプリで使用可能にする ライブラリを使う?自前で実装する? エラーを送信する 各サービス固有の処理を呼び出す ステップ1 ステップ2 ステップ3 → サービスを1つ導入するにもやることが多い

Slide 50

Slide 50 text

● エラー検知サービスの導入をPatapataで行う場合 ※PatapataでPlugin化されているサービスに限ります ログ・エラー 50 利用するサービスを選定する Firebase Crashlytics? Sentry? etc… アプリで使用可能にする ライブラリを使う?自前で実装する? エラーを送信する 共通化されている処理を呼び出す ステップ1 ステップ2 ステップ2 設定後すぐ利用可能! → サービスを導入するための作業が簡単!!

Slide 51

Slide 51 text

ログ・エラー 51 final _logger = Logger('patapata.example'); try { // 処理を記述 } catch (e, stackTrace) { _logger.severe( e.toString(), e, stackTrace ); } ログと連携した エラー送信

Slide 52

Slide 52 text

// フィルタを追加 app.log.addFilter( (record) => record.message.startsWith('ignore') ? null : record, ); // 出力される _logger.severe('logged'); // 出力されない _logger.severe('ignore: no log'); ログ・エラー 52 Firebase Crashlytics Sentry ※開発環境で発生したエラー 各サービスに 送信されたエラー

Slide 53

Slide 53 text

アナリティクス 53

Slide 54

Slide 54 text

アナリティクス 54 ● 実例 ○ タップイベントを送信する処理をいちいち記述する必要がある ○ 画面にUIが表示されていない場合でも、インプレッションイベントが送信される → Widgetで囲むだけでUIにアナリティクスイベントを付与する ● サポートしているサービス ○ Firebase Analytics, Adjust, Karte

Slide 55

Slide 55 text

4種類のイベントを標準でサポート 好きなイベントを追加することも可能 アナリティクス 55 名称 詳細 Event UIに対する操作 Impression UIの表示 Revenue 課金 RouteView 画面遷移

Slide 56

Slide 56 text

例) Impression → 連携しているサービスにアナリティクスイベントが送信される アナリティクス 56 AnalyticsImpressionWidget( // イベントを付与するためのコード ・・・・ name: 'featureSliderItemImpression', // イベントの名前 child: tWidget, // イベントを付与させたいUI ・・・・ ) Widgetで囲むことで Impression機能付与

Slide 57

Slide 57 text

例) 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( "ページを開く", ), ), ) イベントが 送信される

Slide 58

Slide 58 text

● アナリティクスイベントを送信する処理をPatapataで実装する場合 ※PatapataでPlugin化されているサービスに限ります アナリティクス 58 利用するサービスを選定する Firebase Analytics? Adjust?Karte? … アプリで使用可能にする ライブラリを使う?自前で実装する? アナリティクスイベントを送信する 共通化されている処理を呼び出す ステップ1 ステップ2 ステップ2 設定後すぐ利用可能! → サービスを導入するための作業が簡単!!

Slide 59

Slide 59 text

後半パートのまとめ ● 起動シーケンス … ルールに沿ったシーケンシャルな実装で、安全に定義 ● ローカライゼーション … yaml形式でストレスフリーな多言語対応 ● リモートコンフィグ … アプリの機能や設定をリリースなしで変更 ● ログ・エラー … 2つのシステムが連携して障害検知をサポート ● アナリティクス … Widgetで囲むだけでアナリティクスイベントを送信 59

Slide 60

Slide 60 text

まとめ ● Patapataは、商用レベルのアプリ開発を行うためのフレームワークです。 ● ローカライゼーションやアナリティクスなど、汎用的な機能を実装する際の負担を軽 減することができます。 ● 今後も様々な機能をPatapataに追加し、開発効率を向上させていきたいです。 60

Slide 61

Slide 61 text

61