Compose を既存のアプリ アーキテクチャと統合する

Unidirectional Data Flow(UDF)アーキテクチャ パターンは、Compose とシームレスに連携します。アプリで Model View Presenter(MVP)のような他のアーキテクチャ パターンを使用している場合は、Compose を導入する前、または導入中に UI の該当部分を UDF に移行することをおすすめします。

Compose の ViewModel

Architecture Components ViewModel を使用している場合、Compose と共通ライブラリの統合に関するドキュメントで説明されているように、viewModel() 関数を呼び出すことで、任意のコンポーザブルから ViewModel にアクセスできます。

Compose を導入する場合、ViewModel 要素は View のライフサイクル スコープに従うため、異なるコンポーザブルで同じ ViewModel タイプを使用する際は注意が必要です。スコープは、Navigation ライブラリが使用されている場合は、ホスト アクティビティ、フラグメント、ナビゲーション グラフのいずれかになります。

たとえば、コンポーザブルがアクティビティでホストされている場合、viewModel() は常に同じインスタンスを返しますが、このインスタンスはアクティビティ終了時にのみクリアされます。次の例では、同じ GreetingViewModel インスタンスがホスト アクティビティ下のすべてのコンポーザブルで再利用されるため、同じユーザー(「user1」)にメッセージが 2 回表示されます。最初に作成された ViewModel インスタンスは他のコンポーザブルで再利用されます。

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

また、ナビゲーション グラフは ViewModel 要素のスコープを設定するため、ナビゲーション グラフ内のデスティネーションであるコンポーザブルには ViewModel の異なるインスタンスがあります。この場合、ViewModel のスコープはデスティネーションのライフサイクルに設定され、デスティネーションがバックスタックから削除されるとクリアされます。次の例では、ユーザーがプロフィール画面に移動すると、GreetingViewModel の新しいインスタンスが作成されます。

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

状態の信頼できる情報源

UI の一部に Compose を導入する場合、Compose と View システムコードでデータを共有する必要が出てくる可能性があります。可能であれば、両方のプラットフォームで使用される UDF ベスト プラクティスに即した別のクラスに、共有状態をカプセル化することをおすすめします。たとえば、データ更新を出力するために共有データのストリームを公開する ViewModel にカプセル化します。

ただし、共有するデータが可変型であったり、UI 要素と密接に結びついていたりすると、そうはいかない場合もあります。その場合、一方のシステムが信頼できる情報源であり、もう一方のシステムとデータの更新を共有する必要があります。一般的な経験則として、信頼できる情報源は、UI 階層のルートに近い方の要素が所有する必要があります。

信頼できる情報源としての Compose

SideEffect コンポーザブルを使用して、Compose 以外のコードに Compose 状態をパブリッシュします。この場合、信頼できる情報源は、状態の更新を送信するコンポーザブルに保持されます。

たとえば、分析ライブラリを使用すると、カスタム メタデータ(この例では「ユーザー プロパティ」)を後続のすべての分析イベントにアタッチすることで、ユーザー全体をセグメント化できます。現在のユーザーのユーザータイプを分析ライブラリに伝えるには、SideEffect を使用してその値を更新します。

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

詳しくは、Compose における副作用をご覧ください。

信頼できる情報源としての View システム

View システムが状態を所有し、それを Compose と共有する場合は、状態を mutableStateOf オブジェクトでラップして、Compose がスレッドセーフになるようにすることをおすすめします。このアプローチを使用すると、信頼できる情報源がないため、コンポーズ可能な関数は簡略化されますが、View システムは、可変状態と、その状態を使用する View を更新する必要があります。

次の例では、CustomViewGroupTextViewComposeView(内部に TextField コンポーザブルがある)が含まれています。TextView は、ユーザーが TextField に入力した内容を表示する必要があります。

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}