宣言型UIフレームワークを比較してみた

この記事はGoodpatch Advent Calendar 2022の2日目の記事です。

こんにちは!エンジニアの藤井(@touyou_dev)です。今年もアドベントカレンダーの季節がやってきましたね🙌 今年は特に気温が極端かつ変則的で、季節感が迷子な一年でしたが、それに負けず劣らず自分も実は従事する領域を反復横跳びのような形でいったりきたりしていました。 メインはiOS開発ですが、副業ではNext.jsでの開発をやっていたり、最近だとWebの案件に関わることもあってAngularを触ることになるなど......

専門領域を深掘りにくいというデメリットもあるように思えるこういった働き方ですが、一方でそれぞれの領域での知見を実感を持って知ることでこれらの共通点や違い、そこからの気づきなど見えてくるものもあります。

今回は自分が実際に業務で触っているSwiftUI, Next.js(React.js), Angularに加えて、学生時代にAndroid開発をやっていたこともあるのでJetpack Compose、さらにはNext.jsとよく対比されるNuxt.js(Vue.js)のバージョン3以降のもの、そしてFlutterの計6つを比較してみることで、それぞれの特徴や共通点、わかってくることなどをまとめたいと思います。

なお関連する記事としてSwiftUIとReact.jsの共通項からSwiftUI上でのObservableObjectの扱いを提案したものも過去に書いているので、興味のある方はぜひあわせてご覧ください。

goodpatch-tech.hatenablog.com

どうやって比較するか?

もちろんそれぞれの特徴を逐一調べてまとめるだけでも比較にはなると思いますが、それだと見えてこない部分というのもあると考えています。 そこで今回は実際にすべてのフレームワークで、とてもシンプルなカウントアプリを作ってみることを通して比較してみました。

この後の記事内でもコードを適宜引用することがあると思いますが、先に見ておきたい方はこちらのリポジトリに上がっているので参考にしてください。

github.com

なお、リポジトリのREADMEにも記載していますが、サードパーティのライブラリは全てにおいて導入していません。 その他にも自分の慣れの問題で最適なコードになっていない箇所もあるかとは思いますが、あらかじめその点ご理解いただいた上で読んでいただければと思います。

実際の比較の前におさらい

それでは早速比較していきたいのですが、その前にまずそもそもこれらが分類されている宣言的なUIフレームワークとはどういったものなのかを簡単におさらいしておきましょう。

まずそもそもの用語ですが、調べてみるとわかるとおりWikipediaも存在していませんし*1、宣言「型」UIフレームワークなのか、宣言「的」UIフレームワークなのかはあまり明確に決められていません。(検索をかけると宣言的UIフレームワークと書いている記事が多いですが、Googleの公式ドキュメントでは宣言型と訳されています。)

このように出所が見つけにくい言葉ではありますが、対となるのは命令型UIで、iOSのUIKitやAndroidのこれまでのUI記述方式などが該当します。 ではこの宣言型と命令型の何が違うかというと、UIの記述方法の違いです。

具体的にiOSで比較してみましょう。わかりやすいようにStoryboardを使わないUIKitとSwiftUIで比較します。 単純なボタンを実装することを考えます。UIKitだとこのようになるでしょうか。

let button = UIButton()
button.frame = CGRect(x: view.frame.width / 2 - 100, y: view.frame.height / 2 - 22, width: 200, height: 44)
button.setTitle("Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .blue
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)

一方同じことをSwiftUIで実現しようとするとこのようになります。

VStack(alignment: .center) {
  HStack(alignment: .center) {
    Button(action: {
      buttonTapped()
    }) {
      Text("Button")
        .foregroundColor(.white)
    }
    .frame(width: 200, height: 44)
    .background(.blue)
  }
}

記述量的にはそこまで大きな差があるわけではないですが、命令型であるUIKitがボタンに対して振る舞いを一個ずつ命令しているのに対し、宣言型のSwiftUIはUIがどのように構成されているのかを宣言し、そこに対して設定値などを加えていく形をとっています。 この違いによって、宣言型はコードを見ただけである程度どのような見た目になるかをそこまで詳しくない人でも想像しやすくなっています

まずはこの程度の違いの認識で大丈夫だと思います。ここからさまざまなフレームワークを見ていくことによってその性質の理解をさらに深めていきましょう。

比較してみよう!

それでは実際に比較してみましょう。といってもコード全体を載せるのは読むのが大変かと思うので、ポイントごとに抜き出して紹介していきたいと思います。

比較ポイント1:言語

まずは基本のキ、言語です。Webは複数の言語を組み合わせるような形になっているものもありますが、特にUI記述の部分に関して着目すると次のようになっています。

SwiftUI Jetpack Compose Flutter Next.js Nuxt.js3 Angular
Swift Kotlin Dart JSX Vue テンプレート言語(HTML)

全体的な傾向としてモバイルはそれぞれの基本の開発言語で、WebはHTMLに近いタグ型の言語で記述していることがわかります。 ただこれだけだとなんとも、という感じですね。これを踏まえた上で次のポイントを見ると少し見えてくるものがあります。

