Pro Yearly is on sale from $80 to $50! »

Flutter移行の苦労と、乗り越えた先に得られたもの

 Flutter移行の苦労と、乗り越えた先に得られたもの

2020/9/20_iOSDC Japan 2020での、桐山の講演資料になります

Eea9a05e6e222a3d50c73f54a49fadf4?s=128

Recruit Technologies

October 01, 2020
Tweet

Transcript

  1. Flutter移行の苦労と、
 乗り越えた先に得られたもの
 Recruit Co., Ltd. Keisuke Kiriyama
 1


  2. • Recruit Co., Ltd.
 • iOS / Flutter 
 •

    じゃらんアプリ開発T
 Keisuke Kiriyama

  3. 3
 旅を、もっと豊かに
 宿・ホテル予約アプリ


  4. 現在じゃらんは
 Flutterへの移行に挑戦しています!
 4


  5. Flutterとは
 • Google製のクロスプラットフォームSDK
 
 • 単一のソースコードで、複数のプラットフォームの
 アプリケーションを構築可能
 
 • 開発言語:

    Dart
 5

  6. 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)
  7. 国内におけるFlutterのプロダクション採用
 • しかし、国内においてFlutterを
 プロダクションに採用している例はそれほど多くない
 • 弊社においてもFlutterを採用したのはじゃらんが初
 
 7


  8. Flutterどうなの?
 8
 実際メリット 得られるの? 課題はないの?

  9. Flutterを採用して実際どうだったのかをお伝えします
 話すこと
 9


  10. Flutterを採用して実際どうだったのかをお伝えします
 1. どんな技術的課題に直面したのか
 
 話すこと
 10


  11. Flutterを採用して実際どうだったのかをお伝えします
 1. どんな技術的課題に直面したのか
 2. 課題を乗り越えた結果、どんなメリットを得られたのか
 
 話すこと
 11


  12. 発表のゴール
 • Flutter開発経験者
 →直面した課題と得られたメリットを知り
  技術選定の際の判断材料になる
 
 • Flutter開発未経験者
 →まずはFlutter触ってみたいと思ってもらう
 12


  13. 1. 前提の共有
 ◦ じゃらんのFlutter移行
 ◦ Flutterのレイアウト構築
 
 2. 直面した課題
 


    3. 得られたメリット
 
 4. まとめ
 説明の流れ
 13

  14. じゃらんのFlutter移行
 14


  15. Flutter採用の背景
 • じゃらんアプリはiOS/Android共にリリースから
 10年を迎え、長年に渡る開発が行われてきた
 
 
 
 • 上記課題を解決するために、リプレースを検討
 15


    プロジェクトの大規模化によ るビルド時間の増加 プロジェクト全体の コードが古くなっている
  16. じゃらんアプリのリプレース検討
 • クロスプラットフォーム技術の検討
 ◦ iOS/Android開発工数
 ◦ リプレースコスト
 
 • クロスプラットフォーム技術の中でも


    Flutterの開発生産性が最も高いと実感し
 Flutterの採用を決断
 16
 半減

  17. 17


  18. 18


  19. 19
 ここから先の画面は
 全てFlutterで実装


  20. Flutterへの段階的移行
 • Add-to-app(Add Flutter to existing app)を使用
 • 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み


    
 20
 じゃらん
 アプリ
 Swift
 Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト

  21. Flutterへの段階的移行
 • Add-to-app(Add Flutter to existing app)を使用
 • 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み


    
 21
 じゃらん
 アプリ
 Swift
 Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト
 Flutterモジュール

  22. Flutterのレイアウト構築
 22


  23. Flutterのレイアウト構築
 • Widget
 ◦ UIの構成情報を保持するクラス
 
 • Widgetをツリー上に構成することによって
 UIの構築を行う
 


    23

  24. 24
 class HomePage extends StatelessWidget { @override Widget build(BuildContext context)

    { return Scaffold( appBar: AppBar( title: Text('Flutter Demo Home Page'), ), body: Center( child: Text( 'Hello iOSDC!!', style: TextStyle( fontSize: 30, ), ...
  25. 25
 class HomePage extends StatelessWidget { @override Widget build(BuildContext context)

    { return Scaffold( appBar: AppBar( title: Text('Flutter Demo Home Page'), ), body: Center( child: Text( 'Hello iOSDC!!', style: TextStyle( fontSize: 30, ), ...
  26. 26
 class HomePage extends StatelessWidget { @override Widget build(BuildContext context)

    { return Scaffold( appBar: AppBar( title: Text('Flutter Demo Home Page'), ), body: Center( child: Text( 'Hello iOSDC!!', style: TextStyle( fontSize: 30, ), ...
  27. 27
 class HomePage extends StatelessWidget { @override Widget 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. 直面した課題
 28


  29. 直面した課題
 29
 1. タブ切り替えのパフォーマンス
 2. Flutterの画面が初期化されない
 3. ネットワーク通信がproxyサーバーを経由しない
 4. Google

    Mapのクラッシュ

  30. 直面した課題
 30
 1. タブ切り替えのパフォーマンス
 2. Flutterの画面が初期化されない
 3. ネットワーク通信がproxyサーバーを経由しない
 4. Google

    Mapのクラッシュ

  31. • タブを使用して表示する情報を
 切り替えるページが存在する
 
 • このタブのページでは、
 口コミの一覧や、プランの一覧を
 リストで表示する
 タブの切り替えをする画面
 31


  32. 発生した問題
 • リストのアイテムを大量に読み込んでタブを切り替える
 
 
 • タブ切り替えのアニメーションが重くなってしまう
 • まれにタブ切り替えのタイミングで
 アプリがクラッシュすることがある


    32

  33. class TabPage extends StatelessWidget { @override Widget build(BuildContext context) {

    return DefaultTabController( ... child: Scaffold( appBar: AppBar( title: Text('Tab Sample'), bottom: TabBar(tabs: <Widget>[ const Tab(child: Text('Tab A')), const Tab(child: Text('Tab B')) ])), body: TabBarView( children: <Widget>[ TabA(), TabB(), ], • タブのページを作成する サンプルコード: タブのページ
 33

  34. class TabPage extends StatelessWidget { @override Widget build(BuildContext context) {

    return DefaultTabController( ... child: Scaffold( appBar: AppBar( title: Text('Tab Sample'), bottom: TabBar(tabs: <Widget>[ const Tab(child: Text('Tab A')), const Tab(child: Text('Tab B')) ])), body: TabBarView( children: <Widget>[ TabA(), TabB(), ], • 2つのタブ • TabA() • TabB() サンプルコード: タブのページ
 34

  35. class TabA extends StatelessWidget { @override Widget build(BuildContext context) {

    return Center( child: Text('Tab A'), ); } } • TabA()は画面の中心に “Tab A”を表示するだけ サンプルコード: TabA
 35

  36. class TabB extends StatelessWidget { @override Widget 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. 37
 タブ切り替え
 TabA
 TabB


  38. 38
 TabB
 • Tab Bのリストを下までスクロールする

  39. 39
 TabB
 TabA
 TabAに
 切り替え
 TabBに
 切り替え
 TabB


  40. 40
 TabB
 TabA
 TabAに
 切り替え
 TabBに
 切り替え
 TabB
 • タブ切り替えのアニメーションが非

    常に重くなる • まれにクラッシュする
  41. なぜアニメーション重くなる?
 • タブを切り替えた際、表示
 されないタブはWidget
 ツリーから除外される
 41
 TabA表示時
 TabB表示時


  42. • 再度タブを表示する際には、表示するタブの
 レイアウトを再計算する必要がある
 
 なぜアニメーション重くなる?
 • タブを切り替えた際、表示
 されないタブはWidget
 ツリーから除外される
 42


    TabA表示時
 TabB表示時

  43. タブが保持するリストのアイテムの高さが可変の場合
 • 1つ目のアイテムから順にレイアウト
 を計算して、高さを決定しないと
 
 なぜアニメーション重くなる?
 43
 …


  44. タブが保持するリストのアイテムの高さが可変の場合
 • 1つ目のアイテムから順にレイアウト
 を計算して、高さを決定しないと
 
 • 前回のスクロール位置の
 アイテムを表示できない
 
 


    なぜアニメーション重くなる?
 44
 …
 前回の
 スクロール位置

  45. タブが保持するリストのアイテムの高さが可変の場合
 • 1つ目のアイテムから順にレイアウト
 を計算して、高さを決定しないと
 
 • 前回のスクロール位置の
 アイテムを表示できない
 
 •

    この演算のために
 パフォーマンスが低下
 なぜアニメーション重くなる?
 45
 …
 前回の
 スクロール位置

  46. なぜまれにクラッシュする?
 • リストのレイアウトを決定する演算を行うことで、
 一時的にメモリを圧迫する
 • この一時的な圧迫で許容値を超えてしまった場合に
 クラッシュが発生していた
 46
 タブ切り替え時


  47. どう回避したか
 • タブのWidgetにAutomaticKeepAliveClientMixin
 を適用する
 • 非表示になったタブもWidgetツリーから除外されなくなるた め、再度レイアウトの演算が不要
 47
 TabA表示時にTabBが除外されない 


  48. class TabB extends StatefulWidget { ... class _TabBState extends State<TabB>

    with AutomaticKeepAliveClientMixin { @override Widget 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'), ... @override bool get wantKeepAlive => true; } • TabのWidgetを StatefulWidgetに変更する タブにAutomaticKeepAliveClientMixinを適用
 48

  49. class TabB extends StatefulWidget { ... class _TabBState extends State<TabB>

    with AutomaticKeepAliveClientMixin { @override Widget 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'), ... @override bool get wantKeepAlive => true; } • Stateに AutomaticKeepAlive ClientMixin を適用する タブにAutomaticKeepAliveClientMixinを適用
 49

  50. class TabB extends StatefulWidget { ... class _TabBState extends State<TabB>

    with AutomaticKeepAliveClientMixin { @override Widget 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'), ... @override bool get wantKeepAlive => true; } タブにAutomaticKeepAliveClientMixinを適用
 50
 • super.buildの呼び出し • wantKeepAliveのgetterで trueを返す
  51. class TabB extends StatefulWidget { ... class _TabBState extends State<TabB>

    with AutomaticKeepAliveClientMixin { @override Widget 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'), ... @override bool get wantKeepAlive => true; } タブにAutomaticKeepAliveClientMixinを適用
 51
 • 一時的なメモリ圧迫も 起こらなくなる
  52. 直面した課題
 52
 1. タブ切り替えのパフォーマンス
 2. Flutterの画面が初期化されない
 3. ネットワーク通信がproxyサーバーを経由しない
 4. Google

    Mapのクラッシュ

  53. 53
 Nativeの画面
 Flutterの画面


  54. 発生した問題
 • Flutterの画面を閉じて再度開く
 
 
 • 前回開いた画面の状態が残ってしまっている
 54


  55. 55
 Nativeの画面
 Flutterの画面


  56. 56
 Nativeの画面
 • Flutterの画面に遷移する

  57. 57
 Flutterの画面
 • +のFABをタップ • 画面の中心にタップした回数が表示

  58. 58
 Nativeの画面
 Flutterの画面
 dismiss


  59. 59
 Nativeの画面
 Flutterの画面
 present
 dismiss
 Flutterの画面


  60. 60
 Nativeの画面
 Flutterの画面
 present
 dismiss
 Flutterの画面
 • 前回のFlutterの画面の 状態が残ってしまっている •

    画面を破棄して再生成したら、初 期状態になるのでは?
  61. 61
 じゃらんTOP
 
 present
 dismiss
 遊び・体験(Flutter)
 遊び・体験(Flutter)
 検索条件指定
 じゃらん遊び・体験予約
 を再度開く


    前回の検索条件のまま

  62. 62
 じゃらんTOP
 
 present
 dismiss
 遊び・体験(Flutter)
 遊び・体験(Flutter)
 検索条件指定
 遊び・体験を再度開く
 前回の検索条件のまま


    • Add-to-appでFlutterの画面を表示する方法の説明 ↓ • この問題の原因の説明
  63. おさらい: Add-to-app
 • 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み
 
 63
 じゃらん
 アプリ
 Swift


    Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト
 Flutterモジュール

  64. Flutterの画面を表示するために重要なクラス
 64
 FlutterEngine FlutterViewController FlutterView

  65. Flutter
 View
 Controller
 
 
 View
 Controller
 
 
 Flutterの画面を表示するために重要なクラス


    65
 FlutterEngine FlutterViewController FlutterView • ViewControllerの派生クラス
 • FlutterViewControllerに遷移する
 ことでFlutterの画面を表示
 画面遷移

  66. Flutterの画面を表示するために重要なクラス
 66
 FlutterEngine FlutterViewController FlutterView • FlutterViewControllerに
 乗っているView
 • FlutterモジュールのUIが描画される


    FlutterViewController
 FlutterView

  67. Flutterの画面を表示するために重要なクラス
 67
 FlutterEngine FlutterViewController FlutterView • Dartを実行して、FlutterViewに
 FlutterモジュールのUIを描画する
 FlutterViewController
 FlutterView


    FlutterEngine

  68. Flutterの画面を表示するために重要なクラス
 68
 FlutterEngine FlutterViewController FlutterView • Dartを実行して、FlutterViewに
 FlutterモジュールのUIを描画する
 FlutterViewController
 FlutterView


    FlutterEngine
 • Flutterの画面を描画するためには、 FlutterEngineの初期化が必要
  69. class AppDelegate: FlutterAppDelegate { lazy var flutterEngine = FlutterEngine(name: "my

    flutter engine") override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { flutterEngine.run(); GeneratedPluginRegistrant.register(with: self.flutterEngine); return super.application(application, didFinishLaunchingWithOptions: launchOptions); } } FlutterEngineの初期化
 69
 • AppDelegateにおいてFlutterEngineインスタンスの生成
  70. class AppDelegate: FlutterAppDelegate { lazy var flutterEngine = FlutterEngine(name: "my

    flutter engine") override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { flutterEngine.run(); GeneratedPluginRegistrant.register(with: self.flutterEngine); return super.application(application, didFinishLaunchingWithOptions: launchOptions); } } FlutterEngineの初期化
 70
 • Dartのmainを実行し、FlutterEngineの初期化を行う • FlutterEngineの初期化は時間がかかるため、予め呼ぶ必要がある
  71. class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() }

    @IBAction func showFlutter(_ sender: Any) { let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) present(flutterViewController, animated: true, completion: nil) } } FlutterViewControllerへ遷移
 71
 • FlutterEngineを指定して、FlutterViewControllerをインスタンス化 • FlutterViewControllerに画面遷移することでFlutterの画面を表示
  72. 何故画面を再生成しても初期状態にならない?
 72
 FlutterEngine FlutterViewController • Flutterの画面を閉じた段階で FlutterViewControlerは破棄される


  73. • FlutterEngineはAppDelegateで初期化し、
 参照を保持しておくので、破棄されない
 何故画面を再生成しても初期状態にならない?
 73
 FlutterEngine class AppDelegate: FlutterAppDelegate {

    lazy var flutterEngine = FlutterEngine(name: "my flutter engine") FlutterViewController
  74. • FlutterEngineはAppDelegateで初期化し、
 参照を保持しておくので、破棄されない
 何故画面を再生成しても初期状態にならない?
 74
 FlutterEngine • Dartを実行しているのはFlutterEngine
 • Flutterの画面を閉じても、Dart内で破棄


    していないStateは残ってしまう
 FlutterViewController
  75. • FlutterEngineはAppDelegateで初期化し、
 参照を保持しておくので、破棄されない
 何故画面を再生成しても初期状態にならない?
 75
 FlutterEngine • Dartを実行しているのはFlutterEngine
 • Flutterの画面を閉じても、Dart内で破棄


    していないStateは残ってしまう
 • 時間がかかるためFlutterEngineを毎回初期化 するわけにもいかない
 FlutterViewController
  76. • Flutterモジュールの最初に空の画面を挿入(InitialPage)
 
 • FlutterViewController遷移時に
 ◦ InitialPage以外のページを全て破棄
 ◦ 本来最初に表示したいページを生成して、
 即座に遷移する


    
 どう回避したか
 76

  77. FlutterViewControllerに遷移するコード
 class ViewController: UIViewController { ... @IBAction func showFlutter(_ sender:

    Any) { let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) let channel = FlutterMethodChannel(name: "channel", binaryMessenger: flutterViewController.binaryMessenger) channel.invokeMethod("setup", arguments: nil); flutterViewController.modalPresentationStyle = .fullScreen present(flutterViewController, animated: true, completion: nil) } } 77
 • Method Channelを使用して、setupのDartコードを呼び出す
  78. FlutterViewControllerに遷移するコード
 class ViewController: UIViewController { ... @IBAction func showFlutter(_ sender:

    Any) { let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) let channel = FlutterMethodChannel(name: "channel", binaryMessenger: flutterViewController.binaryMessenger) channel.invokeMethod("setup", arguments: nil); flutterViewController.modalPresentationStyle = .fullScreen present(flutterViewController, animated: true, completion: nil) } } 78
 • その後FlutterViewControllerへ画面遷移する
  79. Initial Pageのコード
 class InitialPage extends StatelessWidget { static const MethodChannel

    channel = MethodChannel('channel'); @override Widget build(BuildContext context) { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'setup': return Navigator.pushNamedAndRemoveUntil<void>( context, TopPage.routeName, (Route<dynamic> route) => route.isFirst, ); } 79
 • 空のInitial pageを作成 • Flutterモジュールの先頭の 画面に設定
  80. Initial Pageのコード
 class InitialPage extends StatelessWidget { static const MethodChannel

    channel = MethodChannel('channel'); @override Widget build(BuildContext context) { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'setup': return Navigator.pushNamedAndRemoveUntil<void>( context, TopPage.routeName, (Route<dynamic> route) => route.isFirst, ); } 80
 • setupのMethod Channelが 呼び出された際に 実行されるコード
  81. • pushNamedAndRemoveUntil 条件が満たされるまで、 画面を破棄する。 その後新たな画面をpush Initial Pageのコード
 class InitialPage extends

    StatelessWidget { static const MethodChannel channel = MethodChannel('channel'); @override Widget build(BuildContext context) { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'setup': return Navigator.pushNamedAndRemoveUntil<void>( context, TopPage.routeName, (Route<dynamic> route) => route.isFirst, ); } 81

  82. • 条件 一番最初の画面であること。 すなわちInitial Pageに到達す るまで画面が破棄される Initial Pageのコード
 class InitialPage

    extends StatelessWidget { static const MethodChannel channel = MethodChannel('channel'); @override Widget build(BuildContext context) { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'setup': return Navigator.pushNamedAndRemoveUntil<void>( context, TopPage.routeName, (Route<dynamic> route) => route.isFirst, ); } 82

  83. • 条件を満たしたタイミングで本 来表示したかった 最初の画面がpushされる Initial Pageのコード
 class InitialPage extends StatelessWidget

    { static const MethodChannel channel = MethodChannel('channel'); @override Widget build(BuildContext context) { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'setup': return Navigator.pushNamedAndRemoveUntil<void>( context, TopPage.routeName, (Route<dynamic> route) => route.isFirst, ); } 83

  84. 84
 Nativeの画面
 Flutterの画面
 present
 dismiss
 Flutterの画面
 • 再度表示した際に、初期化される

  85. 直面した課題
 85
 1. タブ切り替えのパフォーマンス
 2. Flutterの画面が初期化されない
 3. ネットワーク通信がproxyサーバーを経由しない
 4. Google

    Mapのクラッシュ

  86. • 詳細なデバッグや試験を行う際に
 パケットモニタリングを使用したい
 • iOSプロジェクトにおいては
 Wi-Fi設定からproxyサーバーの
 IPアドレスとポート番号を入力する
 ことでパケットモニタリング可能
 (例: Charles)


    iOSプロジェクトでパケットモニタリング
 86

  87. • 同様のWi-Fi設定をFlutterプロジェクトに行っても、通信が Proxyサーバーを経由せず
 パケットモニタリングを使用できない
 
 発生した問題
 87


  88. proxyサーバーを経由するためには
 88
 • HttpClientクラスに
 プロキシ自動設定(PAC)を
 明示的に指定する必要がある


  89. 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

  90. Future<void> main() async { HttpOverrides.global = _HttpOverrides(); runApp(Application()); } class

    _HttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = (uri) { return 'PROXY localhost:8888; DIRECT'; }; } • HttpOverridesを継承した クラスを定義 PACを指定する方法(httpパッケージ)
 90

  91. Future<void> main() async { HttpOverrides.global = _HttpOverrides(); runApp(Application()); } class

    _HttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = (uri) { return 'PROXY localhost:8888; DIRECT'; }; } • HttpOverridesの createHttpClientメソッドを overrideする • 作成されるHttpClientクラス にfindProxyを指定する PACを指定する方法(httpパッケージ)
 91

  92. Future<void> main() async { HttpOverrides.global = _HttpOverrides(); runApp(Application()); } class

    _HttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = (uri) { return 'PROXY localhost:8888; DIRECT'; }; } • HttpOverridesの派生クラス のインスタンスを HttpOverrides.globalに 指定する PACを指定する方法(httpパッケージ)
 92

  93. Future<void> main() async { HttpOverrides.global = _HttpOverrides(); runApp(Application()); } class

    _HttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = (uri) { return 'PROXY localhost:8888; DIRECT'; }; } • PACをベタ書きしている PACを指定する方法(httpパッケージ)
 93

  94. システムProxyからPACを指定する
 • じゃらんではsystem_proxyパッケージを使用
 94
 pub system_proxy:https://pub.dev/packages/system_proxy, (参照2020-08-10)

  95. void main() async { WidgetsFlutterBinding.ensureInitialized(); Map<String, String> 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; @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = (uri) { return _host != null ? "PROXY $_host:$_port;" : 'DIRECT'; }; • システムのProxy設定を 取得 • その情報を使用してPACを findProxyに指定 システムProxyからPACを指定する
 95

  96. 直面した課題
 96
 1. タブ切り替えのパフォーマンス
 2. Flutterの画面が初期化されない
 3. ネットワーク通信がproxyサーバーを経由しない
 4. Google

    Mapのクラッシュ

  97. Google Mapの使用
 • レジャー施設の場所や集合場所を示 すために、地図(Google Map)を表示す る
 • google_maps_flutterパッケージを使用
 97


    pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)
  98. • google_maps_flutterはDevelopers Preview
 • 実際に使用すると、
 Google Mapを何度も表示した際に
 アプリがクラッシュする問題が発覚
 
 •

    Google Mapを閉じてもメモリが解放
 されない
 • 地図を開くたびにメモリを圧迫してしまい、
 最終的にクラッシュしてしまっていた
 発生した問題
 98
 Memory Usage

  99. 原因と問題の回避
 • GoogleMapが内部で使用しているPlatformViewに
 おいて循環参照があり、それによってGoogleMapが
 解放されなくなっていた
 
 • 当時使用していたFlutter ver1.12.13+hotfix.7から
 Flutter

    ver1.15.17にアップデートしたことで、
 メモリが解放される様になり、回避することができた
 
 
 99

  100. ライブラリのステータス
 • developers preview等のライブラリや機能に関する
 既知の問題には、issueにタグが付与されている
 • その様なライブラリや機能を使用する際には、
 タグでフィルタリングして、関連issueを確認することで
 事前に問題を把握すると吉
 100


    pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)
  101. 直面した課題まとめ
 • Flutterを採用してみると、いくつかの課題に直面した
 • GoogleMapがdevelopers previewである等、
 プラットフォームの未成熟な部分は若干ある?
 • しかし、いずれの直面した課題も回避することは
 できていて、プロダクション採用不可能となる様な


    事態には直面しなかった
 101

  102. これらの課題を乗り越えた結果
 どんなメリットを得られたのか
 102


  103. 工数の 削減 開発効率 の向上 最も大きく得られたメリット
 103


  104. • 開発効率は著しく向上した
 
 1. 既成部品の充実
 
 2. hot reload/restart
 


    3. IDE(Android Studio)の機能の充実
 
 得られたメリット:開発効率の向上
 104
 開発効率 の向上
  105. 1. 既成部品の充実
 105
 • Widgetの種類がとても充実
 している
 
 • じゃらんにおいては、これら既成 部品でほぼ事足りた


    • 既成部品を積極的に使用できた ことが、開発効率向上に
 寄与した
 
 Widget catalog:https://flutter.dev/docs/development/ui/widgets, (参照2020-08-10)
  106. • コードを修正した際に、ビルドし直さなくても
 その修正が即座にアプリに反映される仕組み
 ◦ hot reload: 約0.5s
 ◦ hot restart:

    約3s 
 
 • じゃらんはビルド時間が大分増加してしまって
 いたので、この仕組みの開発効率向上への
 寄与は大きかった
 2. hot reload/ restart
 106

  107. • Widgetの上でoption+Enterを押すことで、包む Widget等の候補を表示。
 Widgetツリーの構築をサクサクできる
 
 • “stless”や”stful”と打つことで
 Stateless WidgetやStateful Widget


    を自動生成
 3. IDE(Android Studio)の機能の充実
 107
 • XCodeで開発する場合に比べて開発スピードが向上した

  108. 
 
 1. iOS/Androidの開発工数削減
 
 2. 開発以外の工数削減
 
 3. 移行工数の削減


    
 得られたメリット:工数の削減
 108
 工数の 削減
  109. • じゃらんはメディアであり、
 プラットフォーム固有の機能が少ない
 • 完全移行が完了すれば、iOS/Androidの開発工数を
 ほぼ半分にすることができそう
 1. iOS/Androidの開発工数削減
 109


  110. • iOSとAndroidの仕様差分をなるべく減らす
 • デザインをマテリアルデザインに統一
 
 • 開発以外の工数も削減することができている
 
 2. 開発以外の工数削減


    110
 開発工数
 要件検討工数
 デザイン作成工数
 5割減
 5割減
 3割減

  111. • 段階的移行を行っていることにより、各プラットフォームの 実装が多少必要になっている
 ◦ 例えば、ネイティブ側が保持するアプリの設定情報を
 Flutterモジュールに伝播する処理
 
 • しかし、大部分は共通化できていて、その点
 移行コストも大きく削減することができている


    3. 移行工数の削減
 111

  112. 開発効率向上、工数削減以外にも多くのメリット
 • 宣言的UI構築が素晴らしい
 ◦ 参考: 宣言的UI そな太さん https://speakerdeck.com/sonatard/xuan-yan-de-ui
 
 •

    FlutterがOSSであることで、内部処理を確認できる
 
 • パフォーマンスモニタリングが充実している
 
 • 全てDartで記述するため、コードレビューや
 コンフリクトの解消がしやすい
 などなど...
 112

  113. まとめ
 113


  114. • じゃらんでは現在Flutterへの段階的移行を行っている
 • 複数課題に直面したものの、回避することはできた
 • 直面した課題を乗り越えたことで、開発効率向上や開発
 工数削減など多くのメリットを得ることができた
 • 完全移行に向けて引き続きFlutter頑張ります
 まとめ


    114

  115. ありがとうございました!
 115