Dart(Flutter) / Swift(iOS) / Kotlin(Android) 色々比較
dart/swift/kotlinでコードを書いてて、これswiftだったらどう書くのだっけという状態によくなるので、自分がよくそうなるところをメモする。
書いてたらDart(Flutter) / Swift(iOS) / Kotlin(Android) な比較にもなってしまっているのでその辺はすいません。
また、個人の経験での感覚で書いてしまっているので、もっと詳しい人はより正しい見解を述べられるのではとも思うので、へぇそういうふうに思ってるんだくらいに思って見てもらえると良いかもしれません🙏
nullだったらデフォルト値を指定
dart
final Item? item = null;
final name = item?.name ?? "default";
print(name);
kotlin
val item: Item? = null
val name = item?.name ?: "default"
println(name)
swift
let item: Item? = nil
let name = item?.name ?? "default"
print(name)
if ~ else ~ で変数を指定する
dart
final isHoge = true;
final name = isHoge ? "a" : "b";
// kotlinみたいに if else 形式では書けない
kotlin
val isHoge = true
val name = if (isHoge) {
"a"
} else {
"b"
}
swift
let isHoge = true
let name = isHoge ? "a" : "b"
// kotlinみたいに if else 形式では書けない
日時周り
dart
標準のDateTimeクラスが結構優れている様に感じます。
kotlin
標準のDate型, Calendar型で頑張るパターンもあるけど、どちらかというとライブラリを利用した方が良い気がします。 kotlinx-datetimeとかjoda-time などの日時系のライブラリを使うのがよくあるパターンだと思われる。
swift
標準のDate, Calendar型でExtensionして頑張ることが多い気がするが実際他のプロジェクトではどうなのかあまりわかってないです。
なんとなく、Kotlin(Android)よりも標準のDate, Calendarなどで頑張れる気がします。
エンティティやデータ用の構造体
dart
freezed 使う
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'item.freezed.dart';
class Item with _$Item {
const factory Item({required String name}) = _Item;
}
kotlin
data class Item(val name: String)
swift
struct Item {
let name: String
}
構造体のプロパティに複数の値をセット
dart
..
を使う
class Item {
Item();
String name = "";
String label = "";
}
void main() {
final item = Item()..name = "a"
..label = "b";
print(item.name);
print(item.label);
}
kotlin
with
を使う
data class Item(var name: String = "", var label: String = "")
fun main() {
var item = Item()
with(item) {
name = "a"
label = "b"
}
println(item)
}
swift
swiftは普通にやるしかない。少なくとも私はこれ系のswiftのシンタックスシュガーを知らない。
struct Item {
var name: String = ""
var label: String = ""
}
var item = Item()
item.name = "a"
item.label = "b"
print(item)
nullチェック、スマートキャスト
dart
スマートキャストできる
class Item {
Item(this.name);
String name = "";
}
void main() {
final Item? item = Item("a");
// finaなどの固定値に対して item == null や != nullで nullじゃないことが解れば、その後は
// ?などでアンラップしなくても良くなる
if (item == null) {
return;
}
print(item.name);
}
kotlin
dartと同じようにスマートキャストできる
data class Item(var name: String = "")
fun main() {
val item: Item? = Item(name = "a")
if (item == null) {
return
}
item.name = "a"
}
swift
スマートキャストはできない。その代わりswiftだけguard
文がある。
struct Item {
var name: String = ""
}
func smartCastExample() {
let item: Item? = Item(name: "a")
if item == nil {
return
}
// スマートキャストできないので ? が必要
print(item?.name)
}
func guardExample() {
let item: Item? = Item(name: "a")
guard let item = item else {
return
}
print(item.name)
}
guardExample()
smartCastExample()
List操作まわり
dart
標準でも色々できるが、
collection (多分dart公式ライブラリ) というpackageがあって便利。
それ使うと、 firstOrNull
や firstWhereOrNull
とかが使える様になる。
kotlin
標準でも色々できると思うが、このライブラリで色々できるとらしい(わたしはよく知らない)。
swift
ここに色々書いてある。基本的に標準で結構色々できるイメージ。
REST API Client
dart
dio が有名な気がします。
公式だと httpというパッケージが紹介されていました。
(どちらもわたしは使ったことがないです)
kotlin
retrofitがデファクトスタンダートだと思われる。
swift
alamofireがデファクトスタンダートだと思われる。
パッケージ管理
dart
pubspec一択だと思われる。
kotlin
gradle一択だと思われる。
実はgradleでもdependency lockingできるようになったらしい(結構前らしいが)。
swift
いっぱいある。
Swift Package Manager
Swift公式。今後の本命。
cocoapods
SPMが台頭してくる前までは、一番利用されていたと思われる。Flutterのプロジェクト作るとiOSのプロジェクトのところにはPodfileができていて、Flutterアプリはcocoapodsに依存していると思います。
cocoapodsで入れたライブラリはソースからビルドされるので、その分ビルドが長くなる問題はあってそれを解消するためにpre buildしたライブラリをリンクするという cocoapods-binary もあったらしいが、いつの間にか開発が止まっている様でした。
carthage
カッセージとかカルタゴとか呼ばれてて、ビルド済みのモジュールをリンクする感じだからcocoapodsよりもリンクするアプリ側のビルドが速くなるというので一時期結構流行ったと思います。
いまはそこまで流行ってはいないとは思うものの使っているプロジェクトはそこそこあるんじゃないかなと思います。
モック化
dart
dartは Implicit interfaces という思想があるので、interfaceみたいなのを定義せずに実体のクラスをインターフェースとして利用してモックする、というのが主流のようです(わたしはabstruct classでinterface定義してしまってますが)。riverpodのテストのRepositoryのモック化もそんな感じになっています。
mock化のライブラリはmockito とか mocktailなどありそうです(まだ使ったことはないです)。
ライブラリ使わなかったらこんな感じだと思います(riverpod testの説明からのコピペ...)。
class Todo {
Todo({
required this.id,
required this.label,
required this.completed,
});
final String id;
final String label;
final bool completed;
}
class Repository {
Future<List<Todo>> fetchTodos() async => [];
}
class FakeRepository implements Repository {
Future<List<Todo>> fetchTodos() async {
return [
Todo(id: '42', label: 'Hello world', completed: false),
];
}
}
kotlin
mock化のライブラリはmockk とか mockito-kotlinなどありそうです。
ライブラリ使わなかったらこんな感じだと思います。
data class Todo(val id: String, val label: String, val completed: Boolean)
interface Repository {
suspend fun fetchTodos(): List<Todo>
}
class FakeRepository: Repository {
override suspend fun fetchTodos(): List<Todo> = listOf(
Todo(id = "42", label = "Hello world", completed = false)
)
}
swift
mock化のライブラリはmockolo とか Cuckooなどありそうです。
ライブラリ使わなかったらこんな感じだと思います。
struct Todo {
let id: String
let label: String
let completed: Bool
}
protocol Repository {
func fetchTodos() async throws -> [Todo]
}
struct FakeRepository: Repository {
func fetchTodos() async throws -> [Todo] {
[
Todo(id: "42", label: "Hello world", completed: false)
]
}
}
DI周り
DIライブラリは、本来の目的である依存性注入以外の機能を有しているかどうかが広く普及するかどうかのポイントなんじゃないかとわたしは感じています。
実際、下記で紹介しているriverpodやDaggerはDI以外の機能を有していると思っています。
dart
いまは riverpodが人気だと思われる。
ちょっと前まではriverpod公式の説明にDIコンテナと状態管理を簡単にするライブラリという説明があったと思うのですが、今はパッと見、DIコンテナであることが書かれてなさそうで、今見たらリアクティブ・キャッシングとデータバインディングを実現するフレームワーク
と書かれていました。
riverpodはDI以外にアプリの状態管理をできて、むしろそれが主要な機能になっていると思います。
kotlin
いまは公式でも推奨しているDaggerやDagger Hiltがデファクトとなっていると思われる。koinとかも話題になったことはあったがいまどうなのかはよく知らない。
Dagger使うと、アプリのライフサイクルと関連づけできたり、シングルトン(androidでdagger使わないでstatic変数を使ったシングルトンパターンを適用すると、static変数が勝手に初期化されたりするので苦労します)パターンが簡単にできたりする。
swift
swift(iOS)に関しては、あまりデファクトスタンダートなDIライブラリを私は知らないです。
swinject とかneedleが存在していますが、デファクトスタンダートではないような気がします。
ライブラリ使わずにコンストラクタインジェクション(できない場合はフィールドインジェクションとか)とか、別のアプローチでFactory Patternとかが利用されていると思われます。
ローカルDB
dart
👇の拝見させていただいたりして、いろいろあってデファクトはよくわらないのですが、
個人開発しているFlutterアプリではisarを利用しています。
kotlin
多分もうRoomがデファクトスタンダートな気がします。
昔はRealmが流行りかけたと思うが昔はバグも結構あってiOSのようには流行らなかった印象がある。
swift
RealmかiOS標準のCoreDataなど。
おそらく、Realmが人気だと思われる。
アーキテクチャ
アーキテクチャはある程度そのときの流行り廃りがあると思うのですが、あまりそういうのに左右されずに判断していけるのが理想なのだろうなと思います。
実際は流行り廃りなどの影響を受けることはよくあるので、途中でアーキテクチャが変わってアプリ内に複数のアーキテクチャが存在するような状況によくなると思うのですが、それはそれでアリだと思いますし、許容/移行しつつアプリの開発保守していければいいと思います。
また、宣言的UI(Flutter, SwiftUI, Jetpack Compose)かどうかもアーキテクチャの選定に影響を与えると思います。
dart(Flutter)
基本的にはriverpodや provider, getxなどの状態管理パッケージを利用したアプリの設計が主流だと思います。
Flutterのアーキテクチャは、状態管理を何でやるかが設計に影響を与える部分がそれなりにあるんじゃないかと思われますが、いまはriverpodが人気があると思います。
個人開発しているFlutterアプリ では、
riverpodを利用したレイヤードアーキテクチャを採用しているのですが、本来ドメインレイヤーとするところでfirestoreを直接操作したりしているので、そのあたりのIFを
もっと抽象化した方がすっきりするかな、とアプリが複雑になってきて思い始めています。
最初だからレイヤー細かく区切らずというふうに思ってたが、長期で育てていくことを考えると、最初からUI, State, ドメイン、データレイヤーくらいにはレイヤーを区別しておいた方が良いのだろうなと思ってきてます。
👇は個人的にはとても良い記事だと思いました。
※色々書いておいて、わたしは会社でFlutterアプリを開発したことがないので、普通どうやっているのかはそんなによくはわかりません。
kotlin(Andorid)
公式で推奨しているアーキテクチャがあるし、Android Architecture Components(AAC)もあるので、
公式の推奨する通りにやっていくと良さそうに思います。
Jetpack Compose 用の設計も公式にあるようです。
swift(iOS)
Apple公式のMVC がそんなに流行ってなくて色々存在していると思います。
個人的には、UIKitなら VIPERなどのMVP + Clean Architecture、SwiftUIならThe Composable Architecture(TCA)みたいなstateを管理するようなアーキテクチャが良さそうな気がしますが、SwiftUIのアーキテクチャはもう少し自分でも検証してみたい。
VIPERは結構前の記事ですが👇のがわかりやすかったです。
昔、RxSwiftがとても流行っていた時は双方向バイディング(ViewModel -> View, View -> ViewModel)を利用したアーキテクチャが流行っていたと思います(こういう感じのバインディングです)。
これの View -> ViewModel方向のバインディングがrx.sentMessage(#selector(UIViewController.viewWillAppear(_:))).mapToVoid().asDriverOnErrorJustComplete()
という感じでviewのイベントをRxSwiftのObservableに変えていくのが結構面倒 + 過剰にRxSwift依存を高める様に感じているので、自分の個人開発しているiOSアプリでは、ViewModel -> View方向のバインディングのみかつRxSwiftを使わない形(代わりにクロージャーを利用)のMVVMにしています(これが正しいかはよくわからないですが個人開発なので)。
SDKのバージョン管理
dart(Flutter)
fvmや asdf など
fvm使うと 最初に fvm
って書かなければならなくなるので、 asdf を今後検討していきたいと👇の記事を見て思っています。
kotlin(Android)
kotlinもライブラリみたいな位置付けになっているので、gradleで管理する感じになっていると思います。
swift(iOS)
XcodeとiOS SDKやSwiftのバージョンがセットになっていると思います。
参考
さいごに
間違ってるところなど、こういうのあるよなどあればコメントください🙏
Discussion