比較ポイント2:UIをどうやって実装しているか?

続いてのポイントがUIをどうやって実装しているか、です。もっと言えばどのような言語機能を使うかというところになります。 こちらも比較表の形でまとめてみると次のようになります。

SwiftUI Jetpack Compose Flutter Next.js Nuxt.js3 Angular
プロトコルに適合した構造体(もしくはそれを返す関数) 関数と専用デコレータ ベースクラスを継承したクラス 関数もしくはベースクラスを継承したクラス テンプレート言語 テンプレート言語

見ただけだと一見「だから何だ」という感覚になるかと思いますが、これらの共通点として基本的に変化をそれだけでは許さないという特徴があります。もう少し踏み込んで言うと基本副作用がない、といえます。

厳密に言えばクラスは副作用を生む代表格でもあるので当てはまらないのですが、Flutterでは副作用がないStatelessWidgetをなるべく使うことが望ましいとされていますし、Next.jsのベースとなっているReact.jsでも、関数コンポーネントが出てきてから新規プロジェクトではなるべくそちらで実装することが望ましいとされています。 つまり全体として副作用がないことが良いとされているわけですね。

この特徴がある理由はふたつのアプローチで説明できます。 一つ目の理由は副作用がないことによって同じ設定で記述すれば常に表示が一意に定まる、すなわち結果が想像しやすいというメリットがあるからです。これによって学習コストも下がり、より多くの人が開発に携わることができるようになります。 もう一つはそもそもの「宣言型」という言葉の由来です。冒頭に書いた通り、宣言的UIフレームワークという言葉自体はWikipediaなどもなく、なんとなくみんなが使っている言葉なのですが、この元になっている言葉として宣言型プログラミングというものがあります。これは命令型プログラミングと対になる言葉で、この中に関数型プログラミングなども含まれます。そしてこの関数型プログラミング、大きな特徴として副作用を基本的には許さないというものがあります(副作用を一切排除したものを純粋関数型プログラミングと呼んだりもします。)つまりそもそも「宣言型」という言葉の定義にこの副作用を排除するという性質が含まれるわけです。

実際宣言型プログラミングのWikipediaにもこのように書かれています。

宣言型言語は、what the program must accomplish(何をなすべきか)方針で、副作用を排除した式や純粋関数の実装に努める。これは命令型言語の、how to accomplish it(どうなすべきか)方針で、副作用を前提にした操作的意味論下のアルゴリズム実装とよく対比される。

以上長くなりましたが結論として、ほとんど全ての宣言型UIフレームワークが副作用を基本的に排除するという性質を言語機能によって明示的に表現しているということがわかりました。

比較ポイント3:スタイリング方法

続いてはスタイリング方法です。早速まとめたものを見ていきましょう。

SwiftUI Jetpack Compose Flutter Next.js Nuxt.js3 Angular
ViewModifier Modifier引数 styleなどの引数 CSS CSS CSS

基本的にはWebはCSS、モバイルはmodifierなどの名前がついたものを引数などの方法で渡す形になっています。 実装方法の差異は言語機能によるところが大きいですが、こちらの共通点としては構造とスタイリングが分離しているというところがあげられるでしょう。

ちなみにNext.js(React.js)にはCSS in JSとCSSモジュールに代表される書き方のバリエーションがあるように、全ての書き方が一意なわけではないですが、個人的にはSwiftUIのViewModifier形式が好きです。

比較ポイント4:結果の確認方法

宣言型の大きな特徴のもう一個として、結果の確認方法があります。

SwiftUI Jetpack Compose Flutter Next.js Nuxt.js3 Angular
プレビュー機能 プレビュー機能とホットリロード ホットリロード ホットリロード ホットリロード ホットリロード

このように全てのフレームワークに対してプレビュー機能か、ホットリロードが備わっていることが多いです。(厳密には標準で備わっているわけではない場合もありますが) これは単に時代が進んで便利になったという見方もできますが、もう少し深ぼってみるととても興味深い共通点になります。

そのことがよく分かるのがiOSで、Xcodeでプレビュー機能をサポートしているのはSwiftUIだけでUIKitやAppKitにはないという違いがあります。 この一番大きな理由としては、副作用がないという特徴があるために即時に見た目を構築しやすくなっているという部分があるでしょう。その証拠としてSwiftUIでは単純な見た目だけであればプレビュー機能が大活躍しますが、状態管理などが入ると少しややこしくなったり作り方によっては表示できなくなってしまったりします。

理由としては以上なのですが、これは宣言型が昨今注目されている理由としてのわかりやすさという性質に拍車をかけている部分もあるなと感じました。書いて即時にどうなるかがわかるわけですから、トライアンドエラーのサイクルは最速です。やはり何事もトライアンドエラーのサイクルをいかに回すかで成長できるわけですから、わかりやすいのはもちろん学習のしやすさもあるわけです。 このような部分が今注目されている要因になっているのかなと思いました。

