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

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

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

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

Recruit Technologies

October 01, 2020
Tweet

More Decks by Recruit Technologies

Other Decks in Technology

Transcript

  1. Flutter移行の苦労と、

    乗り越えた先に得られたもの

    Recruit Co., Ltd. Keisuke Kiriyama

    1


    View Slide

  2. • Recruit Co., Ltd.

    • iOS / Flutter 

    • じゃらんアプリ開発T

    Keisuke Kiriyama


    View Slide

  3. 3

    旅を、もっと豊かに

    宿・ホテル予約アプリ


    View Slide

  4. 現在じゃらんは

    Flutterへの移行に挑戦しています!

    4


    View Slide

  5. Flutterとは

    ● Google製のクロスプラットフォームSDK


    ● 単一のソースコードで、複数のプラットフォームの

    アプリケーションを構築可能


    ● 開発言語: Dart

    5


    View Slide

  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)

    View Slide

  7. 国内におけるFlutterのプロダクション採用

    ● しかし、国内においてFlutterを

    プロダクションに採用している例はそれほど多くない

    ● 弊社においてもFlutterを採用したのはじゃらんが初


    7


    View Slide

  8. Flutterどうなの?

    8

    実際メリット
    得られるの?
    課題はないの?

    View Slide

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

    話すこと

    9


    View Slide

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

    1. どんな技術的課題に直面したのか


    話すこと

    10


    View Slide

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

    1. どんな技術的課題に直面したのか

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


    話すこと

    11


    View Slide

  12. 発表のゴール

    ● Flutter開発経験者

    →直面した課題と得られたメリットを知り

     技術選定の際の判断材料になる


    ● Flutter開発未経験者

    →まずはFlutter触ってみたいと思ってもらう

    12


    View Slide

  13. 1. 前提の共有

    ○ じゃらんのFlutter移行

    ○ Flutterのレイアウト構築


    2. 直面した課題


    3. 得られたメリット


    4. まとめ

    説明の流れ

    13


    View Slide

  14. じゃらんのFlutter移行

    14


    View Slide

  15. Flutter採用の背景

    ● じゃらんアプリはiOS/Android共にリリースから

    10年を迎え、長年に渡る開発が行われてきた




    ● 上記課題を解決するために、リプレースを検討

    15

    プロジェクトの大規模化によ
    るビルド時間の増加
    プロジェクト全体の
    コードが古くなっている

    View Slide

  16. じゃらんアプリのリプレース検討

    ● クロスプラットフォーム技術の検討

    ○ iOS/Android開発工数

    ○ リプレースコスト


    ● クロスプラットフォーム技術の中でも

    Flutterの開発生産性が最も高いと実感し

    Flutterの採用を決断

    16

    半減


    View Slide

  17. 17


    View Slide

  18. 18


    View Slide

  19. 19

    ここから先の画面は

    全てFlutterで実装


    View Slide

  20. Flutterへの段階的移行

    ● Add-to-app(Add Flutter to existing app)を使用

    ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的
    に組み込む仕組み


    20

    じゃらん

    アプリ

    Swift

    Objective-c

    じゃらん遊び・体験


    Flutterプロジェクト


    View Slide

  21. Flutterへの段階的移行

    ● Add-to-app(Add Flutter to existing app)を使用

    ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的
    に組み込む仕組み


    21

    じゃらん

    アプリ

    Swift

    Objective-c

    じゃらん遊び・体験


    Flutterプロジェクト

    Flutterモジュール


    View Slide

  22. Flutterのレイアウト構築

    22


    View Slide

  23. Flutterのレイアウト構築

    ● Widget

    ○ UIの構成情報を保持するクラス


    ● Widgetをツリー上に構成することによって

    UIの構築を行う


    23


    View Slide

  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,
    ),
    ...

    View Slide

  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,
    ),
    ...

    View Slide

  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,
    ),
    ...

    View Slide

  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で表現する

    View Slide

  28. 直面した課題

    28


    View Slide

  29. 直面した課題

    29

    1. タブ切り替えのパフォーマンス

    2. Flutterの画面が初期化されない

    3. ネットワーク通信がproxyサーバーを経由しない

    4. Google Mapのクラッシュ


    View Slide

  30. 直面した課題

    30

    1. タブ切り替えのパフォーマンス

    2. Flutterの画面が初期化されない

    3. ネットワーク通信がproxyサーバーを経由しない

    4. Google Mapのクラッシュ


    View Slide

  31. ● タブを使用して表示する情報を

    切り替えるページが存在する


    ● このタブのページでは、

    口コミの一覧や、プランの一覧を

    リストで表示する

    タブの切り替えをする画面

    31


    View Slide

  32. 発生した問題

    ● リストのアイテムを大量に読み込んでタブを切り替える



    ● タブ切り替えのアニメーションが重くなってしまう

    ● まれにタブ切り替えのタイミングで

    アプリがクラッシュすることがある

    32


    View Slide

  33. class TabPage extends StatelessWidget {
    @override
    Widget 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


    View Slide

  34. class TabPage extends StatelessWidget {
    @override
    Widget 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


    View Slide

  35. class TabA extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Center(
    child: Text('Tab A'),
    );
    }
    }
    ● TabA()は画面の中心に
    “Tab A”を表示するだけ
    サンプルコード: TabA

    35


    View Slide

  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


    View Slide

  37. 37

    タブ切り替え

    TabA
 TabB


    View Slide

  38. 38

    TabB

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

    View Slide

  39. 39

    TabB
 TabA

    TabAに

    切り替え

    TabBに

    切り替え

    TabB


    View Slide

  40. 40

    TabB
 TabA

    TabAに

    切り替え

    TabBに

    切り替え

    TabB

    ● タブ切り替えのアニメーションが非
    常に重くなる
    ● まれにクラッシュする

    View Slide

  41. なぜアニメーション重くなる?

    ● タブを切り替えた際、表示

    されないタブはWidget

    ツリーから除外される

    41

    TabA表示時
 TabB表示時


    View Slide

  42. ● 再度タブを表示する際には、表示するタブの

    レイアウトを再計算する必要がある


    なぜアニメーション重くなる?

    ● タブを切り替えた際、表示

    されないタブはWidget

    ツリーから除外される

    42

    TabA表示時
 TabB表示時


    View Slide

  43. タブが保持するリストのアイテムの高さが可変の場合

    ● 1つ目のアイテムから順にレイアウト

    を計算して、高さを決定しないと


    なぜアニメーション重くなる?

    43

    …


    View Slide

  44. タブが保持するリストのアイテムの高さが可変の場合

    ● 1つ目のアイテムから順にレイアウト

    を計算して、高さを決定しないと


    ● 前回のスクロール位置の

    アイテムを表示できない



    なぜアニメーション重くなる?

    44

    …

    前回の

    スクロール位置


    View Slide

  45. タブが保持するリストのアイテムの高さが可変の場合

    ● 1つ目のアイテムから順にレイアウト

    を計算して、高さを決定しないと


    ● 前回のスクロール位置の

    アイテムを表示できない


    ● この演算のために

    パフォーマンスが低下

    なぜアニメーション重くなる?

    45

    …

    前回の

    スクロール位置


    View Slide

  46. なぜまれにクラッシュする?

    ● リストのレイアウトを決定する演算を行うことで、

    一時的にメモリを圧迫する

    ● この一時的な圧迫で許容値を超えてしまった場合に

    クラッシュが発生していた

    46

    タブ切り替え時


    View Slide

  47. どう回避したか

    ● タブのWidgetにAutomaticKeepAliveClientMixin

    を適用する

    ● 非表示になったタブもWidgetツリーから除外されなくなるた
    め、再度レイアウトの演算が不要

    47

    TabA表示時にTabBが除外されない 


    View Slide

  48. class TabB extends StatefulWidget {
    ...
    class _TabBState extends State 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


    View Slide

  49. class TabB extends StatefulWidget {
    ...
    class _TabBState extends State 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


    View Slide

  50. class TabB extends StatefulWidget {
    ...
    class _TabBState extends State 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を返す

    View Slide

  51. class TabB extends StatefulWidget {
    ...
    class _TabBState extends State 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

    ● 一時的なメモリ圧迫も
    起こらなくなる

    View Slide

  52. 直面した課題

    52

    1. タブ切り替えのパフォーマンス

    2. Flutterの画面が初期化されない

    3. ネットワーク通信がproxyサーバーを経由しない

    4. Google Mapのクラッシュ


    View Slide

  53. 53

    Nativeの画面
 Flutterの画面


    View Slide

  54. 発生した問題

    ● Flutterの画面を閉じて再度開く



    ● 前回開いた画面の状態が残ってしまっている

    54


    View Slide

  55. 55

    Nativeの画面
 Flutterの画面


    View Slide

  56. 56

    Nativeの画面

    • Flutterの画面に遷移する

    View Slide

  57. 57

    Flutterの画面

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

    View Slide

  58. 58

    Nativeの画面

    Flutterの画面

    dismiss


    View Slide

  59. 59

    Nativeの画面

    Flutterの画面

    present

    dismiss

    Flutterの画面


    View Slide

  60. 60

    Nativeの画面

    Flutterの画面

    present

    dismiss

    Flutterの画面

    ● 前回のFlutterの画面の
    状態が残ってしまっている
    ● 画面を破棄して再生成したら、初
    期状態になるのでは?

    View Slide

  61. 61

    じゃらんTOP


    present

    dismiss

    遊び・体験(Flutter)

    遊び・体験(Flutter)

    検索条件指定
 じゃらん遊び・体験予約

    を再度開く

    前回の検索条件のまま


    View Slide

  62. 62

    じゃらんTOP


    present

    dismiss

    遊び・体験(Flutter)

    遊び・体験(Flutter)

    検索条件指定
 遊び・体験を再度開く
 前回の検索条件のまま

    ● Add-to-appでFlutterの画面を表示する方法の説明

    ● この問題の原因の説明

    View Slide

  63. おさらい: Add-to-app

    ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的
    に組み込む仕組み


    63

    じゃらん

    アプリ

    Swift

    Objective-c

    じゃらん遊び・体験


    Flutterプロジェクト

    Flutterモジュール


    View Slide

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

    64

    FlutterEngine
    FlutterViewController
    FlutterView

    View Slide

  65. Flutter

    View

    Controller



    View

    Controller



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

    65

    FlutterEngine
    FlutterViewController
    FlutterView
    ● ViewControllerの派生クラス

    ● FlutterViewControllerに遷移する

    ことでFlutterの画面を表示

    画面遷移


    View Slide

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

    66

    FlutterEngine
    FlutterViewController
    FlutterView
    ● FlutterViewControllerに

    乗っているView

    ● FlutterモジュールのUIが描画される

    FlutterViewController

    FlutterView


    View Slide

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

    67

    FlutterEngine
    FlutterViewController
    FlutterView
    ● Dartを実行して、FlutterViewに

    FlutterモジュールのUIを描画する

    FlutterViewController

    FlutterView

    FlutterEngine


    View Slide

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

    68

    FlutterEngine
    FlutterViewController
    FlutterView
    ● Dartを実行して、FlutterViewに

    FlutterモジュールのUIを描画する

    FlutterViewController

    FlutterView

    FlutterEngine

    • Flutterの画面を描画するためには、
    FlutterEngineの初期化が必要

    View Slide

  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インスタンスの生成

    View Slide

  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の初期化は時間がかかるため、予め呼ぶ必要がある

    View Slide

  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の画面を表示

    View Slide

  72. 何故画面を再生成しても初期状態にならない?

    72

    FlutterEngine
    FlutterViewController
    ● Flutterの画面を閉じた段階で
    FlutterViewControlerは破棄される


    View Slide

  73. ● FlutterEngineはAppDelegateで初期化し、

    参照を保持しておくので、破棄されない

    何故画面を再生成しても初期状態にならない?

    73

    FlutterEngine
    class AppDelegate: FlutterAppDelegate {
    lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
    FlutterViewController

    View Slide

  74. ● FlutterEngineはAppDelegateで初期化し、

    参照を保持しておくので、破棄されない

    何故画面を再生成しても初期状態にならない?

    74

    FlutterEngine
    ● Dartを実行しているのはFlutterEngine

    ● Flutterの画面を閉じても、Dart内で破棄

    していないStateは残ってしまう

    FlutterViewController

    View Slide

  75. ● FlutterEngineはAppDelegateで初期化し、

    参照を保持しておくので、破棄されない

    何故画面を再生成しても初期状態にならない?

    75

    FlutterEngine
    ● Dartを実行しているのはFlutterEngine

    ● Flutterの画面を閉じても、Dart内で破棄

    していないStateは残ってしまう

    ● 時間がかかるためFlutterEngineを毎回初期化
    するわけにもいかない

    FlutterViewController

    View Slide

  76. ● Flutterモジュールの最初に空の画面を挿入(InitialPage)


    ● FlutterViewController遷移時に

    ○ InitialPage以外のページを全て破棄

    ○ 本来最初に表示したいページを生成して、

    即座に遷移する


    どう回避したか

    76


    View Slide

  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コードを呼び出す

    View Slide

  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へ画面遷移する

    View Slide

  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(
    context,
    TopPage.routeName,
    (Route route) => route.isFirst,
    );
    }
    79

    • 空のInitial pageを作成
    • Flutterモジュールの先頭の
    画面に設定

    View Slide

  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(
    context,
    TopPage.routeName,
    (Route route) => route.isFirst,
    );
    }
    80

    • setupのMethod Channelが
    呼び出された際に
    実行されるコード

    View Slide

  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(
    context,
    TopPage.routeName,
    (Route route) => route.isFirst,
    );
    }
    81


    View Slide

  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(
    context,
    TopPage.routeName,
    (Route route) => route.isFirst,
    );
    }
    82


    View Slide

  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(
    context,
    TopPage.routeName,
    (Route route) => route.isFirst,
    );
    }
    83


    View Slide

  84. 84

    Nativeの画面

    Flutterの画面

    present

    dismiss

    Flutterの画面

    ● 再度表示した際に、初期化される

    View Slide

  85. 直面した課題

    85

    1. タブ切り替えのパフォーマンス

    2. Flutterの画面が初期化されない

    3. ネットワーク通信がproxyサーバーを経由しない

    4. Google Mapのクラッシュ


    View Slide

  86. ● 詳細なデバッグや試験を行う際に

    パケットモニタリングを使用したい

    ● iOSプロジェクトにおいては

    Wi-Fi設定からproxyサーバーの

    IPアドレスとポート番号を入力する

    ことでパケットモニタリング可能

    (例: Charles)

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

    86


    View Slide

  87. ● 同様のWi-Fi設定をFlutterプロジェクトに行っても、通信が
    Proxyサーバーを経由せず

    パケットモニタリングを使用できない


    発生した問題

    87


    View Slide

  88. proxyサーバーを経由するためには

    88

    ● HttpClientクラスに

    プロキシ自動設定(PAC)を

    明示的に指定する必要がある


    View Slide

  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


    View Slide

  90. Future 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


    View Slide

  91. Future 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


    View Slide

  92. Future 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


    View Slide

  93. Future 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


    View Slide

  94. システムProxyからPACを指定する

    ● じゃらんではsystem_proxyパッケージを使用

    94

    pub system_proxy:https://pub.dev/packages/system_proxy, (参照2020-08-10)

    View Slide

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

    95


    View Slide

  96. 直面した課題

    96

    1. タブ切り替えのパフォーマンス

    2. Flutterの画面が初期化されない

    3. ネットワーク通信がproxyサーバーを経由しない

    4. Google Mapのクラッシュ


    View Slide

  97. Google Mapの使用

    ● レジャー施設の場所や集合場所を示
    すために、地図(Google Map)を表示す
    る

    ● google_maps_flutterパッケージを使用

    97

    pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)

    View Slide

  98. ● google_maps_flutterはDevelopers Preview

    ● 実際に使用すると、

    Google Mapを何度も表示した際に

    アプリがクラッシュする問題が発覚


    ● Google Mapを閉じてもメモリが解放

    されない

    ● 地図を開くたびにメモリを圧迫してしまい、

    最終的にクラッシュしてしまっていた

    発生した問題

    98

    Memory Usage


    View Slide

  99. 原因と問題の回避

    ● GoogleMapが内部で使用しているPlatformViewに

    おいて循環参照があり、それによってGoogleMapが

    解放されなくなっていた


    ● 当時使用していたFlutter ver1.12.13+hotfix.7から

    Flutter ver1.15.17にアップデートしたことで、

    メモリが解放される様になり、回避することができた



    99


    View Slide

  100. ライブラリのステータス

    ● developers preview等のライブラリや機能に関する

    既知の問題には、issueにタグが付与されている

    ● その様なライブラリや機能を使用する際には、

    タグでフィルタリングして、関連issueを確認することで

    事前に問題を把握すると吉

    100

    pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)

    View Slide

  101. 直面した課題まとめ

    ● Flutterを採用してみると、いくつかの課題に直面した

    ● GoogleMapがdevelopers previewである等、

    プラットフォームの未成熟な部分は若干ある?

    ● しかし、いずれの直面した課題も回避することは

    できていて、プロダクション採用不可能となる様な

    事態には直面しなかった

    101


    View Slide

  102. これらの課題を乗り越えた結果

    どんなメリットを得られたのか

    102


    View Slide

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

    103


    View Slide

  104. ● 開発効率は著しく向上した


    1. 既成部品の充実


    2. hot reload/restart


    3. IDE(Android Studio)の機能の充実


    得られたメリット:開発効率の向上

    104

    開発効率
    の向上

    View Slide

  105. 1. 既成部品の充実

    105

    ● Widgetの種類がとても充実

    している


    ● じゃらんにおいては、これら既成
    部品でほぼ事足りた

    ● 既成部品を積極的に使用できた
    ことが、開発効率向上に

    寄与した


    Widget catalog:https://flutter.dev/docs/development/ui/widgets, (参照2020-08-10)

    View Slide

  106. ● コードを修正した際に、ビルドし直さなくても

    その修正が即座にアプリに反映される仕組み

    ○ hot reload: 約0.5s

    ○ hot restart: 約3s 


    ● じゃらんはビルド時間が大分増加してしまって

    いたので、この仕組みの開発効率向上への

    寄与は大きかった

    2. hot reload/ restart

    106


    View Slide

  107. ● Widgetの上でoption+Enterを押すことで、包む
    Widget等の候補を表示。

    Widgetツリーの構築をサクサクできる


    ● “stless”や”stful”と打つことで

    Stateless WidgetやStateful Widget

    を自動生成

    3. IDE(Android Studio)の機能の充実

    107

    ● XCodeで開発する場合に比べて開発スピードが向上した


    View Slide



  108. 1. iOS/Androidの開発工数削減


    2. 開発以外の工数削減


    3. 移行工数の削減


    得られたメリット:工数の削減

    108

    工数の
    削減

    View Slide

  109. ● じゃらんはメディアであり、

    プラットフォーム固有の機能が少ない

    ● 完全移行が完了すれば、iOS/Androidの開発工数を

    ほぼ半分にすることができそう

    1. iOS/Androidの開発工数削減

    109


    View Slide

  110. ● iOSとAndroidの仕様差分をなるべく減らす

    ● デザインをマテリアルデザインに統一


    ● 開発以外の工数も削減することができている


    2. 開発以外の工数削減

    110

    開発工数
 要件検討工数
 デザイン作成工数

    5割減
 5割減
 3割減


    View Slide

  111. ● 段階的移行を行っていることにより、各プラットフォームの
    実装が多少必要になっている

    ○ 例えば、ネイティブ側が保持するアプリの設定情報を

    Flutterモジュールに伝播する処理


    ● しかし、大部分は共通化できていて、その点

    移行コストも大きく削減することができている

    3. 移行工数の削減

    111


    View Slide

  112. 開発効率向上、工数削減以外にも多くのメリット

    ● 宣言的UI構築が素晴らしい

    ○ 参考: 宣言的UI そな太さん
    https://speakerdeck.com/sonatard/xuan-yan-de-ui


    ● FlutterがOSSであることで、内部処理を確認できる


    ● パフォーマンスモニタリングが充実している


    ● 全てDartで記述するため、コードレビューや

    コンフリクトの解消がしやすい

    などなど...
 112


    View Slide

  113. まとめ

    113


    View Slide

  114. ● じゃらんでは現在Flutterへの段階的移行を行っている

    ● 複数課題に直面したものの、回避することはできた

    ● 直面した課題を乗り越えたことで、開発効率向上や開発

    工数削減など多くのメリットを得ることができた

    ● 完全移行に向けて引き続きFlutter頑張ります

    まとめ

    114


    View Slide

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

    115


    View Slide