Compose がクロスプラットフォームである理由は何ですか?

これは2022 Kotlin Chinese Developers Conference私が持ってきた共有物です. 会議後、一部のネチズンが PPT コンテンツを読みやすいようにテキストに整理したいと報告したため、この記事が生まれました。このカンファレンスのエキサイティングな内容をさらに知りたい場合は、JetBrains 公式ビデオ アカウントにアクセスしてカンファレンスのライブ リプレイを視聴することもできます。

序文

Compose は、Android アプリケーション開発に使用できるだけでなく、レイヤード アーキテクチャ設計と Kotlin のクロスプラットフォームの利点により、大きな可能性を秘めた Kotlin クロスプラットフォーム フレームワークとしても使用できます。この記事では、Compose ランタイムの観点からクロスプラットフォーム開発を実現するための Compose の基本原則を見ていきます。

アーキテクチャ層を構成する

フレームワークとして、Compose はアーキテクチャの下から上にレイヤに分割されています。

  • Compose Compiler : Kotlin コンパイラ プラグイン。コンポーザブル関数の静的チェックとコード生成を担当します。
  • Compose Runtime : Composable 関数の状態管理と、実行後のレンダリング ツリーの生成と更新を担当します。
  • Compose UI : UI レイアウトや描画などの UI レンダリングのレンダリング ツリーに基づく
  • Compose Foundation : レイアウト用の基本的なコンポーザブル コンポーネントを提供しますColumnRow
  • Compose Materials : マテリアル デザイン スタイルに合わせた上位レベルのコンポーザブル コンポーネントを提供します。
    各レイヤーの役割は明確であり、その中で Compose コンパイラーとランタイムが宣言型 UI 全体の操作をサポートする基礎となります。

コンパイルコンパイラ

まず Compose コンパイラーの役割を見てみましょう。

左側のソースコードは非常にシンプルなコンポーザブル関数で、状態を持った大きなボタンを定義しており、ボタンをクリックするとボタン内に表示されるカウントが増加します。

Compose Compilerでソースコードをコンパイルすると右のようになり、大量のコードが生成されます。まず、関数シグネチャにはさらにいくつかのパラメーター、特に %composer パラメーターがあります。次に、startRestartGroup/endRestartGroup、startReplaceGroup/endReplaceGroup など、%composer への多くの呼び出しが関数本体に挿入されます。これらの生成されたコードは、Compose Runtime レイヤーの作業を完了するために使用されます。次に、ランタイムが何を行っているかを分析しましょう。

グループとスロットテーブル

Composable 関数は値を返しませんが、実行中に UI レンダリングを提供するプロダクト (コンポジションと呼ばれます) を生成する必要があります。パラメータ %composer は、Composition のメンテナであり、Composition の作成と更新に使用されます。コンポジションには、ステート ツリーとレンダー ツリーという 2 つのツリーが含まれています。

2 つのツリーについて: React を知っている場合は、これら 2 つのツリー間の関係を React の仮想 DOM ツリーとリアル DOM ツリーにたとえることができます。Compose の「仮想 DOM」は、UI 表示に必要な状態情報を記録するために使用されるため、これを状態ツリーと呼びます。

ステート ツリー上のノード単位はグループです。コンパイラによって生成される startXXXGroup は基本的にグループ単位を作成します。startXXXGroup と endXXXGroup の間に生成されるデータ状態は現在のグループに属します。生成されたグループはサブグループになります。 Composable の場合、グループベースのツリー構造が構築されます。

グループについて: グループはいくつかの機能単位です。たとえば、RestartGroup は再編成できる最小単位であり、ReplaceableGroup は動的に挿入できる最小単位です。状態はグループに編成され、状態ツリーをより柔軟に更新できます。コードにどのような startXXXGroup が挿入されるかは、Compose Compiler によって完全にインテリジェントに生成されるため、コードを記述するときに考える必要はありません。