比較ポイント5:状態管理

最後の比較ポイントとして、副作用を基本的に許さない宣言型UIフレームワークがどのように状態変化などを扱っているのかについて見ていこうと思います。 粒度はまちまちですが表にすると以下のようになります。

SwiftUI Jetpack Compose Flutter Next.js Nuxt.js3 Angular
@State (Property Wrapper) remember + mutableStateOf StatefulWidget useState useState @Componentをつけたクラス

タイプとしてはざっくり三つに分かれると思います。一つは単純にクラスに状態変化の役割を担わせているFlutterとAngular、多数派を占めているのはReact.jsのReact Hooksに代表されるような、関数に対して状態変化を導入する仕組みを導入したJetpack Compose、Next.js、Nuxt.js 3、そしてそれに似てはいますが、関数ではなくそれを構造体に対して実装したSwiftUIです。ただこれはあくまであえて分けるなら、であって、Flutterも多くのプロジェクトでは状態管理用のライブラリを入れてなるべくStatefulWidgetは使わないようにするのが主流ですし、Angularも標準でDI機構が充実しており、またRx系のサポートも手厚いため、純粋にコンポーネント内で状態変化を全て扱うというようなことは少ないかと思います。

このように全てにおいてやはり、ロジックと見た目を分離するという方針は共通しているように思えます。もちろんその上で各フレームワークにおいてより良い書き方というのは模索されているわけですが、副作用を言語レベルでは許さないというところは崩さず、その上で必要に応じて副作用を加えられるようにしているという姿勢は一貫していますよね。

比較からの学びを通して

以上5つのポイントについて比較結果をみてきました。 全体を通しての学びは副作用というキーワードが中心になってきますが、それ以上に大事な気付きは、やり方がそれぞれとはいえ、全てのフレームワークの根底に流れる思想は似ている、ということです。

この気付きによって、グッと知識の輸出入がやりやすくなると思います。まさに冒頭で紹介したSwiftUIのObservableObjectをReact Hooksとしてみなすというのはその代表例です。 ぜひ皆さんもこの記事をきっかけに、周辺領域の動向を少し覗いてみて、良さげなものを自分の得意分野に持ち込んでみるというのをしてみてはいかがでしょうか?

最後に、「宣言型」について

ざっくりとこの記事の内容に関しては先ほどの節で締めて、最後に「宣言型」とその周辺事情について少し考察をめぐらせておわりにさせていただこうと思います。 今回この比較をするにあたって、宣言型というものに関しても少し調査をしていました。そうしたところこちらの記事宣言型がオートマで、命令型がマニュアルであるというアナロジーが紹介されているのを見つけました。

今回のこの記事を細かく深ぼるとか、他の解説記事を調べ漁るよりずっとわかりやすいアナロジーです。ですがこのアナロジーを受け入れた時、一個の疑問が生まれました。 それは「どうして宣言型プログラミングは宣言型UIが広まるまでメジャーにならなかったのか?」というものです。

ここまで見てきた宣言型UIも、自動車のオートマも対となるものに比べて初心者にも扱いやすい、という特徴は共通しています。 ですが、宣言型プログラミングの代表格である関数型プログラミングは、むしろ世の中的には難しいものとされていることが多いように感じます。

こう考えた時に、何が違うのか、というところを深ぼってみると、今回の記事の途中でも言及した結果の即時性が大きく関わっているのではないかという結論にいたりました。 そして結果の即時性は、必ず結果が一意に定まる副作用の排除があって初めて実用化されます。そしてこれが進むとどうなるか?

そこにUI構築の自動化が見えてくるのかな、というのが今回感じたところです。現に自動車はマニュアルからオートマを経て自動運転が徐々に実現されつつあります。それと同じ流れがいずれ来る可能性もあるのではないでしょうか?

それと同時に自動車とUIでは明確に違う部分があります。それは結果に対しての厳格なルールの有無の差です。自動車は交通ルールに沿って動いてもらわないと事故につながりかねないため、自動運転が実行された結果は必ず特定のルールにしたがっていなければなりませんが、UIの場合使いやすさを保証するHIGやMaterial Designに代表されるガイドラインはあるものの、絶対的にそれに従わなければならないとは限りません。というより、そもそもUIにおいてはそのガイドライン自体が時代によって変化していきます。

つまり何が言いたいかというと、これからの時代エンジニアやUIデザイナーはより「変化」を扱う職能として働いていくべきなのではないかな、と思ったというのが結論です。まずは状態という「変化」をどのように設計しプログラミングするか、そしてUI構築が自動化された未来でどのように従うべき正解を「変化」させていくか。 話が最後の最後で大きくなってしまいましたが、機械学習が普及し始めた今、そして宣言型UIが市民権を獲得し始めたまさに「過渡期」である今に、次を見据えていくというのも大事なことなのではないかなと思っています。

*1:正確には「宣言型プログラミング」という用語だとWikipediaは存在しています。このパラダイムをUI記述に持ってきたものが宣言型UIだと捉えてください。