Slide 1

Slide 1 text

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


Slide 2

Slide 2 text

• Recruit Co., Ltd.
 • iOS / Flutter 
 • じゃらんアプリ開発T
 Keisuke Kiriyama


Slide 3

Slide 3 text

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


Slide 4

Slide 4 text

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


Slide 5

Slide 5 text

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


Slide 6

Slide 6 text

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)

Slide 7

Slide 7 text

国内におけるFlutterのプロダクション採用
 ● しかし、国内においてFlutterを
 プロダクションに採用している例はそれほど多くない
 ● 弊社においてもFlutterを採用したのはじゃらんが初
 
 7


Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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


Slide 10

Slide 10 text

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


Slide 11

Slide 11 text

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


Slide 12

Slide 12 text

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


Slide 13

Slide 13 text

1. 前提の共有
 ○ じゃらんのFlutter移行
 ○ Flutterのレイアウト構築
 
 2. 直面した課題
 
 3. 得られたメリット
 
 4. まとめ
 説明の流れ
 13


Slide 14

Slide 14 text

じゃらんのFlutter移行
 14


Slide 15

Slide 15 text

Flutter採用の背景
 ● じゃらんアプリはiOS/Android共にリリースから
 10年を迎え、長年に渡る開発が行われてきた
 
 
 
 ● 上記課題を解決するために、リプレースを検討
 15
 プロジェクトの大規模化によ るビルド時間の増加 プロジェクト全体の コードが古くなっている

Slide 16

Slide 16 text

じゃらんアプリのリプレース検討
 ● クロスプラットフォーム技術の検討
 ○ iOS/Android開発工数
 ○ リプレースコスト
 
 ● クロスプラットフォーム技術の中でも
 Flutterの開発生産性が最も高いと実感し
 Flutterの採用を決断
 16
 半減


Slide 17

Slide 17 text

17


Slide 18

Slide 18 text

18


Slide 19

Slide 19 text

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


Slide 20

Slide 20 text

Flutterへの段階的移行
 ● Add-to-app(Add Flutter to existing app)を使用
 ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み
 
 20
 じゃらん
 アプリ
 Swift
 Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト


Slide 21

Slide 21 text

Flutterへの段階的移行
 ● Add-to-app(Add Flutter to existing app)を使用
 ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み
 
 21
 じゃらん
 アプリ
 Swift
 Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト
 Flutterモジュール


Slide 22

Slide 22 text

Flutterのレイアウト構築
 22


Slide 23

Slide 23 text

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


Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

直面した課題
 28


Slide 29

Slide 29 text

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


Slide 30

Slide 30 text

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


Slide 31

Slide 31 text

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


Slide 32

Slide 32 text

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


Slide 33

Slide 33 text

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


Slide 34

Slide 34 text

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


Slide 35

Slide 35 text

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


Slide 36

Slide 36 text

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


Slide 37

Slide 37 text

37
 タブ切り替え
 TabA
 TabB


Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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


Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

なぜアニメーション重くなる?
 ● タブを切り替えた際、表示
 されないタブはWidget
 ツリーから除外される
 41
 TabA表示時
 TabB表示時


Slide 42

Slide 42 text

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


Slide 43

Slide 43 text

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


Slide 44

Slide 44 text

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


Slide 45

Slide 45 text

タブが保持するリストのアイテムの高さが可変の場合
 ● 1つ目のアイテムから順にレイアウト
 を計算して、高さを決定しないと
 
 ● 前回のスクロール位置の
 アイテムを表示できない
 
 ● この演算のために
 パフォーマンスが低下
 なぜアニメーション重くなる?
 45
 …
 前回の
 スクロール位置


Slide 46

Slide 46 text

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


Slide 47

Slide 47 text

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


Slide 48

Slide 48 text

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


Slide 49

Slide 49 text

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


Slide 50

Slide 50 text

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を返す

Slide 51

Slide 51 text

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
 ● 一時的なメモリ圧迫も 起こらなくなる