状態ツリーは実際には、スロット テーブルと呼ばれる線形データ構造を使用して実装されます。スロット テーブルは、状態ツリーの深い走査の結果を格納する配列として理解でき、配列の各セクションは、対応する UI ノード上の状態を格納します。

Comopsable が初めて実行されるとき、生成されたグループとブラインド状態がスロット テーブルに埋められます。埋め込む際には、コンパイル時にコード位置に対して生成された非反復キーが伴います。スロット テーブルは、コード位置の保存 (位置メモ化) に基づいて呼び出すこともできます。再編成が発生すると、Composable は再び SlotTable を走査し、startXXXGroup のキーに従って現在のコードに必要な状態にアクセスします。たとえば、count は記憶を通じて再編成の最新の値を取得できます。

アプライヤーとノードツリー

スロット テーブルの状態をレンダリングに直接使用することはできず、UI のレンダリングはコンポジション内の別のツリー (レンダリング ツリー) に依存します。スロット テーブルは、Applier によってレンダリング ツリーに変換されます。レンダリング ツリーは実際のツリー構造のノード ツリーです。

Applier はインターフェイスであり、Node 型のノード ツリーの追加、削除、変更などのメンテナンス作業に使用されることはインターフェイス定義から容易にわかります。UI の挿入を例にとると、Compoable の if ステートメントで UI フラグメントの挿入を実現できます。if コードブロックはコンパイル時に ReplaceGroup を生成しますが、再編成時に if 条件がヒットして startReplaceGroup が実行されると、Slot Table に Group に対応するキーの情報が不足していることがわかり、これは挿入操作であり、新しいグループとその管轄区域を挿入します。ノード情報は、アプライヤを通じてノード ツリーに新しく挿入されたノードに変換されます。

SlotTable に新しい要素を挿入した後、後続の要素は直接削除されるのではなく、ギャップ バッファー メカニズムを通じて元に戻されます。これにより、ノード ツリーの後続要素の対応するノードの保持が保証され、ノード ツリーの増分更新が実現され、部分的なリフレッシュが実現され、パフォーマンスが向上します。

フェーズを構成する

前回の紹介と組み合わせて、ソース コードから画面アップロードまでの Compose のプロセス全体を見てみましょう。

  • Composable ソース コードが Compiler によって処理された後、Composition を更新するためのコードが挿入されます。作業のこの部分は Compose Compiler によって実行されます。

  • Composeフレームワークは、システム側から送信されたフレーム信号を受信すると、最上位層からComposable関数を実行し、その実行過程でComposition内のステートツリーやレンダリングツリーを順次更新していく処理を「コンポジション」と呼びます。作業のこの部分は Compose Runtime によって実行されます。

  • Android プラットフォーム上の Compose のコンテナは AndroidComposeView で、システムから送信された disptachDraw を受信すると、Composition のレンダリング ツリーの駆動を開始し、Measure、Lyaout、Drawing を実行して UI のレンダリングを完了します。作業のこの部分は Compose UI によって実行されます。

Compopse は、コンポジション -> レイアウト -> 描画という 3 つの段階でフレームをレンダリングします。
従来のビュー開発では、レンダリング ツリー (ビュー ツリー) のメンテナンスはコード ロジック内で完了する必要がありますが、Compose レンダリング ツリーのメンテナンスはフレームワークに引き渡されるため、Composition の段階が追加されます。これは、Compose がカスタム View コードよりも単純である根本的な理由でもあります。

プロセス全体を 2 つに分割すると、Compose Compiler と Compose Runtime がノード ツリーの更新を実行します。この部分はプラットフォームとは何の関係もありません。ノード ツリーは、任意のタイプのノード ツリーにすることも、レンダリングに依存しないノード ツリーにすることもできます。木。プラットフォームが異なればレンダリング メカニズムも異なるため、Compose UI はプラットフォームに依存します。Compoe UI レイヤーで独自のノード ツリーとさまざまなプラットフォームに対応するアプライヤを実装する限り、Compose Runtime によって駆動される UI 宣言型開発を実現できます。

