Integrating Compose with your existing app architecture

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 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 ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.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 MyScreen() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            Greeting(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

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
    }
}