Slide 52

Slide 52 text

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


Slide 53

Slide 53 text

53
 Nativeの画面
 Flutterの画面


Slide 54

Slide 54 text

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


Slide 55

Slide 55 text

55
 Nativeの画面
 Flutterの画面


Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

58
 Nativeの画面
 Flutterの画面
 dismiss


Slide 59

Slide 59 text

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


Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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


Slide 62

Slide 62 text

62
 じゃらんTOP
 
 present
 dismiss
 遊び・体験(Flutter)
 遊び・体験(Flutter)
 検索条件指定
 遊び・体験を再度開く
 前回の検索条件のまま
 ● Add-to-appでFlutterの画面を表示する方法の説明 ↓ ● この問題の原因の説明

Slide 63

Slide 63 text

おさらい: Add-to-app
 ● 既存のネイティブプロジェクトにFluterプロジェクトを部分的 に組み込む仕組み
 
 63
 じゃらん
 アプリ
 Swift
 Objective-c
 じゃらん遊び・体験
 
 Flutterプロジェクト
 Flutterモジュール


Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Flutter
 View
 Controller
 
 
 View
 Controller
 
 
 Flutterの画面を表示するために重要なクラス
 65
 FlutterEngine FlutterViewController FlutterView ● ViewControllerの派生クラス
 ● FlutterViewControllerに遷移する
 ことでFlutterの画面を表示
 画面遷移


Slide 66

Slide 66 text

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


Slide 67

Slide 67 text

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


Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

何故画面を再生成しても初期状態にならない?
 72
 FlutterEngine FlutterViewController ● Flutterの画面を閉じた段階で FlutterViewControlerは破棄される


Slide 73

Slide 73 text

● FlutterEngineはAppDelegateで初期化し、
 参照を保持しておくので、破棄されない
 何故画面を再生成しても初期状態にならない?
 73
 FlutterEngine class AppDelegate: FlutterAppDelegate { lazy var flutterEngine = FlutterEngine(name: "my flutter engine") FlutterViewController

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

● FlutterEngineはAppDelegateで初期化し、
 参照を保持しておくので、破棄されない
 何故画面を再生成しても初期状態にならない?
 75
 FlutterEngine ● Dartを実行しているのはFlutterEngine
 ● Flutterの画面を閉じても、Dart内で破棄
 していないStateは残ってしまう
 ● 時間がかかるためFlutterEngineを毎回初期化 するわけにもいかない
 FlutterViewController

Slide 76

Slide 76 text

● Flutterモジュールの最初に空の画面を挿入(InitialPage)
 
 ● FlutterViewController遷移時に
 ○ InitialPage以外のページを全て破棄
 ○ 本来最初に表示したいページを生成して、
 即座に遷移する
 
 どう回避したか
 76


Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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モジュールの先頭の 画面に設定

Slide 80

Slide 80 text

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が 呼び出された際に 実行されるコード

Slide 81

Slide 81 text

• 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


Slide 82

Slide 82 text

● 条件 一番最初の画面であること。 すなわち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


Slide 83

Slide 83 text

● 条件を満たしたタイミングで本 来表示したかった 最初の画面が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


Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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


Slide 86

Slide 86 text

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


Slide 87

Slide 87 text

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


Slide 88

Slide 88 text

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


Slide 89

Slide 89 text

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


Slide 90

Slide 90 text

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


Slide 91

Slide 91 text

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


Slide 92

Slide 92 text

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


Slide 93

Slide 93 text

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


Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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


Slide 96

Slide 96 text

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


Slide 97

Slide 97 text

Google Mapの使用
 ● レジャー施設の場所や集合場所を示 すために、地図(Google Map)を表示す る
 ● google_maps_flutterパッケージを使用
 97
 pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)

Slide 98

Slide 98 text

