最近Flutterを初めて、Android(Kotlin)の書き方よりもずいぶん素早く開発できることに驚いたので、TODOアプリを通して比較してみました。
筆者はJavaもKotlinもFlutterも実務経験はなく、半年程度の経験しかありません。
なので対象読者としては今からスマホのアプリを開発してみたいけど、ネイティブ(Java/Kotlin,Objective-C/Swift)を勉強するかFlutterのようなクロスプラットフォーム開発を勉強しているか悩んでいる人を想定しています。
先にこの記事の主張を述べると、要するに以下のような理由で、FlutterのProviderパターン最高って話です。
- iOS/Android同時に開発できて嬉しい
- Kotlinのdata bindingのようなものがnotifyListener()だけでできる。直感的
- 特にリストのバインドが面倒(アダプターを作る必要がある)
作ったアプリはこんなものです。
Kotlinバージョン
GitHubリポジトリ: https://github.com/tokku5552/TODOAppSample-Kotlin #### Flutterバージョン GitHubリポジトリ: https://github.com/tokku5552/TODOAppSample-Flutterアーキテクチャ概要
5分くらいで書いたアーキテクチャメモをもとにざっくり説明します。
Kotlin - MVVM
MainActivityでは画面遷移のみを行い、2つのフラグメントでTodoリスト一覧と、詳細画面の表示を行っています。
それぞれのフラグメントはMainActivityViewModelを参照していて、画面遷移が伴うアクションの時はMainActivityViewModelを通して画面遷移します。
一覧のTodoItemをクリックすると、MainActivityのclickedItemが呼ばれて詳細画面に各値を入れた状態で表示させます。
もう少し詳しくこちらのブログで解説しています。
バージョン
- Realm 2.1.1
Flutter - Provider
main.dartで一覧を表示し、TodoItemDetailPageで詳細画面を表示しています。
本当はRealmを使いたかったのですが、FlutterではRealmのいい感じのパッケージが見つからなかったので、
しょうがなくSQLiteを使っています。
基本的に非同期で描画しておいて、MainModel(ViewModelにあたる)で値をとってきたり更新したりした後に、画面側へ値の更新をnotifyListenerで通知します。
こちらでもう少し詳しく解説しています。
https://tokku-engineer.tech/todoapp-provider-flutter/
パッケージのバージョン
- provider: 4.3.2+2
- sqflite: 1.3.2+1
- shared_preferences: 0.5.12+4
それでは、私がなぜFlutterのProviderパターンを推してるかを以下で説明します。
iOS/Android同時に開発できて嬉しい
いきなりそもそもの話になりますが、Flutterはクロスプラットフォーム開発のためのフレームワークなので、1つコードを書くだけで、AndroidとiOSのアプリを同時に作ることが出来ます。
Androidだと、画面を作る際にレイアウト用のxmlと、View(Activity or Fragment)を用意する必要がありますが、Flutterでは1つのdartファイルを書けばよいので非常に効率的です。
また、ホットリロード出来る点も、すぐに結果が確認できてスピードが上がった気がします。
KotlinのDataBindingのようなものがnotifyListener()だけでできる
Androidではdata bindingというjetpack1ライブラリを使って、ViewとViewModelの依存関係の方向を整理しています。
例えば詳細画面のコードを比べてみます。
//詳細画面の表示
private fun showTask(todoItem: TodoItem) {
binding.editTitle.setText(todoItem.title, TextView.BufferType.EDITABLE)
binding.editDetail.setText(todoItem.detail, TextView.BufferType.EDITABLE)
binding.editCreate.text = todoItem.createDate.toString("yyyy/MM/dd")
binding.createDate.isVisible = true
binding.editCreate.isVisible = true
if (todoItem.createDate != todoItem.updateDate) {
binding.editUpdate.text = todoItem.updateDate.toString("yyyy/MM/dd")
binding.editUpdate.isVisible = true
binding.update.isVisible = true
}
binding.buttonLeft.text = "更新"
binding.buttonLeft.setOnClickListener {
todoItemDetailFragmentViewModel.updateTask(
todoItem.id,
binding.editTitle.text.toString(),
binding.editDetail.text.toString()
)
closeFragment()
}
}
Kotlinの場合です。
上記のようにそれぞれのTextViewにbindしているのに加えて、
xmlファイルで以下のようにdatabindingのためのmodelの指定を記載する必要があります。
<data>
<import type="android.view.View" />
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewmodel"
type="tech.tokku_engineer.todoappsample_kotlin.viewmodels.TodoItemDetailFragmentViewModel" />
</data>
Flutterの場合はこんな感じになります。
body: Consumer<TodoItemDetailModel>(builder: (context, model, child) {
model.todoTitle = todoItem?.title;
model.todoBody = todoItem?.body;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: "タイトル",
hintText: "やること",
),
onChanged: (title) {
model.todoTitle = title;
},
controller: titleEditingController,
),
SizedBox(
height: 16,
),
TextField(
decoration: InputDecoration(
labelText: "詳細",
hintText: "やることの詳細",
),
onChanged: (body) {
model.todoBody = body;
},
controller: detailEditingController,
),
SizedBox(
height: 16,
),
RaisedButton(
child: Text(isUpdate ? "更新する" : "追加する"),
onPressed: () async {
try {
isUpdate
? await model.update(todoItem.id)
: await model.add();
Navigator.pop(context);
} catch (e) {
//なんかエラー処理
)
],
);
},
);
}
},
)
],
),
);
}),
私は個人的に、この括弧をいくつも改行して表示するスタイルはあまり好きではないですが、実際はコードのほとんどがレイアウト用のコードになっていて、行数だけがたくさんあるように見えて実はコード量は少ないです。
データを画面側から直接modelのフィールドに代入したり取得したりして、model側で処理したらnotifyListenerを呼んで変更を伝えます。
Future<void> update(int id) async {
await TodoItemRepository.updateTodoItem(
id: id,
title: (todoTitle.isEmpty) ? todoTitle = "" : todoTitle,
body: (todoBody.isEmpty) ? "" : todoBody,
);
notifyListeners();
}
上記の例はDBで更新をかけている例ですが、フィールドに値を代入した後、notifyListeners();とするだけでUI側に更新が通知されます。
非同期処理もasync:awaitで書けて、私にとってはわかりやすかったです。(KotlinもCoroutine使うとずいぶん直感的に書けますが・・・)
特にリストのバインドが面倒(アダプターを作る必要がある)
特に分かりやすさに差が出たのがリストの表示です。Kotlinの場合ListViewAdapterというクラスを作成して、ViewHolderを定義して、onCreateViewHolderをとonBindViewHolderをそれぞれオーバライドして生成⇒bindしてあげて・・・
date bindingしてる場合はexecutePendingBindings()とかを呼ばないとうまくバインドされなくて・・・
と、結構初学者殺しな感じがします。
ですが、Flutterだと、listをmapするだけです。
なんと直感的
body: Consumer<MainModel>(builder: (context, model, child) {
final todoList = model.list;
return ListView(
children: todoList
?.map(
(todo) => ListTile(
leading: Checkbox(
value: todo.isDone,
onChanged: (bool value) {
//なんか処理
},
),
title: //いい感じの文字
),
onTap: () {
pushWithReload(context, model, todoItem: todo);
},
),
)
?.toList() ??
[
ListTile(
title: Text(""),
)
],
);
}),
リストがnullだった時の処理を三項演算で書いてしまっているので少しわかりずらいですが、listをmodelに入れて、UI側は単にmapでしこしこウィジェットを表示していくだけで良いです。
もちろん状態が変わってもmodel側でnotifyListenersしてあげれば即座に更新されます。
まとめ
おそらく私の経験や知識が不足していることもあり、いろいろなことが考慮できていなかったり設計が甘かったりするのだと思いますが、それでもFlutterはかなり直感的に書けるという印象でした。
AndroidにしてもFlutterにしても(おそらくiOSも)このようなパターンが生まれるまでにGoogleやAppleの優秀な開発者たちの紆余曲折があったんだと思います。
ちなみに筆者はまだKotlinなんて登場していなくて、Eclipseを使って開発することが主流だった時代に一度Androidアプリの開発にチャレンジしたことがありますが、エミュレータはまともに動かないし、Javaで毎回findViewByIdしてidをとりまくらないといけないしで途中で挫折してしまいました・・・
これからもどんどん書き方が変わっていくだろうと思いますので、素早く優れたパターンを設計に取り入れられるかが、スマホアプリの開発において重要だと感じました。
以下の記事でhooks_riverpod+state_notifier+freezedでDDDを使って同じようにTodoアプリのサンプルを作って解説しています。よかったらご参考ください。
【Flutter】hooks_riverpod+state_notifier+freezedでのドメイン駆動設計 - Qiita
[参考]
- データ バインディング ライブラリ:(https://developer.android.com/topic/libraries/data-binding?hl=ja)
- DataBindingを使っていてexecutePendingBindingsを呼び出さないとどうなるか:(https://android.gcreate.jp/358/)
-
- FlutterのProviderパターンを3分で理解する:(https://tamappe.com/2020/06/09/2020-06-09-200000/)
-
Android Jetpack(https://developer.android.com/jetpack?hl=ja) ↩