宣言的UIでモバイルアプリを作るときの選択肢(SwiftUI, Jetpack Compose, React Native, Flutter) ※2020年12月
はじめに
Web開発では、ReactのようなコードでUIを記述していく「宣言的なシンタックスによるUI開発」が流行りになり大分安定した地位を確立してきた印象がありますが、AndroidやiOSのモバイルアプリ開発においてはまだまだ昔からのレイアウトファイルによる開発をしているケースも多いかと思います。
僕自身、新しくアプリを開発するときは積極的に宣言的UIでの開発をすることが多いのですが「結局何がよいんだろうか?」という疑問があり一度整理使しようと思い記事にしてみました。
※ この記事では僕が視野に入れているもの(または実際に触ったことがあるもの)をピックアップしているため、実際はその他の選択肢も存在すると思います。
※ 僕の感想や好みを主な内容としており、パフォーマンス面など客観的な優劣を比較した物ではありません。
選択肢
1.SwiftUI
2.Jetpack Compose ※
3.React Native(Expo)
4.Flutter
※ 2020年12月時点ではアルファ版です
モバイルアプリを宣言的UIで開発するときの、主な選択肢はこの4つになるかと思います。(Jetpack Composeについては、実際に製品として運用するのはもう少し先になるかと思いますが。。)
最近の開発ではモバイルアプリアプリの開発を検討するときに、iOSとAndroidのアプリをそれぞれネイティブの開発環境で開発するかまたは、一つのコードでマルチプラットフォーム環境として開発するかという選択をする場合が増えているように感じます。
どちらを選択するか?についてのメリット/デメリットについては開発リソースのスキルセットやアプリの機能にも影響するためこの記事では言及しませんが、それぞれネイティブ環境で開発する場合は、「1.SwiftUI」「2.Jetpack Compose」のいずれかを選択し、マルチプラットフォームで開発する場合は「3.React Native(Expo)」「4.Flutter」のどちらかを選択することになります。
僕は、Swift、React Native(非Expo)、Expo、Flutterでアプリの開発をしたことがありますが、Jetpack Composeについてはアルファ版ということもありチュートリアルを読んだ程度で開発をしたことはありません。
SwiftUI
SwiftUIは2019年にAppleが発表したUIフレームワークです。
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Image(systemName: "pencil")
.font(.system(size: 60))
Text("テキスト")
.padding(.init(top: 20, leading: 0, bottom: 0, trailing: 0))
}
.navigationTitle("SwiftUI サンプル")
}
}
}
コードの雰囲気としてはこんな感じになっています。
SwiftUI以前からSwiftを書いている人からするとSwift構文として奇妙な感じもするのですが、SwiftUIのために追加されたといっても functionBuilderなど様々な機能によって {} の中にUIを重ねていく表現などDSLっぽい書き方ができます。
書き味の特徴としては、
・ すべてのコンポーネントは Viewプロトコル を実装していて「body」というプロパティの中でレンダリングされるUIを記述する。
・ Viewの変更要素(上記のfontやpaddingなど)はModifierと呼ばれメソッドチェインとしてコンポーネントにつなげて記述する。
などがあげられます。
SwiftUIの良いところ
個人的に感じたSwiftUIの良いところは次のとおりです。
① プレビュー機能が強力
SwiftUIには、UIのライブプレビュー機能がついていて、アプリを起動しなくてもその場でUIの変更をリアルタイムで確認しながら反映して開発することができます。(コードを保存した瞬間に即時反映されます)
そのため、いちいちビルドを待って実機はシミュレーターを起動して見た目を確認する必要がありません。
② ネイティブの機能を直接使える
これは、後述するマルチプラットフォーム環境開発のデメリットに対する良いところになりますが、SiwftUIはAppleが直接開発しているためiOSの機能を直接使えることはメリットの一つです。
SwiftUIの良くないところ
対して、良くないところは次のとおりです。
① 機能が足りていない
フレームワークが成熟していないという理由に尽きるといえばそれまでなのですが、基本的なUIコンポーネントがまだ足りていない印象です。
例えば、アプリでは比較的よく使われるWebページを表示するためのWebViewもSwiftUIのコンポーネントとしては用意されていません。そのためUIKit(今までのiOSのUIフレームワーク)をSwiftUIから呼び出すというブリッジのコードを書く必要があります。
また、現時点では画面遷移なども基本はUIKitを元に実装されている節があるため、UIKitの仕組みを知らないエンジニアがゼロから触ると理解しにくい挙動をすることもしばしばあります。
宣言的UIの機能としても、iOS14になるまでObjectのStateが管理できなかったりと、まだまだ足りていない機能が多そうな印象です。
(個人的には、iOS14からだいぶ拡充したイメージがあり、iOS14で足切れるならありかなという印象)
② Modifierの可読性が悪い
①と比べれると個人的な好みの話になりますが、Viewコンポーネントを変更するためのModifierが多くつながると可読性が下がる印象があります。
VStack {
Image(systemName: "pencil")
.font(.system(size: 60))
Text("タイトル")
.font(.system(size: 30))
.fontWeight(.bold)
.foregroundColor(.red)
.padding(.init(top: 20, leading: 0, bottom: 0, trailing: 0))
Text("詳細")
.font(.system(size: 16))
.foregroundColor(.black)
.padding(.init(top: 16, leading: 0, bottom: 0, trailing: 0))
}
例えば、色々なサイズや文字色のテキストを並べたり、各コンポーネントにパッディングをつけると上記のようになるのですが、実際に運用するとコンポーネントは結構な数になるのでこのModifierが結構読みづらい感じになってきます。
これを解決するには、適切なコンポーネント分割やカスタムModiferなどをすれば良いのですが、それなりに工数もかかるので個人的にはもう少しスッキリ書けたらなと思っています。
この点は、CSSという強力なスタイル機能が使えるのReact Nativeのほうが好みではあります。
Jetpack Compose
こちらも2019年にGoogleが発表したUIフレームワークですが、こちらについては先日アルファ版になったばかりということもあり、僕自身まだ触っていない段階のため特に記載することはありません。
ただ、先日 PCアプリを作るフレームワークとしても使える発表(「Jetpack Compose for Desktop」)があるなどGoogleも力を入れていそうなので、これからの期待はしています。
余談にはなりますが、Androidは現状のxmlによるUI宣言 + LiveData + ViewModel などのエコシステムで結構気持ちよくUIが書けるイメージがあり、現時点では結構安定しているのではないかなと思っています。
React Native(Expo)
React NativeとはもともとFacebookがWebアプリを作るために開発していたReactをネイティブアプリ(iOS, Android)でも使えるようにした物です。
React Nativeは、基本的にネイティブのコードとJavaScriptを介したインタフェースでやり取りします。
そのためプロジェクトの構成としては、
・iOS-React Native間のライブラリコード
・Android-React Native間のライブラリコード
・ReactNativeのJavaScriptのコード
のような構成になっており、プロジェクトを作成すると「iOSプロジェクト」「Androidプロジェクト」「Javascriptプロジェクト」の3つが作成されます。
Expoは、このネイティブのプロジェクトを隠蔽してJavaScriptだけ操作すれば良い状態にしてくれるフレームワークです。
詳細は割愛しますが、最低限必要なライブラリ郡は予めExpoライブラリがネイティブに組み込んでくれており、Expoが有している機能についてはネイティブを一切意識せずJavaScriptだけで開発ができます。
(アプリのビルドもExpoサーバが行ってくれるためローカルにはビルド環境も必要ありません)
この記事では基本的にExpoを使って開発する前提で記載しています。僕自身は素のReactNativeでも開発したことがあるのですが、基本的に「Expo一択でよい」というのが結論です。
(理由も割愛しますが、素のReactNativeはメンテナンスコストが割に合わなすぎるというのが主な理由です)
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: 100,
height: 100,
},
label: {
color: 'red',
fontSize: 30,
},
})
class SampleScreen extends React.Component {
public render() {
return (
<View style={styles.container}>
<Image
style={styles.image}
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
}}
/>
<Text style={styles.label}>テキスト</Text>
</View>
)
}
}
コードの雰囲気としてはこんな感じになっています。
基本的には、ReactのJSX構文なのでReactを書いたことがあるひとであれば、馴染みやすいかと思います。
違いはとしては、React NativeはReactのようにHTMLのタグをそのまま使用することはできません。<View>や<Text>といったReact Nativeが用意したUIコンポーネントのタグを使う必要があります。
スタイリングは、上記のようにReactのstyleプロパティを使うことも可能ですし、cssやstyled componentなどJavaScriptの資産を使って自由に選択することが可能です。
React Native(Expo)の良いところ
個人的に感じたExpoの良いところは次のとおりです。
① ネイティブの知識がほとんどいらない
これはExpoの良いところですが、Expoに適切な情報を登録して置くと、iOSやAndroidの製品版をビルドする時に管理する必要がある証明書などは、全てExpoのサービス側で作成してくれます。
Expoのビルドコマンドを実行後、アプリバイナリのリンクからアプリをインストールして申請するだけです。
また、デバッグ実行はExpoのアプリ(ストアにExpoアプリがある)上にJavaScriptをインストールすることで行われるため、デバッグ用のアプリをビルドして実行する必要もありません。
他のフレームワークと比べるとこの辺は本当にうまくやってくれていて、Expoが一番便利だと個人的には思っています。
② ReactやJavaScriptの資産を使うことができる
既存で使われているJavaScriptの多くのライブラリを使うことができるのは、React Nativeを使うことの大きなメリットだと思います。
HTTPクライアントなどReactに依存しないライブラリは、大体使えますし、ReactのライブラリでもReact Nativeにも対応しているものが割とありエコシステムは割と成熟しているのかなと思います。
③ JavaScript(TypeScript)が強力
最近のJavaScript開発においては、TypeScriptを使うことが多いかと思いますが、React Nativeでも問題なくTypeScriptが使用できます。
また、個人的にはWebのアプリケーションを書くこともあるため、WebでReactを利用すれば、汎用的な設計や言語のコンテキストスイッチを切り替えなくても良いというのがメリットかと思います。
最近は、APIサーバもJavaScriptで書くことが容易になってきたため、API、Web、アプリをすべてJavaScript(TypeScript)で記述することもできることを魅力に感じる人はいるのではないでしょうか。
React Native(Expo)の良くないところ
① ライブラリのメンテナンスコスト
マルチプラットフォームのフレームワーク(React Native, Flutterなど)の場合ネイティブコードとのブリッジが必要なライブラリが多いためメンテナンスにコストが掛かることがあります。
例えば、「あるOSのアップデートのタイミングで仕様が変わってしまい、アプリが動かなくなってしまった」といった問題が起きるなどです。
ライブラリのメンテナンスコスト自体は、ネイティブのライブラリでも発生する問題ですが、個人的な体感としてはiOS、Androidと全く運営がことなるプラットフォームを2つ管理する特性上、この手の問題が起きやすい気がしています。
Expoが公式でメンテナンスしているライブラリであれば、このような場合も迅速に対応されることが多いですが、その他のライブラリだと長期間メンテナンスされない事象なども発生しがちです。
この時ネイティブアプリエンジニアであれば、コードを読んでOSS側にプルリクエストを出すことも可能かと思いますが、ネイティブの経験があまりいらないことをメリットとしているExpoエンジニアとしては、これも難しかったりします。
② iOSとAndroidで挙動が若干異なる
React Nativeは、基本的にはJavaScript経由でネイティブのUIコンポーネントを動かしているため、1つのコードで同じように書いていてもネイティブで動かしたときに微妙にデザインや挙動が異なる場合があります。
この時、どうしても修正したければ
if (Platform.OS === 'ios') { // iosの処理
のような感じでOSごとに処理分岐する必要があり結果的にメンテナンスコストが上がることもあります。
Flutter
Flutterは、2018年にGoogleによって発表されたマルチプラットフォームのモバイルアプリを開発するためのフレームワークです。
Flutterの特徴として、画面の描画をネイティブのUIコンポーネントではなくFlutterの独自の描画エンジンで行っているため、iOSでもAndroidでもほとんど見た目に違いがないUIが作成できます。
(その代わり、分かる人が見ればわかる程度ですが、UIの挙動がネイティブとは若干異なって見えます)
class SampleWidget extends StatelessWidget {
const SampleWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter サンプル'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: Alignment.center,
child: Icon(
Icons.pedal_bike,
size: 50,
),
),
Align(
alignment: Alignment.center,
child: Text(
'テキスト',
style: TextStyle(
fontSize: 20,
color: Colors.red,
),
),
),
],
),
);
}
}
コードの雰囲気としてはこんな感じになっています。
Flutterは、一つのUIコンポーネントが Widget という単位で管理されていてすべてWidgetを継承して描画されています。
SwiftUIの 「body」React Nativeの「render」と同様に「build」というレンダリング用のメソッドの戻り値でWidgetを返すことで画面描画ができます。
Flutterの良いところ
個人的に感じたFlutterの良いところは次のとおりです。
① iOSとAndroidで同じ見た目で開発できる
Flutterは、描画を独自で行っているため、OSの差異やバージョンの差異を気にする必要がありません。(意図的に分岐することは可能)
iOSとAndroidの標準コンポーネントの差異やOSバージョンにごとに見た目が大きく異なるAndroidに気を使わなくて良いのはメリットだと感じました。
② フレームワークの機能が充実している
好みもありそうですが、ExpoやSwiftUIと比べるとフレームワークの完成度が高いなと感じています。
例えば、Flutterの場合レイアウトやスタイルもWidget経由で行うことが多くどんどんネストしていくことになります。
書き始めのころは、「ネストが深くなって見ずらいし混乱しそう」と思っていたのですが、Widgetのパラメタの書き方がほぼすべて統一されているため、慣れてくると苦にならなくなりました。
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: Alignment.center,
child: Icon(
Icons.pedal_bike,
size: 50,
),
),
Align(
alignment: Alignment.center,
child: Text(
'テキスト',
style: TextStyle(
fontSize: 20,
color: Colors.red,
),
),
),
],
)
基本的にWidgetを受け取るパラメタ名は「child(複数の場合はchildren)」になっていてWidgetは「()」で必ず閉じられます。
(SwiftUIのModifierのようなチェインメソッドも無いし、JSXのように閉じタグを省略できるようなパターンもない)
そのため、一つのコンポーネントの構造を追うのがそこまでわかりにくくなりません。また、IDE側でもWidgetを1階層消したり、外側にラップするための機能が用意されているため比較的簡単に構造を変えられます。
また、シンプルなベクターアイコンも多数用意されているし、Androidで使える基本的なUIコンポーネントもほぼサポートされています。(iOSは若干不足しているのかも?)
画面遷移のシステムもFlutter独自なのでネイティブを意識せず新しい仕組みとして理解すれば良いといった所も学習コストが低くて気持ち良く書けると感じました。
Flutterの良くないところ
① dartが他言語に比べると若干弱い
個人的には、dartの言語機能がSwift, Kotlin, TypeScriptと比べると若干書き味が悪いと感じました。
nullableな仕組みも最近導入されたようですが、(僕は最近はじめたので既にありました)既存ライブラリでも未対応なものもまだ多いように思います。
スマートキャストやcase式などもSwift, Kotlinと比べるとまだ進歩の余地があるのかなと思います。
async/awaitなどはSwiftやKotlinより直感的だと思うのであくまで若干という感じです。
② ネイティブのプロジェクトを結構触らなくてはならない
例えば権限周りの設定や、アイコン、スプラッシュスクリーンなどiOSとAndroidのプロジェクトを直接触らなくてはならない箇所が結構あるように感じました。
特に僕の体験としてネイティブのビルドツールのバージョン差異などでうまく動かないケースがあったりして、全くモバイルアプリが初見の人はそのあたりは頑張る必要がありそうです。
個人的なおすすめ
4つの(ほぼ3つ)フレームワークについて、実際にどれを使うのが良いかは前述のとおり開発のスキルセットやリソースによって異なるため、あくまで僕が開発する場合のおすすめを書きたいと思います。
① モバイル専属のエンジニアが不在でかつ比較的単機能な場合 => 「Expo」
WebViewが多いなどExpoで機能がほぼまかなえる見込みがあるなら、ネイティブのメンテナンスコストが少ないExpoがおすすめです。
② モバイル専属のエンジニアがいて、OS機能をゴリゴリ使いたいなら => 「SwiftUI、(Jetpack Compose)」
結局のところネイティブのブリッジは結構たいへんなので、モバイルのエンジニアが各OSごとにいるならネイティブのコードで書くのが良いかなと思います。
また、都度更新されるOS機能(MLとか音声とか無線とか)をゴリゴリ使うような案件であればやはり、ネイティブコードのほうがリスクが少ないのではないかと思います。
また、iOSとAndroidの各OSごとの特性を生かしたUIを作りたい場合も別々が良いと思います。
③ その他 => 「Flutter」
ある程度、ネイティブ側の知識があり、両OSの開発コストを減らしたいならFlutterがおすすめです。
最後に触ったからというのも影響ありそうですが、個人的には現時点では一番完成度が高いかなと感じています。
まとめ
意外に書くことが多くて全く書ききれなかったのですが、モバイルの開発環境を選択するときに迷っている方の少しでも参考になれば幸いです。
この記事が気に入ったらサポートをしてみませんか?