Android ビュー用に作成

この結論に基づいて、Compose Runtime を使用して Android ネイティブ ビューのレンダリングを駆動する実験を行います。

まず、ビュー タイプ ノードに基づいてアプライヤを定義します。 ViewApplier

class ViewApplier(val view: FrameLayout) : AbstractApplier<View>(view) {
    
    
    override fun onClear() {
    
    
        (view as? ViewGroup)?.removeAllViews()
    }

    override fun insertBottomUp(index: Int, instance: View) {
    
    
        (current as? ViewGroup)?.addView(instance, index)
    }

    override fun insertTopDown(index: Int, instance: View) {
    
    
    }

    override fun move(from: Int, to: Int, count: Int) {
    
    
        // NOT Supported
        TODO()
    }

    override fun remove(index: Int, count: Int) {
    
    
        (view as? ViewGroup)?.removeViews(index, count)
    }
}

次に、2 つの Android ビュー、TextView と LinearLayout に対応するコンポーザブルを作成します。

@Composable
fun TextView(
    text: String,
    onClick: () -> Unit = {
    
    }
) {
    
    
    val context = localContext.current
    ComposeNode<TextView, ViewApplier>(
        factory = {
    
    
            TextView(context)
        },
        update = {
    
    
            set(text) {
    
    
                this.text = text
            }
            set(onClick) {
    
    
                setOnClickListener {
    
     onClick() }
            }
        },
    )
}

@Composable
fun LinearLayout(children: @Composable () -> Unit) {
    
    
    val context = localContext.current
    ComposeNode<LinearLayout, ViewApplier>(
        factory = {
    
    
            LinearLayout(context).apply {
    
    
                orientation = LinearLayout.VERTICAL
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT,
                )
            }
        },
        update = {
    
    },
        content = children,
    )
}

ComposeNode は Compose Runtime が提供する API で、スロット テーブルなどのノード情報を追加するために使用されます。Slot Tabl が Applier を通じて View ベースのノード ツリーを作成すると、Node のファクトリを通じて対応する View ノードが作成されます。

上記の実験により、Compose を使用して Android View を構築でき、同時に Compose の SnapshotState を通じて View の更新を実行できます。

@Composable
fun AndroidViewApp() {
    
    

    var count by remember {
    
     mutableStateOf(1) }

    LinearLayout {
    
    
        TextView(
            text = "This is the Android TextView!!",
        )
        repeat(count) {
    
    
            TextView(
                text = "Android View!!TextView:$it $count",
                onClick = {
    
    
                    count++
                }
            )
        }

    }
}

実行効果は以下の通りです。

同様に、Compose ランタイムに基づいて、任意のプラットフォーム向けに Compose ベースの宣言型 UI フレームワークを構築できます。

デスクトップと Web 向けに作成

JetBrains は Compose マルチプラットフォーム アプリケーションに多くの試みを行い、多くの成果を上げてきました。Google Jetpack Compose のフォークに基づいて、JetBrains は Compose for Desktop と Compose for Web を連続してリリースしました。

Compose Desktop と Android も LayoutNode のレンダリング ツリーに基づいており、Skia エンジンを通じて完全なクロスプラットフォーム レンダリングを実行します。そのため、レンダリング効果と開発経験において非常に一貫性があります。Compose Desktop は、Kotlin/JVM を利用してバイトコード製品にコンパイルし、Jpackage と Jlink を使用してさまざまなデスクトップ システム (Linux/Mac/Windows) 用のインストール パッケージにパッケージ化します。これは、JVM なしで直接実行できます。

