Google I/OでAndroid向けの新しいUIライブラリとしてJetpack Composeがアナウンスされ、その翌月にはApple WWDCでSwiftUIがアナウンスされ、GUIフレームワークには確実に新しい流れが来ている。Flutterについても同じようなトレンドに乗っているということができる。
これらは、「トレンド」として捉えれば妥当だが、「革新的」だったわけではない。これからいくらでも宣伝過剰な記事が出てくると思うので、なぜこれらが革新的ではないのかを説明することを主眼に置いて、これまでのGUI開発において起こってきたことをなぞりつつまとめておきたい。革新的でないことを立証するのは過去の実例を出せばいいだけの話なので簡単だ。
Flutter、Jetpack Compose、SwiftUIに共通する特徴を挙げてみよう。
- 特定の言語(Dart/Kotlin/Swift)に依存したフレームワークの最適化
- リアクティブな設計に基づく宣言的UI (Streams, Combine frameworks)
- インスタントなUIの反映を意識した設計(Hot Reload, Apply Changes, Xcode Swift REPL)
- プラットフォームの古いUIツールキットに依存しない描画レイヤーからのフルスクラッチ設計
Jetpack ComposeとSwiftUIについては次の共通点もある。
- ネイティブUIファーストの復権
UIフレームワークが特定の言語に合わせて設計・強化されるという側面もあるが、言語のほうが新しいUIフレームワークの発展に合わせて機能強化されるというトレンドがある。
Dart 2.0がそもそも全面的にFlutterのために新機能を追加したもの
- TODO: Kotlin最新版の新機能を確認
- コレクションの記述など?
- Kotlinを活用したイベントの記述は非常にシンプル。同じことはJavaでは到底実現できないし、C#でも怪しい。イベントなどはobject initializerで簡単に記述できない。
- some https://stackoverflow.com/questions/56433665/what-is-the-some-keyword-in-swiftui
- 全部structなのはFlutterの「何でもかんでもWidget」みたいな状況になったときに無駄にメモリを食わないようにすることを想定している? あるいは差分計算の単純化とその後の対象メモリブロック全コピーを可能にするため?
- (差分計算をやっていることはやっているようだ https://twitter.com/luka_bernardi/status/1136050906766569472 )
- UI as codeの実現
- 開発環境の単純化・コード分析の一本化
- XMLの表現力は貧弱なので、Androidのdata-bindingのようなテンプレート言語が入ってくることになって、結局そのためのリソース(学習時間や解析系実装コスト)が無駄に費やされることになる
- 反復処理やささいな条件分岐などコードで書けば瞬殺で終わるものがXMLで無駄に面倒になる
- 言語ツールが単なるコンパイラだけではなくlanguage service的な機能を統合できたことによって可能になった側面が多分それなりにあるかもしれない。現状あまり関係ない気がしている。
- Jetpack Composeについては正直よくわからない。Apply ChangesはAST分析などを伴わないし、annotationの付いたコードは主にUIコード生成に使われている雰囲気があるので、コード分析は全くいらない気がする
- SwiftはREPLが強い。ただこれ自体はlanguage serviceが実現しているわけではない
- Flutter/Dartはもともとインタープリターでコードの部分的実行を実現できているのではないか(Dart処理系は正直よくわからん)
- UIをXMLで記述するフレームワークでもHot Reloadのような機能は実現できていた(LiveXAMLなど)
- リソースをまるっと入れ替えてActivity等をrestartするだけなので実行の仕組みさえあれば全然難しくない
- ElmやF#のElmish
- リアクティブな設計 ≒ 宣言的にイベントを記述できるモデル
- 宣言的UI: テストが容易になる
- MV-whateverの実現(BLoC, AAC ViewModel, )
この辺はフレームワーク側が開発モデルを用意するところまで来たという程度で、実のところフレームワーク側がやる必要があったとは言い難い。どのフレームワークも最初はフレームワークの外側からReactiveを実現していた。
「Hot Reloadができること」を目標にしたり、コレでUIフレームワークの出来の良し悪しを判断するのはバカバカしい。目標は「アプリケーションのUIを手早く構築・確認できること」にあるはずだ。目標が実現できるなら手段は何でもよい。
いくつかのアプローチがある。
- 疑似再現環境におけるUIの部分的実現 - Xamarin Inspectorなど
- REPLによる実現 - SwiftUI, Continuous Coding
- (主に)UIコードの差分(部分的)実行による実現 - Flutter Hot Reload, LiveXAML (, Xamarin LiveReload), Apply Changes, Instant Run warm swap, LayoutCast
Inspector、Previwerの類は実のところHTML等を使用してUI部品を擬似的に実現したものが多く(XamarinもFlutter も)、再現度には限界がある。
LiveXAMLやLayoutCastはその手段においてXMLリソースの更新とアプリケーションの部分的再実行(Activityの再起動など)を行っているのであって、本質的にはFlutterのHot Reloadと何ら変わらない。
Continuous CodingにおいてはXamarinアプリケーションにおけるコードのREPL実行を(UI、非UIを問わず)実現している。
ただUIはActivityの再起動など所定のマナーに基づいて行われるべきであり、そのための実行基盤が整備されているべき。これを整備して「ビューのコードではStateとWidgetを切り離せ」と明示したのはFlutter Hot Reloadの特性であり、その他の機構(Continuous Coding, Instant Runなど)の問題といえる。当然ながらビューとロジックの切り離しという発想は20世紀からある。Apply Changesは処理内容が明確化されており、この特性を別のかたちで実現しているといえる。
インスタントなコード実行を可能にする・妨げる要因:
- アプリケーション・パッケージに含まれないコードの追加実行可否: iOSデバイスの動的ネイティブコード実行の禁止やAndroidで実行中に差し替えられないようなのDalvikバイトコードの更新(クラスの追加など)が問題になる
- プラットフォーム コードでないものは実行が比較的容易(React Native, Dart, mono)
- ただしプラットフォーム コードの変更を伴う場合は別(XamarinでACWクラスのメンバー追加を伴う場合など)
- 単にコード実行方式がインスタント実行を容易にしている側面が強く、要は都合が良かっただけであって、これらが可能になっている言語が特別に優れているわけではない
- いろいろ古い前提で作られたレガシーAPIを切り捨てることができる
- iOS: 32bitの切り捨て。Metal前提
- Android: armeabiの切り捨て (Vulkanは前提にできるかもしれないし、できないかもしれない)
- Jetpack Compose(Android)については、GUIフレームワークに新機能を追加する際のプラットフォーム バージョン依存性を排除できる
- SwiftUIの場合はむしろ新しいフレームワークが新しいOSに依存するので状況は悪化している(過渡的な問題ではある)
UI as codeについて思うのは、XML的なアプローチがIDEの補助をそれほど得られなかったというのが私的に大きいと感じました。XAMLでもintellisenseはC#のそれとは比較にならず、当初の説得力だったコードで表現できない(listboxのitem templateのような)要素も、時代が進んだら遅延評価関数が簡単に書けるようになって、なんでこんな面倒くさいものを書かなきゃならないのか、という思いが強くなり... ましてやデザイナー(人間)との協業なんてどこかに吹き飛んで、今やデザイナーがフレームワーク知ってないとお話しにならないという状況...