将 Compose 与您现有的应用架构集成

单向数据流 (UDF) 架构模式可与 Compose 无缝协作。如果应用改用其他类型的架构模式,例如 Model View Presenter (MVP),我们建议您在采用 Compose 之前或期间将界面中的相应部分迁移到 UDF。

Compose 中的 ViewModel

如果您使用架构组件 ViewModel 库,您可以通过调用 viewModel() 函数从任意可组合项访问 ViewModel,如 Compose 与通用库的集成指南中所述。

采用 Compose 时,请务必注意在不同的可组合项中使用相同的 ViewModel 类型,因为 ViewModel 元素遵循视图生命周期范围。如果使用 Navigation 库,范围将是主机 activity、fragment 或导航图。

例如,如果可组合项托管在 activity 中,viewModel() 始终返回相同实例,该实例只有在 activity 完成时才被清除。在以下示例中,系统会向同一位用户显示两次问候语,因为主机 activity 中所有可组合项都重复使用了相同的 GreetingViewModel 实例。其他可组合项重复使用了创建的第一个 ViewModel 实例。

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

由于导航图还限定了 ViewModel 元素的范围,因此作为导航图中某个目标位置的可组合项具有不同的 ViewModel 实例。在这种情况下,ViewModel 的范围是目标位置的生命周期,它将在从返回堆栈中移除目标位置时被清除。在以下示例中,当用户转到“个人资料”屏幕时,系统会创建新的 GreetingViewModel 实例。

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

状态可信来源

当您在界面的某个部分采用 Compose 时,Compose 和 View 系统代码可能需要共享数据。如有可能,我们建议您将相应共享状态封装在另一个遵循两个平台所用的 UDF 最佳做法的类中,例如,封装在会公开共享数据流以发出数据更新的 ViewModel 中。

但是,如果要共享的数据会发生变化或与界面元素密切相关,这种方式就不可行。在这种情况下,必须有一个系统是可信来源,同时该系统需要与另一系统共享所有数据更新。一般来说,可信来源应由更靠近界面层次结构根目录的任一元素拥有。

将 Compose 视为可信来源

使用 SideEffect 可组合项向非 Compose 代码发布 Compose 状态。在这种情况下,可信来源会保留在发送状态更新的可组合项中。

例如,需要注册 OnBackPressedCallback 来监听通过 OnBackPressedDispatcher 按下的返回按钮。如需说明是否应启用回调,请使用 SideEffect 更新其值。

@Composable
fun BackHandler(
    enabled: Boolean,
    backDispatcher: OnBackPressedDispatcher,
    onBack: () -> Unit
) {

    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)

    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        // Always intercept back events. See the SideEffect for a more complete version
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // On every successful composition, update the callback with the `enabled` value
    // to tell `backCallback` whether back events should be intercepted or not
    SideEffect {
        backCallback.isEnabled = enabled
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(backCallback)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}

如需详细了解附带效应,请参阅生命周期和附带效应文档。

将 View 系统视为可信来源

如果 View 系统拥有状态并将其与 Compose 共享,我们建议您将相应状态封装在 mutableStateOf 对象中,以保证 Compose 的线程安全。如果您使用此方式,可组合函数将会有所简化,因为它们不再拥有可信来源,但 View 系统需要更新可变状态以及使用相应状态的视图。

在以下示例中,CustomViewGroup 包含 TextView 以及内含 TextField 可组合项的 ComposeViewTextView 需要显示用户在 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
    }
}