● google_maps_flutterはDevelopers Preview
 ● 実際に使用すると、
 Google Mapを何度も表示した際に
 アプリがクラッシュする問題が発覚
 
 ● Google Mapを閉じてもメモリが解放
 されない
 ● 地図を開くたびにメモリを圧迫してしまい、
 最終的にクラッシュしてしまっていた
 発生した問題
 98
 Memory Usage


Slide 99

Slide 99 text

原因と問題の回避
 ● GoogleMapが内部で使用しているPlatformViewに
 おいて循環参照があり、それによってGoogleMapが
 解放されなくなっていた
 
 ● 当時使用していたFlutter ver1.12.13+hotfix.7から
 Flutter ver1.15.17にアップデートしたことで、
 メモリが解放される様になり、回避することができた
 
 
 99


Slide 100

Slide 100 text

ライブラリのステータス
 ● developers preview等のライブラリや機能に関する
 既知の問題には、issueにタグが付与されている
 ● その様なライブラリや機能を使用する際には、
 タグでフィルタリングして、関連issueを確認することで
 事前に問題を把握すると吉
 100
 pub google_maps_flutter:https://pub.dev/packages/google_maps_flutter, (参照2020-08-10)

Slide 101

Slide 101 text

直面した課題まとめ
 ● Flutterを採用してみると、いくつかの課題に直面した
 ● GoogleMapがdevelopers previewである等、
 プラットフォームの未成熟な部分は若干ある?
 ● しかし、いずれの直面した課題も回避することは
 できていて、プロダクション採用不可能となる様な
 事態には直面しなかった
 101


Slide 102

Slide 102 text

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


Slide 103

Slide 103 text

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


Slide 104

Slide 104 text

● 開発効率は著しく向上した
 
 1. 既成部品の充実
 
 2. hot reload/restart
 
 3. IDE(Android Studio)の機能の充実
 
 得られたメリット:開発効率の向上
 104
 開発効率 の向上

Slide 105

Slide 105 text

1. 既成部品の充実
 105
 ● Widgetの種類がとても充実
 している
 
 ● じゃらんにおいては、これら既成 部品でほぼ事足りた
 ● 既成部品を積極的に使用できた ことが、開発効率向上に
 寄与した
 
 Widget catalog:https://flutter.dev/docs/development/ui/widgets, (参照2020-08-10)

Slide 106

Slide 106 text

● コードを修正した際に、ビルドし直さなくても
 その修正が即座にアプリに反映される仕組み
 ○ hot reload: 約0.5s
 ○ hot restart: 約3s 
 
 ● じゃらんはビルド時間が大分増加してしまって
 いたので、この仕組みの開発効率向上への
 寄与は大きかった
 2. hot reload/ restart
 106


Slide 107

Slide 107 text

● Widgetの上でoption+Enterを押すことで、包む Widget等の候補を表示。
 Widgetツリーの構築をサクサクできる
 
 ● “stless”や”stful”と打つことで
 Stateless WidgetやStateful Widget
 を自動生成
 3. IDE(Android Studio)の機能の充実
 107
 ● XCodeで開発する場合に比べて開発スピードが向上した


Slide 108

Slide 108 text


 
 1. iOS/Androidの開発工数削減
 
 2. 開発以外の工数削減
 
 3. 移行工数の削減
 
 得られたメリット:工数の削減
 108
 工数の 削減

Slide 109

Slide 109 text

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


Slide 110

Slide 110 text

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


Slide 111

Slide 111 text

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


Slide 112

Slide 112 text

開発効率向上、工数削減以外にも多くのメリット
 ● 宣言的UI構築が素晴らしい
 ○ 参考: 宣言的UI そな太さん https://speakerdeck.com/sonatard/xuan-yan-de-ui
 
 ● FlutterがOSSであることで、内部処理を確認できる
 
 ● パフォーマンスモニタリングが充実している
 
 ● 全てDartで記述するため、コードレビューや
 コンフリクトの解消がしやすい
 などなど...
 112


Slide 113

Slide 113 text

まとめ
 113


Slide 114

Slide 114 text

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


Slide 115

Slide 115 text

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