2020/9/20_iOSDC Japan 2020での、桐山の講演資料になります
Flutter移行の苦労と、 乗り越えた先に得られたもの Recruit Co., Ltd. Keisuke Kiriyama 1
View Slide
• Recruit Co., Ltd. • iOS / Flutter • じゃらんアプリ開発T Keisuke Kiriyama
3 旅を、もっと豊かに 宿・ホテル予約アプリ
現在じゃらんは Flutterへの移行に挑戦しています! 4
Flutterとは ● Google製のクロスプラットフォームSDK ● 単一のソースコードで、複数のプラットフォームの アプリケーションを構築可能 ● 開発言語: Dart 5
Flutterのコミュニティ ● 2018年12月のver1.0リリース以降、 Flutterを使用する開発者は増え続け 現在は200万人を超えた ● 国内においてもFlutterの記事や話題を目にする機会が増え、日に日に盛り上がりを感じている 6 Flutter Spring 2020 Update.:https://medium.com/flutter/flutter-spring-2020-update-f723d898d7af, (参照2020-08-02)
国内におけるFlutterのプロダクション採用 ● しかし、国内においてFlutterを プロダクションに採用している例はそれほど多くない ● 弊社においてもFlutterを採用したのはじゃらんが初 7
Flutterどうなの? 8 実際メリット得られるの?課題はないの?
Flutterを採用して実際どうだったのかをお伝えします 話すこと 9
Flutterを採用して実際どうだったのかをお伝えします 1. どんな技術的課題に直面したのか 話すこと 10
Flutterを採用して実際どうだったのかをお伝えします 1. どんな技術的課題に直面したのか 2. 課題を乗り越えた結果、どんなメリットを得られたのか 話すこと 11
発表のゴール ● Flutter開発経験者 →直面した課題と得られたメリットを知り 技術選定の際の判断材料になる ● Flutter開発未経験者 →まずはFlutter触ってみたいと思ってもらう 12
1. 前提の共有 ○ じゃらんのFlutter移行 ○ Flutterのレイアウト構築 2. 直面した課題 3. 得られたメリット 4. まとめ 説明の流れ 13
じゃらんのFlutter移行 14
Flutter採用の背景 ● じゃらんアプリはiOS/Android共にリリースから 10年を迎え、長年に渡る開発が行われてきた ● 上記課題を解決するために、リプレースを検討 15 プロジェクトの大規模化によるビルド時間の増加プロジェクト全体のコードが古くなっている
じゃらんアプリのリプレース検討 ● クロスプラットフォーム技術の検討 ○ iOS/Android開発工数 ○ リプレースコスト ● クロスプラットフォーム技術の中でも Flutterの開発生産性が最も高いと実感し Flutterの採用を決断 16 半減
17
18
19 ここから先の画面は 全てFlutterで実装
Flutterへの段階的移行 ● Add-to-app(Add Flutter to existing app)を使用 ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的に組み込む仕組み 20 じゃらん アプリ Swift Objective-c じゃらん遊び・体験 Flutterプロジェクト
Flutterへの段階的移行 ● Add-to-app(Add Flutter to existing app)を使用 ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的に組み込む仕組み 21 じゃらん アプリ Swift Objective-c じゃらん遊び・体験 Flutterプロジェクト Flutterモジュール
Flutterのレイアウト構築 22
Flutterのレイアウト構築 ● Widget ○ UIの構成情報を保持するクラス ● Widgetをツリー上に構成することによって UIの構築を行う 23
24 class HomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Flutter Demo Home Page'),),body: Center(child: Text('Hello iOSDC!!',style: TextStyle(fontSize: 30,),...
25 class HomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Flutter Demo Home Page'),),body: Center(child: Text('Hello iOSDC!!',style: TextStyle(fontSize: 30,),...
26 class HomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Flutter Demo Home Page'),),body: Center(child: Text('Hello iOSDC!!',style: TextStyle(fontSize: 30,),...
27 class HomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Flutter Demo Home Page'),),body: Center(child: Text('Hello iOSDC!!',style: TextStyle(fontSize: 30,),...● 』UI部品だけではなく、「画面の中心に表示」の様なUIの構成情報もWidgetで表現する
直面した課題 28
直面した課題 29 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google Mapのクラッシュ
直面した課題 30 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google Mapのクラッシュ
● タブを使用して表示する情報を 切り替えるページが存在する ● このタブのページでは、 口コミの一覧や、プランの一覧を リストで表示する タブの切り替えをする画面 31
発生した問題 ● リストのアイテムを大量に読み込んでタブを切り替える ● タブ切り替えのアニメーションが重くなってしまう ● まれにタブ切り替えのタイミングで アプリがクラッシュすることがある 32
class TabPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return DefaultTabController(...child: Scaffold(appBar: AppBar(title: Text('Tab Sample'),bottom: TabBar(tabs: [const Tab(child: Text('Tab A')),const Tab(child: Text('Tab B'))])),body: TabBarView(children: [TabA(),TabB(),],● タブのページを作成するサンプルコード: タブのページ 33
class TabPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return DefaultTabController(...child: Scaffold(appBar: AppBar(title: Text('Tab Sample'),bottom: TabBar(tabs: [const Tab(child: Text('Tab A')),const Tab(child: Text('Tab B'))])),body: TabBarView(children: [TabA(),TabB(),],● 2つのタブ● TabA()● TabB()サンプルコード: タブのページ 34
class TabA extends StatelessWidget {@overrideWidget build(BuildContext context) {return Center(child: Text('Tab A'),);}}● TabA()は画面の中心に“Tab A”を表示するだけサンプルコード: TabA 35
class TabB extends StatelessWidget {@overrideWidget build(BuildContext context) {return ListView.builder(key: PageStorageKey('TabB'),itemCount: 1000,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('Item $index'),);...● TabB()はリストを保持● 1000個のアイテムを表示サンプルコード: TabB 36
37 タブ切り替え TabA TabB
38 TabB • Tab Bのリストを下までスクロールする
39 TabB TabA TabAに 切り替え TabBに 切り替え TabB
40 TabB TabA TabAに 切り替え TabBに 切り替え TabB ● タブ切り替えのアニメーションが非常に重くなる● まれにクラッシュする
なぜアニメーション重くなる? ● タブを切り替えた際、表示 されないタブはWidget ツリーから除外される 41 TabA表示時 TabB表示時
● 再度タブを表示する際には、表示するタブの レイアウトを再計算する必要がある なぜアニメーション重くなる? ● タブを切り替えた際、表示 されないタブはWidget ツリーから除外される 42 TabA表示時 TabB表示時
タブが保持するリストのアイテムの高さが可変の場合 ● 1つ目のアイテムから順にレイアウト を計算して、高さを決定しないと なぜアニメーション重くなる? 43 …
タブが保持するリストのアイテムの高さが可変の場合 ● 1つ目のアイテムから順にレイアウト を計算して、高さを決定しないと ● 前回のスクロール位置の アイテムを表示できない なぜアニメーション重くなる? 44 … 前回の スクロール位置
タブが保持するリストのアイテムの高さが可変の場合 ● 1つ目のアイテムから順にレイアウト を計算して、高さを決定しないと ● 前回のスクロール位置の アイテムを表示できない ● この演算のために パフォーマンスが低下 なぜアニメーション重くなる? 45 … 前回の スクロール位置
なぜまれにクラッシュする? ● リストのレイアウトを決定する演算を行うことで、 一時的にメモリを圧迫する ● この一時的な圧迫で許容値を超えてしまった場合に クラッシュが発生していた 46 タブ切り替え時
どう回避したか ● タブのWidgetにAutomaticKeepAliveClientMixin を適用する ● 非表示になったタブもWidgetツリーから除外されなくなるため、再度レイアウトの演算が不要 47 TabA表示時にTabBが除外されない
class TabB extends StatefulWidget {...class _TabBState extends State with AutomaticKeepAliveClientMixin {@overrideWidget build(BuildContext context) {super.build(context);return ListView.builder(key: PageStorageKey('TabB'),itemCount: 1000,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('Item $index'),...@overridebool get wantKeepAlive => true;}● TabのWidgetをStatefulWidgetに変更するタブにAutomaticKeepAliveClientMixinを適用 48
class TabB extends StatefulWidget {...class _TabBState extends State with AutomaticKeepAliveClientMixin {@overrideWidget build(BuildContext context) {super.build(context);return ListView.builder(key: PageStorageKey('TabB'),itemCount: 1000,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('Item $index'),...@overridebool get wantKeepAlive => true;}● StateにAutomaticKeepAliveClientMixinを適用するタブにAutomaticKeepAliveClientMixinを適用 49
class TabB extends StatefulWidget {...class _TabBState extends State with AutomaticKeepAliveClientMixin {@overrideWidget build(BuildContext context) {super.build(context);return ListView.builder(key: PageStorageKey('TabB'),itemCount: 1000,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('Item $index'),...@overridebool get wantKeepAlive => true;}タブにAutomaticKeepAliveClientMixinを適用 50 ● super.buildの呼び出し● wantKeepAliveのgetterでtrueを返す
class TabB extends StatefulWidget {...class _TabBState extends State with AutomaticKeepAliveClientMixin {@overrideWidget build(BuildContext context) {super.build(context);return ListView.builder(key: PageStorageKey('TabB'),itemCount: 1000,itemBuilder: (BuildContext context, int index) {return ListTile(title: Text('Item $index'),...@overridebool get wantKeepAlive => true;}タブにAutomaticKeepAliveClientMixinを適用 51 ● 一時的なメモリ圧迫も起こらなくなる
直面した課題 52 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google Mapのクラッシュ
53 Nativeの画面 Flutterの画面
発生した問題 ● Flutterの画面を閉じて再度開く ● 前回開いた画面の状態が残ってしまっている 54
55 Nativeの画面 Flutterの画面
56 Nativeの画面 • Flutterの画面に遷移する
57 Flutterの画面 • +のFABをタップ• 画面の中心にタップした回数が表示
58 Nativeの画面 Flutterの画面 dismiss
59 Nativeの画面 Flutterの画面 present dismiss Flutterの画面
60 Nativeの画面 Flutterの画面 present dismiss Flutterの画面 ● 前回のFlutterの画面の状態が残ってしまっている● 画面を破棄して再生成したら、初期状態になるのでは?
61 じゃらんTOP present dismiss 遊び・体験(Flutter) 遊び・体験(Flutter) 検索条件指定 じゃらん遊び・体験予約 を再度開く 前回の検索条件のまま
62 じゃらんTOP present dismiss 遊び・体験(Flutter) 遊び・体験(Flutter) 検索条件指定 遊び・体験を再度開く 前回の検索条件のまま ● Add-to-appでFlutterの画面を表示する方法の説明↓● この問題の原因の説明
おさらい: Add-to-app ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的に組み込む仕組み 63 じゃらん アプリ Swift Objective-c じゃらん遊び・体験 Flutterプロジェクト Flutterモジュール
Flutterの画面を表示するために重要なクラス 64 FlutterEngineFlutterViewControllerFlutterView
Flutter View Controller View Controller Flutterの画面を表示するために重要なクラス 65 FlutterEngineFlutterViewControllerFlutterView● ViewControllerの派生クラス ● FlutterViewControllerに遷移する ことでFlutterの画面を表示 画面遷移
Flutterの画面を表示するために重要なクラス 66 FlutterEngineFlutterViewControllerFlutterView● FlutterViewControllerに 乗っているView ● FlutterモジュールのUIが描画される FlutterViewController FlutterView
Flutterの画面を表示するために重要なクラス 67 FlutterEngineFlutterViewControllerFlutterView● Dartを実行して、FlutterViewに FlutterモジュールのUIを描画する FlutterViewController FlutterView FlutterEngine
Flutterの画面を表示するために重要なクラス 68 FlutterEngineFlutterViewControllerFlutterView● Dartを実行して、FlutterViewに FlutterモジュールのUIを描画する FlutterViewController FlutterView FlutterEngine • Flutterの画面を描画するためには、FlutterEngineの初期化が必要
class AppDelegate: FlutterAppDelegate {lazy var flutterEngine = FlutterEngine(name: "my flutter engine")override func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {flutterEngine.run();GeneratedPluginRegistrant.register(with: self.flutterEngine);return super.application(application, didFinishLaunchingWithOptions: launchOptions);}}FlutterEngineの初期化 69 • AppDelegateにおいてFlutterEngineインスタンスの生成
class AppDelegate: FlutterAppDelegate {lazy var flutterEngine = FlutterEngine(name: "my flutter engine")override func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {flutterEngine.run();GeneratedPluginRegistrant.register(with: self.flutterEngine);return super.application(application, didFinishLaunchingWithOptions: launchOptions);}}FlutterEngineの初期化 70 • Dartのmainを実行し、FlutterEngineの初期化を行う• FlutterEngineの初期化は時間がかかるため、予め呼ぶ必要がある
class ViewController: UIViewController {override func viewDidLoad() {super.viewDidLoad()}@IBAction func showFlutter(_ sender: Any) {let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEnginelet flutterViewController =FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)present(flutterViewController, animated: true, completion: nil)}}FlutterViewControllerへ遷移 71 • FlutterEngineを指定して、FlutterViewControllerをインスタンス化• FlutterViewControllerに画面遷移することでFlutterの画面を表示
何故画面を再生成しても初期状態にならない? 72 FlutterEngineFlutterViewController● Flutterの画面を閉じた段階でFlutterViewControlerは破棄される
● FlutterEngineはAppDelegateで初期化し、 参照を保持しておくので、破棄されない 何故画面を再生成しても初期状態にならない? 73 FlutterEngineclass AppDelegate: FlutterAppDelegate {lazy var flutterEngine = FlutterEngine(name: "my flutter engine")FlutterViewController
● FlutterEngineはAppDelegateで初期化し、 参照を保持しておくので、破棄されない 何故画面を再生成しても初期状態にならない? 74 FlutterEngine● Dartを実行しているのはFlutterEngine ● Flutterの画面を閉じても、Dart内で破棄 していないStateは残ってしまう FlutterViewController
● FlutterEngineはAppDelegateで初期化し、 参照を保持しておくので、破棄されない 何故画面を再生成しても初期状態にならない? 75 FlutterEngine● Dartを実行しているのはFlutterEngine ● Flutterの画面を閉じても、Dart内で破棄 していないStateは残ってしまう ● 時間がかかるためFlutterEngineを毎回初期化するわけにもいかない FlutterViewController
● Flutterモジュールの最初に空の画面を挿入(InitialPage) ● FlutterViewController遷移時に ○ InitialPage以外のページを全て破棄 ○ 本来最初に表示したいページを生成して、 即座に遷移する どう回避したか 76
FlutterViewControllerに遷移するコード class ViewController: UIViewController {...@IBAction func showFlutter(_ sender: Any) {let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEnginelet flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)let channel = FlutterMethodChannel(name: "channel",binaryMessenger: flutterViewController.binaryMessenger)channel.invokeMethod("setup", arguments: nil);flutterViewController.modalPresentationStyle = .fullScreenpresent(flutterViewController, animated: true, completion: nil)}}77 • Method Channelを使用して、setupのDartコードを呼び出す
FlutterViewControllerに遷移するコード class ViewController: UIViewController {...@IBAction func showFlutter(_ sender: Any) {let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEnginelet flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)let channel = FlutterMethodChannel(name: "channel",binaryMessenger: flutterViewController.binaryMessenger)channel.invokeMethod("setup", arguments: nil);flutterViewController.modalPresentationStyle = .fullScreenpresent(flutterViewController, animated: true, completion: nil)}}78 • その後FlutterViewControllerへ画面遷移する
Initial Pageのコード class InitialPage extends StatelessWidget {static const MethodChannel channel = MethodChannel('channel');@overrideWidget build(BuildContext context) {channel.setMethodCallHandler((MethodCall call) async {switch (call.method) {case 'setup':return Navigator.pushNamedAndRemoveUntil(context,TopPage.routeName,(Route route) => route.isFirst,);}79 • 空のInitial pageを作成• Flutterモジュールの先頭の画面に設定
Initial Pageのコード class InitialPage extends StatelessWidget {static const MethodChannel channel = MethodChannel('channel');@overrideWidget build(BuildContext context) {channel.setMethodCallHandler((MethodCall call) async {switch (call.method) {case 'setup':return Navigator.pushNamedAndRemoveUntil(context,TopPage.routeName,(Route route) => route.isFirst,);}80 • setupのMethod Channelが呼び出された際に実行されるコード
• pushNamedAndRemoveUntil条件が満たされるまで、画面を破棄する。その後新たな画面をpushInitial Pageのコード class InitialPage extends StatelessWidget {static const MethodChannel channel = MethodChannel('channel');@overrideWidget build(BuildContext context) {channel.setMethodCallHandler((MethodCall call) async {switch (call.method) {case 'setup':return Navigator.pushNamedAndRemoveUntil(context,TopPage.routeName,(Route route) => route.isFirst,);}81
● 条件一番最初の画面であること。すなわちInitial Pageに到達するまで画面が破棄されるInitial Pageのコード class InitialPage extends StatelessWidget {static const MethodChannel channel = MethodChannel('channel');@overrideWidget build(BuildContext context) {channel.setMethodCallHandler((MethodCall call) async {switch (call.method) {case 'setup':return Navigator.pushNamedAndRemoveUntil(context,TopPage.routeName,(Route route) => route.isFirst,);}82
● 条件を満たしたタイミングで本来表示したかった最初の画面がpushされるInitial Pageのコード class InitialPage extends StatelessWidget {static const MethodChannel channel = MethodChannel('channel');@overrideWidget build(BuildContext context) {channel.setMethodCallHandler((MethodCall call) async {switch (call.method) {case 'setup':return Navigator.pushNamedAndRemoveUntil(context,TopPage.routeName,(Route route) => route.isFirst,);}83
84 Nativeの画面 Flutterの画面 present dismiss Flutterの画面 ● 再度表示した際に、初期化される
直面した課題 85 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google Mapのクラッシュ
● 詳細なデバッグや試験を行う際に パケットモニタリングを使用したい ● iOSプロジェクトにおいては Wi-Fi設定からproxyサーバーの IPアドレスとポート番号を入力する ことでパケットモニタリング可能 (例: Charles) iOSプロジェクトでパケットモニタリング 86
● 同様のWi-Fi設定をFlutterプロジェクトに行っても、通信がProxyサーバーを経由せず パケットモニタリングを使用できない 発生した問題 87
proxyサーバーを経由するためには 88 ● HttpClientクラスに プロキシ自動設定(PAC)を 明示的に指定する必要がある
final httpClient = HttpClient();httpClient.findProxy = (url) {return 'PROXY localhost:8888; DIRECT';};final request = await httpClient.getUrl(Uri.https('jsonplaceholder.typicode.com', '/posts'));final response = await request.close();● HttpClientのfindProxyにPACを指定するPACを指定する方法(HttpClient) 89
Future main() async {HttpOverrides.global = _HttpOverrides();runApp(Application());}class _HttpOverrides extends HttpOverrides {@overrideHttpClient createHttpClient(SecurityContext context) {return super.createHttpClient(context)..findProxy = (uri) {return 'PROXY localhost:8888; DIRECT';};}● HttpOverridesを継承したクラスを定義PACを指定する方法(httpパッケージ) 90
Future main() async {HttpOverrides.global = _HttpOverrides();runApp(Application());}class _HttpOverrides extends HttpOverrides {@overrideHttpClient createHttpClient(SecurityContext context) {return super.createHttpClient(context)..findProxy = (uri) {return 'PROXY localhost:8888; DIRECT';};}● HttpOverridesのcreateHttpClientメソッドをoverrideする● 作成されるHttpClientクラスにfindProxyを指定するPACを指定する方法(httpパッケージ) 91
Future main() async {HttpOverrides.global = _HttpOverrides();runApp(Application());}class _HttpOverrides extends HttpOverrides {@overrideHttpClient createHttpClient(SecurityContext context) {return super.createHttpClient(context)..findProxy = (uri) {return 'PROXY localhost:8888; DIRECT';};}● HttpOverridesの派生クラスのインスタンスをHttpOverrides.globalに指定するPACを指定する方法(httpパッケージ) 92
Future main() async {HttpOverrides.global = _HttpOverrides();runApp(Application());}class _HttpOverrides extends HttpOverrides {@overrideHttpClient createHttpClient(SecurityContext context) {return super.createHttpClient(context)..findProxy = (uri) {return 'PROXY localhost:8888; DIRECT';};}● PACをベタ書きしているPACを指定する方法(httpパッケージ) 93
システムProxyからPACを指定する ● じゃらんではsystem_proxyパッケージを使用 94 pub system_proxy:https://pub.dev/packages/system_proxy, (参照2020-08-10)
void main() async {WidgetsFlutterBinding.ensureInitialized();Map proxy = await SystemProxy.getProxySettings();...HttpOverrides.global = _HttpOverrides(proxy['host'], proxy['port']);runApp(Application());}class _HttpOverrides extends HttpOverrides {_HttpOverrides(this._host, this._port);final String _host;final String _port;@overrideHttpClient createHttpClient(SecurityContext context) {return super.createHttpClient(context)..findProxy = (uri) {return _host != null ? "PROXY $_host:$_port;" : 'DIRECT';};● システムのProxy設定を取得● その情報を使用してPACをfindProxyに指定システムProxyからPACを指定する 95
直面した課題 96 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google Mapのクラッシュ
Google Mapの使用 ● レジャー施設の場所や集合場所を示すために、地図(Google Map)を表示する ● google_maps_flutterパッケージを使用 97 pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)
● google_maps_flutterはDevelopers Preview ● 実際に使用すると、 Google Mapを何度も表示した際に アプリがクラッシュする問題が発覚 ● Google Mapを閉じてもメモリが解放 されない ● 地図を開くたびにメモリを圧迫してしまい、 最終的にクラッシュしてしまっていた 発生した問題 98 Memory Usage
原因と問題の回避 ● GoogleMapが内部で使用しているPlatformViewに おいて循環参照があり、それによってGoogleMapが 解放されなくなっていた ● 当時使用していたFlutter ver1.12.13+hotfix.7から Flutter ver1.15.17にアップデートしたことで、 メモリが解放される様になり、回避することができた 99
ライブラリのステータス ● developers preview等のライブラリや機能に関する 既知の問題には、issueにタグが付与されている ● その様なライブラリや機能を使用する際には、 タグでフィルタリングして、関連issueを確認することで 事前に問題を把握すると吉 100 pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)
直面した課題まとめ ● Flutterを採用してみると、いくつかの課題に直面した ● GoogleMapがdevelopers previewである等、 プラットフォームの未成熟な部分は若干ある? ● しかし、いずれの直面した課題も回避することは できていて、プロダクション採用不可能となる様な 事態には直面しなかった 101
これらの課題を乗り越えた結果 どんなメリットを得られたのか 102
工数の削減開発効率の向上最も大きく得られたメリット 103
● 開発効率は著しく向上した 1. 既成部品の充実 2. hot reload/restart 3. IDE(Android Studio)の機能の充実 得られたメリット:開発効率の向上 104 開発効率の向上
1. 既成部品の充実 105 ● Widgetの種類がとても充実 している ● じゃらんにおいては、これら既成部品でほぼ事足りた ● 既成部品を積極的に使用できたことが、開発効率向上に 寄与した Widget catalog:https://flutter.dev/docs/development/ui/widgets, (参照2020-08-10)
● コードを修正した際に、ビルドし直さなくても その修正が即座にアプリに反映される仕組み ○ hot reload: 約0.5s ○ hot restart: 約3s ● じゃらんはビルド時間が大分増加してしまって いたので、この仕組みの開発効率向上への 寄与は大きかった 2. hot reload/ restart 106
● Widgetの上でoption+Enterを押すことで、包むWidget等の候補を表示。 Widgetツリーの構築をサクサクできる ● “stless”や”stful”と打つことで Stateless WidgetやStateful Widget を自動生成 3. IDE(Android Studio)の機能の充実 107 ● XCodeで開発する場合に比べて開発スピードが向上した
1. iOS/Androidの開発工数削減 2. 開発以外の工数削減 3. 移行工数の削減 得られたメリット:工数の削減 108 工数の削減
● じゃらんはメディアであり、 プラットフォーム固有の機能が少ない ● 完全移行が完了すれば、iOS/Androidの開発工数を ほぼ半分にすることができそう 1. iOS/Androidの開発工数削減 109
● iOSとAndroidの仕様差分をなるべく減らす ● デザインをマテリアルデザインに統一 ● 開発以外の工数も削減することができている 2. 開発以外の工数削減 110 開発工数 要件検討工数 デザイン作成工数 5割減 5割減 3割減
● 段階的移行を行っていることにより、各プラットフォームの実装が多少必要になっている ○ 例えば、ネイティブ側が保持するアプリの設定情報を Flutterモジュールに伝播する処理 ● しかし、大部分は共通化できていて、その点 移行コストも大きく削減することができている 3. 移行工数の削減 111
開発効率向上、工数削減以外にも多くのメリット ● 宣言的UI構築が素晴らしい ○ 参考: 宣言的UI そな太さんhttps://speakerdeck.com/sonatard/xuan-yan-de-ui ● FlutterがOSSであることで、内部処理を確認できる ● パフォーマンスモニタリングが充実している ● 全てDartで記述するため、コードレビューや コンフリクトの解消がしやすい などなど... 112
まとめ 113
● じゃらんでは現在Flutterへの段階的移行を行っている ● 複数課題に直面したものの、回避することはできた ● 直面した課題を乗り越えたことで、開発効率向上や開発 工数削減など多くのメリットを得ることができた ● 完全移行に向けて引き続きFlutter頑張ります まとめ 114
ありがとうございました! 115