Compose Web は、レンダリング ツリー ノードとして W3C 標準に基づく DomNode を使用し、Compose Runtime によって DOM ツリーを生成します。Compose Web は、Kotlin/JS を通じて JavaScript にコンパイルされ、最終的にブラウザーで実行およびレンダリングされます。HTML スタイルに近い Composable API は Compose Web で事前に作成されているため、UI コードを Android/デスクトップで直接再利用することはできません。

compose-jb 公式サンプルを通してデスクトップと Web の違いを実感する

https://github.com/JetBrains/compose-jb/tree/master/examples/todoapp

上記の各プラットフォームで Compose を使用した場合のページ効果、デスクトップと Android のレンダリング効果はまったく同じですが、Web と前者 2 つの実際の効果は異なります。それぞれのコードは次のとおりです。

Compose Desktop と Jetpack Compose の間にコードの違いはありませんが、Compose Web は、HTML タグと同じ名前を持つ Div、Ul、Composable を使用し、Modifier の代わりに style { ...} のような CSS 指向の DSL を使用します。開発エクスペリエンスはフロントエンドの習慣により一致しています。UI 部分のコードはプラットフォームごとに異なりますが、ロジック部分は完全に再利用でき、各プラットフォームの Compopse UI は、component.models.subscribeAsState() を使用して状態の変化を監視します。

マルチプラットフォーム向けに作成

JetBrains は、統一されたグループ ID を持つ Android、デスクトップ、Web の Compose を Kotlin マルチプラットフォーム ライブラリに統合し、Comopse Multiplatform が誕生しました。

KM ライブラリとして、Compose Multiplatform を使用すると、KMP (Kotlin マルチプラットフォーム プロジェクト) 内の共有可能なコードをデータ レイヤーから UI レイヤーおよび UI 関連のロジック レイヤーに上げることができます。

IntelliJ IDEA を使用して Compose Multiplatform プロジェクト テンプレートを作成します。これは、構造的には通常の KMP と変わりません。

  • android/desktop/web フォルダーは、各プラットフォームのプロジェクト ファイルであり、gradle に基づいてターゲット プラットフォームの製品にコンパイルされます。

  • 共通フォルダーは KMP の中心です。commonMain は完全に共有された Kt コードであり、expect/actual キーワードを通じてプラットフォーム差別化開発を実現します。

まず Gradle の Comopse Multiplatform ライブラリに依存し、次に commonMain で共有の Compose ベースの UI コードを開発できます。Compopse Multiplatform の各コンポーネントは、Jetpack Compose の対応するコンポーネントのグループ ID の androidx プレフィックスを org.jertbrains プレフィックスに置き換えます。

androidx.compose.runtime -> org.jetbrains.compose.runtime
androidx.compose.material -> org.jetbrains.compose.material
androidx.compose.foundation -> org.jetbrains.compose.foundation

やっと:

最後に、 「Compose for Multiplatform」「Compose Multiplatform」という言葉の違いについて考えてみましょう。私の意見では、Compose Multiplatform を使用すると、誰もが Multiplatform に集中しやすくなり、Flutter などの同様のフレームワークと自然に比較されるようになります。しかし、この記事の導入を通じて、Compose がクロスプラットフォーム向けに特別に構築されたフレームワークではないことはすでにご存知でしょう。現段階では、まったく同じレンダリング効果や開発エクスペリエンスを追求していません。その外観は、むしろ付加価値のようなものです。 Kotlin によってもたらされたサービス。

Compose for Multiplatform の焦点は Compose にあります。つまり、Compose はより多くのプラットフォームに対応できるということです。強力なコンパイラー層とランタイム層を利用して、より多くのプラットフォーム用の宣言型フレームワークを作成できます。Kotlin のアプリケーション シナリオと Kotlin 開発者の能力を拡張します。将来的には Compose クロスプラットフォームについても言及したいと思っています。誰もが Compose for Multiplatform の観点からその意味と価値を検討できるようになります。

おすすめ

転載: blog.csdn.net/vitaviva/article/details/128442401