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

Flutter はプロダクション開発に耐えうるのか / Flutter ready for...

Flutter はプロダクション開発に耐えうるのか / Flutter ready for production?

Daichi Furiya (Wasabeef)

October 17, 2020
Tweet

More Decks by Daichi Furiya (Wasabeef)

Other Decks in Programming

Transcript

  1. 現在 〜 Android アプリ開発の成熟期 Activity/Fragment Repository ViewModel LiveData Local Source

    Room/DataStore Remote Data Source Retrofit/okHttp Jetpack Jetpack Jetpack Dagger Hilt Android Studio ここでいう成熟のイメージは アーキテクチャ論争、パフォー マンス改善手法確立がひと段落 し、それらの知見が一般化した 状態であり、アプリ開発(コー ド)の簡略化・テンプレート化 に向かっていることとする。
  2. Jetpack Compose などの宣言的な UI の手法を採 用したことによるパラダイムシフトと、それによる 今後の影響は…? MVVM から Flux/Redux/MVU

    のような状態管理 への移行もあるかもしれない。 宣言的 UI による VIEW のパラダイムシフト
  3. • アプリ動作の安定性 (クロスプラットフォーム <<= ネイティブ) • 将来性、先行き不安 • iOS/Android で共通の

    UI(悪くも) • iOS/Android の新機能への追従の遅れ? • Flutter と Kotlin MPP のどちらを採用するか? 課題
  4. • 世界的に200万人以上のエンジニアが Flutter を使って いて毎月10%増加している • 利用数上位5地域は インド、中国、米国、EU、ブラジル • Google

    Play に既に100,000個、毎月10,000個以上の Flutter アプリがアップされている 事例 https://medium.com/flutter/flutter-spring-2020-update-f723d898d7af
  5. iOS/Android の新機能への追従の遅れ? 例えば最近の iOS 14、Android 11 への対応は Flutter 1.22.0 でも改善され

    ていますし、致命的には対応が遅れてないという個人的な感覚はあるが、各 OS の新 API のインターフェース公開が後追いで来るのは事実です。
  6. エンジニアのリソース節約 サーバエンジニア iOS エンジニア Web エンジニア Android エンジニア 一般的な Swift/Kotlin

    アプリのアサインスタイル それぞれ一人の場合、 コードレビューされない ケースも...
  7. • Google によって開発 • 2018 年 12 月リリース (BREAKING CHANGES

    はありえる) • Dart • ステートフルホットリロード (高速開発) • UI ツールキット(豊富なウィジェット) Flutter
  8. • グラフィックライブラリ • Android, Chrome, Firefox, Blink, Flutter で採用されている •

    Skia は Vulkan をバックエンドエンジンとして変更可能 • Android 9 以降、デフォルトのレンダラーは Skia になってい る(8 と 9 でUIが変わったのはそのため)。Flutter にする とレイヤーが増えるもののレンダラーは同じ Skia
  9. 対応プラットフォーム • Windows 7, 8, 8.1, 10 • macOS 10.10.5

    or later • iOS 8 or later • Android 4.1 (JellyBean) or later • Ubuntu 14.04+, Debian 8+, openSUSE 13.3+, or Fedora Linux 24+ もちろん iOS でも Windows でも動作する Skia
  10. • Swift/Kotlin で良い体験をしてしまったエンジ ニアには Dart の言語実装が足りない。 (Data class, Nested Class,

    Enum Custom Value, Switch expression, protected, alias..) • アーキテクチャの議論が活発(良くも悪くも) • 各 SaaS の SDK が無い場合がある Flutter の難点
  11. Flutter だけだと解決できない例 • 各 SaaS や広告計測系などの Flutter SDK 対応が間に合ってない 場合は自分でラッパー相当を実装する必要があり、その時は

    Kotlin/Swift の知識が必要になる。 • どうしても Flutter からだとコールできない OS 側の機能を使い たい時には PlatformView を使い Flutter アプリ内のウィジェッ トの一部だけをネイティブにすることは出来るが、DRM などの 一部制限があるようなものではそれも出来ない。 例
  12. Flutter だけだと解決できない例 例えばこのアプリが Flutter で作ら れてるとして、動画部分に DRM で HW Secure

    Decoder を使っている ような場合には PlatformViewを 使ったとしても Flutter から呼び出 すのは出来ない。
  13. Flutter <-->- iOS/Android Flutter から MethodChannel API 経由で iOS/Android 側で

    実装された画面をまるごと表示 することが出来るデザインだと 解決が可能になる
  14. Flutter <-->- iOS/Android Flutter (Dart code) Android (Kotlin code) iOS

    (Swift code) Activity を起動 ViewController を起動
  15. iOS (arm32, arm64, simulator x86_64) macOS (x86_64) Android (arm32, arm64)

    Windows (mingw x86_64, x86) Kotlin/Native - Target Platforms Linux (x86_64, arm32, MIPS, MIPS LE, Raspberry Pi) WebAssembly (x86_64)
  16. Flutter か? Kotlin MPP か? • ゼロから作る新規プロジェクト? • 既存プロジェクトのリアーキテクチャ?リプレイス? •

    アプリ開発に当てれるエンジニアの人数は? • エンジニアの技術レベル感は?
  17. Flutter か? Kotlin MPP か? • ゼロから作る新規プロジェクト? • 既存プロジェクトのリアーキテクチャ?リプレイス? •

    アプリ開発に当てれるエンジニアの人数は? • エンジニアの技術レベル感は? • 安定的な技術志向か?リスクをとった挑戦志向か?
  18. Flutter か? Kotlin MPP か? • ゼロから作る新規プロジェクト? • 既存プロジェクトのリアーキテクチャ?リプレイス? •

    アプリ開発に当てれるエンジニアの人数は? • エンジニアの技術レベル感は? • 安定的な技術志向か?リスクをとった挑戦志向か? • プロジェクト・プロダクトのスケールを強く意識するか?
  19. Flutter か? Kotlin MPP か? • ゼロから作る新規プロジェクト? • 既存プロジェクトのリアーキテクチャ?リプレイス? •

    アプリ開発に当てれるエンジニアの人数は? • エンジニアの技術レベル感は? • 安定的な技術志向か?リスクをとった挑戦志向か? • プロジェクト・プロダクトのスケールを強く意識するか? • とにかく早くリリースしたい?
  20. Flutter か? Kotlin MPP か? • ゼロから作る新規プロジェクト? • 既存プロジェクトのリアーキテクチャ?リプレイス? •

    アプリ開発に当てれるエンジニアの人数は? • エンジニアの技術レベル感は? • 安定的な技術志向か?リスクをとった挑戦志向か? • プロジェクト・プロダクトのスケールを強く意識するか? • とにかく早くリリースしたい?
  21. Flutter か? Kotlin MPP か?  Flutter   Kotlin MPP  • 新規プロジェクト

    • 少人数(1〜4人) • 少人数でも両プラットフォーム でブランディングの一貫性を維 持したい • とにかく早くリリースしたい • よりチャレンジしたい • 新規/既存プロジェクト • 大人数(10人〜) • iOS/Androidアプリ開発の精 通している • リスクを抑えたい • 各プラットフォームで分けて デザインを拘りたい 例 例
  22. MVVM + Repository View Repository ViewModel Local Source Remote Data

    Source このスライドでは Android エンジニアの馴 染みが一番あるであろう MVVM + Repository を 例として取り上げていこ うと思います。 MVVM Repository
  23. MVVM UI の実装において、例えばテキストを入力し、バリーデーション、データを保持し、ボタン をタップして、サーバに送信するようなコードを View に全て追加していくと UI が複雑に なっていった時には更にコードが肥大化してしまい、それは UI

    とプレゼンテーションロジッ クとビジネスロジックの密結合になっているのでメンテナンスが大変ですし、テストを書く が困難に思えます。 そこで必要になってくる概念が関心の分離 (SoC) です。 簡単に説明す ると全てのアーキテクチャ共通して言えることですが、何をさせたいのか?その役割によっ て分離した構成要素とすることです。
  24. MVVM MVVM は Model–View–ViewModel のことを指し、その構成要素の基本形は以下となっています。 • View は UI (Widget)

    を描画(出力)し、ユーザからの入力データを受け取ります。 • ViewModel は View から入力された状態(データ)を適切に変換して Model として持ちます。 また、Model の状態(データ)を View 渡して画面の更新を促します。 • Model は状態(データ)を保持し、それがどう変換されて画面に描画されるかは知りません。
  25. Repository Repository Local Source Remote Data Source Repository pattern はデータソース

    へのアクセスを抽象化するためのデ ザインパターンです。ViewModel が 持つことになりますが、ViewModel 側からすると Repository とデータ の形式だけを取り決めるだけで、入 手先がサーバからなのか、ローカル の DB なのか、オンメモリなのかな どは知りません。
  26. MVVM + Repository View Repository ViewModel Local Source Remote Data

    Source Android で現在一般的な アーキテクチャは MVVM に Repository Pattern を合わせた設計 となっています。 MVVM Repository 
  27. DI/Service Locator View Repository ViewModel Local Source Remote Data Source

    DI/SL クラス間の密結合を減らして、 再利用性を高めるための手法。 すごくポピュラーな例をいうと テスト書きたいので、テスト実 行時はサーバ API を叩くので はなくて、モックデータを読み 込むような処理に変えたいがた めに使います。 (テスタビリティの向上)
  28. 主な DI/Service Locator のパッケージ  InheritedWidget   Riverpod package  • Flutter SDK

    に内包 • BuildContext が必要 • 子ウィジェットからでも O(1) でオブジェクトが取得できる • 少しローレベルなクラス • 有名な方が個人開発 • 新しい Provider でシンプル • まだ安定版ではない • 今や Dagger みたいな位置付 • コンパイルセーフ • Flutter に依存せずにも使える https://github.com/rrousselGit/river_pod https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
  29. Riverpod https://github.com/rrousselGit/river_pod final myNotifierProvider = ChangeNotifierProvider((_) { return MyNotifier(); });

    class MyNotifier extends ChangeNotifier { int count; /// TODO: typical ChangeNotifier logic } この例では ChangeNotifier を継承した MyNotifier を Riverpod に管理しても らいます。Riverpod には ChangerNotifierProvider というものが用意されてい るので、そこで MyNotifier のインスタンスを返却します。
  30. Riverpod https://github.com/rrousselGit/river_pod class MainPage extends ConsumerWidget { @override Widget build(BuildContext

    context, ScopedReader watch) { final count = watch(myNotifierProvider); return Text(count.toString()); } } Riverpod の ConsumerWidget を継承したウィジェット内であれば、シンプル に値の監視をすることができます。MyNotifier の値が変わっていれば、この build が再実行されます。
  31. State management View Repository ViewModel Local Source Remote Data Source

    DI 主に View とViewModel 間 で状態の共有を行うことが 目的となっています。View 側は状態が変わったことを 知りたいので ViewModel から通知をもらうようなイ メージになります。
  32. State management そのウィジェットツリーを表現するとこ のようになっており、それぞれクラスが 分かれていたとします。 [ADD] ボタン自体は MyListItem クラス が制御を管理しているので、どうやって

    MyCart クラスに通知するかを考えないと いけません。 Observable に該当する仕組みが必要で す。 https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
  33. State management  ChangeNotifier   StateNotifier package  • Flutter SDK に内包 •

    状態を複数管理できる • 割と自由にできる • その状態は Mutable • 状態の変更を通知できる • Riverpod に合わせて使える • 有名な方が個人開発 • ValueNotifier の進化版 • 状態(state)を一つだけ管理 • その状態は Immutable • 状態の変更を通知できる • Freezed で copyWith を自動 生成してもらうと良い • Riverpod に合わせて使える Android の LiveData のような役割
  34. ChangeNotifier を継承した ViewModel final homeViewModelNotifierProvider = ChangeNotifierProvider( (ref) =>= HomeViewModel(repository:

    ref.read(newsRepositoryProvider))); class HomeViewModel extends ChangeNotifier { HomeViewModel({@required NewsRepository repository}) : _repository = repository; final NewsRepository _repository; News _news; News get news =>= _news; Future<void> fetchNews() async { _news = _repository.getNews(); notifyListeners() } } 全体的に少しシンプルにしてありますが、HomeViewModel は ChangeNotifier を継承します。
  35. ChangeNotifier を継承した ViewModel final homeViewModelNotifierProvider = ChangeNotifierProvider( (ref) =>= HomeViewModel(repository:

    ref.read(newsRepositoryProvider))); class HomeViewModel extends ChangeNotifier { HomeViewModel({@required NewsRepository repository}) : _repository = repository; final NewsRepository _repository; News _news; News get news =>= _news; Future<void> fetchNews() async { _news = _repository.getNews(); notifyListeners() } } StateNotifier の場合は管理対象の状態は state を実装が必要になりますが、ChangeNotifier は特に何も。
  36. ChangeNotifier を継承した ViewModel final homeViewModelNotifierProvider = ChangeNotifierProvider( (ref) =>= HomeViewModel(repository:

    ref.read(newsRepositoryProvider))); class HomeViewModel extends ChangeNotifier { HomeViewModel({@required NewsRepository repository}) : _repository = repository; final NewsRepository _repository; News _news; News get news =>= _news; Future<void> fetchNews() async { _news = _repository.getNews(); notifyListeners() } } Repository で取得したデータを格納しただけでは、リスナーに通知はされません。
  37. ChangeNotifier を継承した ViewModel final homeViewModelNotifierProvider = ChangeNotifierProvider( (ref) =>= HomeViewModel(repository:

    ref.read(newsRepositoryProvider))); class HomeViewModel extends ChangeNotifier { HomeViewModel({@required NewsRepository repository}) : _repository = repository; final NewsRepository _repository; News _news; News get news =>= _news; Future<void> fetchNews() async { _news = _repository.getNews(); notifyListeners() } } notifyListeners() をコールすることで、通知がいきます。
  38. ChangeNotifier を継承した ViewModel class MainPage extends ConsumerWidget { @override Widget

    build(BuildContext context, ScopedReader watch) { final news = watch(homeViewModelNotifierProvider); return Text(news.title); } } 先ほどの Riverpod の説明のところで紹介したものと同じです。
  39. Networking View Repository ViewModel Local Source Remote Data Source DI

    主に Remote Data Source でサーバの API  を叩くために Endpoint, Header, Token, Body の制 御を行います。
  40. 主な Http client のパッケージ  http package   dio package  • 公式に

    Google が開発 • メンテは活発 • シンプルな Http client で単純 なことをやるならこれでも十分 • Flutter China コミュニティ が開発(主に一人) • メンテが若干不安 • 機能が豊富で Cancel, Cache や Cookie の制御や retrofit like なジェネレータもある • で人気がある https://github.com/flutterchina/dio https://github.com/dart-lang/http
  41. dio + Cache https://github.com/flutterchina/dio # data_source.dart import 'package:dio/dio.dart'; @override Future<News>

    getNews() async { return _dio.get<Map<String, dynamic>>>( '/v1/wasabeef', queryParameters: <String, String>{ 'apiKey': 'VEhJUyBJUyBBUElLRVk', }, options: buildCacheOptions(const Duration(hours: 1)), ) .then((response) =>= News.fromJson(response.data)); } # pubspec.yaml dependencies: dio: ^3.x.x dio_http_cache: ^0.2.x 何かしら他の分野でも Http client を使ったこと ある人であれば理解は難し くないと思います。
  42. Serializable/Deserialize View Repository ViewModel Local Source Remote Data Source DI

    特にサーバ API 通信で使 われる送受信したデータを シリアライズとデシリアラ イズ。
  43. 主な Serializable/Deserialize のパッケージ  json_serializable package   Freezed package  • 公式に Google

    が開発 • アノテーションを元に自動生成 • to/from JSON • 有名な方が個人開発 • json_serializable のサポートし ている • アノテーションを元に自動生成 • toString, hashCode.. • copyWith • Immutable https://github.com/rrousselGit/freezed https://github.com/google/json_serializable.dart
  44. Freezed part 'article.freezed.dart'; part 'article.g.dart'; @freezed abstract class Article with

    _$Article { factory Article({ @required Source source, String author, @required String title, @required String description, @required String url, String urlToImage, @required DateTime publishedAt, String content, }) = _Article; factory Article.fromJson(Map<String, dynamic> json) =>= _$ArticleFromJson(json); } この例では、@freezed アノ テーションをつけて、 _$Article (自動生成されるク ラス)の mixin を適用していま す。 Freezed は json_serializable にも対応しているので、 _$ArticleFromJson のイン ターフェースも生成されていま す。 https://github.com/rrousselGit/freezed
  45. Assets/Fonts View Repository ViewModel Local Source Remote Data Source DI

    アプリ内で画像やフォ ントなどのファイルを 使いたい場合の設定を 説明していきます。
  46. Assets - ローカル画像ファイルへのアクセス # pubspec.yaml flutter: assets: - assets/images/ -

    assets/images/icons/ pubspec.yaml に画像ファイルを置いたところのパスを assets: に指定します。 ディレクトリを指定した場合は、そのディレクトリ内は全て精査されますが、サ ブディレクトリ内までを再帰的には精査しません。
  47. Android (AAPT) の R.java に該当するもの が Flutter の世界では公式にはなく、画像な どのリソースへのアクセスはファイルパスを 文字列で

    ”assets/images/profile.jpg” で指定 しないといけません。 Assets - ローカル画像ファイルへのアクセス
  48. Assets - ローカル画像ファイルへのアクセス /// main.dart Widget build(BuildContext context) { return

    Image.asset(‘assets/images/icons/profile.jpg'); } 例えば、Image class にローカルの画像を指定したい場合には以下のようにパス を指定します。
  49. Assets - ローカル画像ファイルへのアクセス /// main.dart Widget build(BuildContext context) { return

    Image.asset(‘assets/images/icons/profile.jpg'); } ただ、文字列で指定するのはとてもタイプセーフだと言えず R.java って良くで きた仕組みだなぁって感じることとなります。。
  50. Fonts - ローカルフォントへのアクセス # pubspec.yaml flutter: fonts: - family: RobotoMono

    fonts: - asset: assets/fonts/RobotoMono-Regular.ttf - asset: assets/fonts/RobotoMono-Bold.ttf weight: 700 好きな Fonts を使いたい場合にも pubspec.yaml に指定が必要となります
  51. Fonts - ローカルフォントへのアクセス /// main.dart Widget build(BuildContext context) { return

    Text( 'Hi there', style: TextStyle( fontFamily: 'RobotoMono', ), } フォントもファミリー名を文字列で指定します。
  52. もしタイポしていた場合 拡張子が間違っていたなどのタイポ(❌ jpeg、⭕ jpg)でも ランタイムエラーになるので、気をつけないといけません。 The following assertion was thrown

    resolving an image codec: Unable to load asset: assets/images/profile.jpeg /// main.dart Widget build(BuildContext context) { return Image.asset(‘assets/images/icons/profile.jpeg'); }
  53. FlutterGen /// main.dart Widget build(BuildContext context) { return Image.asset(‘assets/images/icons/profile.jpg'); }

    FlutterGen を使えば、コマンド一つでタイプセーフにクラスを自動生成します プロモーションを含みます /// main.dart Widget build(BuildContext context) { return Assets.images.profile.image(); }
  54. Mockito /// Real class class Dog { String sound() =>=

    "Woof"; } /// Mock class class MockCat extends Mock implements Cat {} test('Dog Sound Test', () async { var dog = MockDog(); dog.sound(); verify(dog.sound()); when(dog.sound()).thenReturn("Grrrr"); expect(dog.sound(), "Grrrr"); }); https://github.com/dart-lang/mockito Android アプリ開発 でもよく使われるモッ ク化ライブラリの Mockito の Dart 版 が Google 公式とし て公開されているの で、最初はこのパッ ケージを導入してみる ことをお勧めします。
  55. Effective Dart style # main.dart library peg_parser.source_scanner; import 'file_system.dart'; import

    'slider_menu.dart'; # main.dart library pegparser.SourceScanner; import 'file-system.dart'; import 'SliderMenu.dart'; 例:Lower snake case でディレクトリやファイル名をつけること。
  56.  effective_dart style  Effective Dart に準拠 した Lint ルール Linter, Analyzer

    設定  pedantic style  Google 内部で採用され ている Lint ルール  flutter style  Flutter analyze コマン ドで適用されている Lint ルール https://dart-lang.github.io/linter/lints/index.html