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 を更新する必要があります。
次の例では、CustomViewGroup
に TextView
と ComposeView
(内部に 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 } }