Unidirectional Data Flow (UDF) architecture patterns work seamlessly with Compose. If the app uses other types of architecture patterns instead, like Model View Presenter (MVP), we recommend you migrate that part of the UI to UDF before or whilst adopting Compose.
ViewModels in Compose
If you use the Architecture Components
ViewModel library, you can access a
ViewModel
from any composable by
calling the
viewModel()
function, as explained in the Compose integration with common libraries
documentation.
When adopting Compose, be careful about using the same ViewModel
type in
different composables as ViewModel
elements follow View-lifecycle scopes. The
scope will be either the host activity, fragment or the navigation graph if the
Navigation library is used.
For example, if the composables are hosted in an activity, viewModel()
always
returns the same instance that will only be cleared when the activity finishes.
In the following example, the same user ("user1") will be greeted twice because
the same GreetingViewModel
instance is reused in all composables under the
host activity. The first ViewModel
instance created is reused in other
composables.
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 } }
As navigation graphs also scope ViewModel
elements, composables that are a
destination in a navigation graph have a different instance of the ViewModel
.
In this case, the ViewModel
is scoped to the lifecycle of the destination and
it will be cleared when the destination is removed from the backstack. In the
following example, when the user navigates to the Profile screen, a new
instance of GreetingViewModel
is created.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
State source of truth
When you adopt Compose in one part of the UI, it's possible that Compose and the View system code will need to share data. When possible, we recommend you encapsulate that shared state in another class that follows UDF best practices used by both platforms, for example, in a ViewModel that exposes a stream of the shared data to emit data updates.
However, that's not always possible if the data to be shared is mutable or is tightly bound to a UI element. In that case, one system must be the source of truth, and that system needs to share any data updates to the other system. As a general rule of thumb, the source of truth should be owned by whichever element is closer to the root of the UI hierarchy.
Compose as the source of truth
Use the
SideEffect
composable to publish Compose state to non-compose code. In this case, the
source of truth is kept in a composable which sends state updates.
As an example, your analytics library might allow you to segment your user
population by attaching custom metadata (user properties in this example)
to all subsequent analytics events. To communicate the user type of the
current user to your analytics library, use SideEffect
to update its value.
@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
}
For more information, see the side effects documentation.
View system as the source of truth
If the View system owns the state and shares it with Compose, we recommend that
you wrap the state in mutableStateOf
objects to make it thread-safe for
Compose. If you use this approach, composable functions are simplified because
they no longer have the source of truth, but the View system needs to update the
mutable state and the Views that use that state.
In the following example, a CustomViewGroup
contains a TextView
and a
ComposeView
with a TextField
composable inside. The TextView
needs to show
the content of what the user types in the 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 } }