Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Flutter移行の苦労と、乗り越えた先に得られたもの
Search
Recruit Technologies
October 01, 2020
Technology
3
11k
Flutter移行の苦労と、乗り越えた先に得られたもの
2020/9/20_iOSDC Japan 2020での、桐山の講演資料になります
Recruit Technologies
October 01, 2020
Tweet
Share
More Decks by Recruit Technologies
See All by Recruit Technologies
障害はチャンスだ! 障害を前向きに捉える
rtechkouhou
1
630
ここ数年間のタウンワークiOSアプリのエンジニアのチャレンジ
rtechkouhou
1
1.5k
大規模環境をAWS Transit Gatewayで設計/移行する前に考える3つのポイントと移行への挑戦
rtechkouhou
1
1.9k
【61期 新人BootCamp】TOC入門
rtechkouhou
3
41k
【RTC新人研修 】 TPS
rtechkouhou
1
41k
Android Boot Camp 2020
rtechkouhou
0
41k
HTML/CSS
rtechkouhou
10
50k
TypeScript Bootcamp 2020
rtechkouhou
9
45k
JavaScript Bootcamp 2020
rtechkouhou
1
43k
Other Decks in Technology
See All in Technology
Lambda10周年!Lambdaは何をもたらしたか
smt7174
2
110
Can We Measure Developer Productivity?
ewolff
1
150
Application Development WG Intro at AppDeveloperCon
salaboy
0
180
OCI Vault 概要
oracle4engineer
PRO
0
9.7k
dev 補講: プロダクトセキュリティ / Product security overview
wa6sn
1
2.3k
リンクアンドモチベーション ソフトウェアエンジニア向け紹介資料 / Introduction to Link and Motivation for Software Engineers
lmi
4
300k
Oracle Cloud Infrastructureデータベース・クラウド:各バージョンのサポート期間
oracle4engineer
PRO
28
12k
個人でもIAM Identity Centerを使おう!(アクセス管理編)
ryder472
3
180
障害対応指揮の意思決定と情報共有における価値観 / Waroom Meetup #2
arthur1
5
470
ドメイン名の終活について - JPAAWG 7th -
mikit
33
20k
RubyのWebアプリケーションを50倍速くする方法 / How to Make a Ruby Web Application 50 Times Faster
hogelog
3
940
20241120_JAWS_東京_ランチタイムLT#17_AWS認定全冠の先へ
tsumita
2
230
Featured
See All Featured
BBQ
matthewcrist
85
9.3k
How to Think Like a Performance Engineer
csswizardry
20
1.1k
KATA
mclloyd
29
14k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.9k
Building a Scalable Design System with Sketch
lauravandoore
459
33k
jQuery: Nuts, Bolts and Bling
dougneiner
61
7.5k
The Language of Interfaces
destraynor
154
24k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
38
1.8k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
229
52k
How GitHub (no longer) Works
holman
310
140k
Building a Modern Day E-commerce SEO Strategy
aleyda
38
6.9k
Making Projects Easy
brettharned
115
5.9k
Transcript
Flutter移行の苦労と、 乗り越えた先に得られたもの Recruit Co., Ltd. Keisuke Kiriyama 1
• 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 { @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 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 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 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
直面した課題 29 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google
Mapのクラッシュ
直面した課題 30 1. タブ切り替えのパフォーマンス 2. Flutterの画面が初期化されない 3. ネットワーク通信がproxyサーバーを経由しない 4. Google
Mapのクラッシュ
• タブを使用して表示する情報を 切り替えるページが存在する • このタブのページでは、 口コミの一覧や、プランの一覧を リストで表示する タブの切り替えをする画面 31
発生した問題 • リストのアイテムを大量に読み込んでタブを切り替える • タブ切り替えのアニメーションが重くなってしまう • まれにタブ切り替えのタイミングで アプリがクラッシュすることがある
32
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
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
class TabA extends StatelessWidget { @override Widget build(BuildContext context) {
return Center( child: Text('Tab A'), ); } } • TabA()は画面の中心に “Tab A”を表示するだけ サンプルコード: TabA 35
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 タブ切り替え 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<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
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
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を返す
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 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 FlutterEngine FlutterViewController FlutterView
Flutter View Controller View Controller Flutterの画面を表示するために重要なクラス
65 FlutterEngine FlutterViewController FlutterView • ViewControllerの派生クラス • FlutterViewControllerに遷移する ことでFlutterの画面を表示 画面遷移
Flutterの画面を表示するために重要なクラス 66 FlutterEngine FlutterViewController FlutterView • FlutterViewControllerに 乗っているView • FlutterモジュールのUIが描画される
FlutterViewController FlutterView
Flutterの画面を表示するために重要なクラス 67 FlutterEngine FlutterViewController FlutterView • Dartを実行して、FlutterViewに FlutterモジュールのUIを描画する FlutterViewController FlutterView
FlutterEngine
Flutterの画面を表示するために重要なクラス 68 FlutterEngine FlutterViewController FlutterView • 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, didFinishLaunchingWithOptions launchOptions: [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, 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の初期化は時間がかかるため、予め呼ぶ必要がある
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 FlutterEngine FlutterViewController • Flutterの画面を閉じた段階で FlutterViewControlerは破棄される
• FlutterEngineはAppDelegateで初期化し、 参照を保持しておくので、破棄されない 何故画面を再生成しても初期状態にならない? 73 FlutterEngine class 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).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コードを呼び出す
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へ画面遷移する
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モジュールの先頭の 画面に設定
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が 呼び出された際に 実行されるコード
• 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
• 条件 一番最初の画面であること。 すなわち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
• 条件を満たしたタイミングで本 来表示したかった 最初の画面が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 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<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
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
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
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
システムProxyからPACを指定する • じゃらんではsystem_proxyパッケージを使用 94 pub system_proxy:https://pub.dev/packages/system_proxy, (参照2020-08-10)
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 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