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

FlutterEngineFlutterViewController
● 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


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