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

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

Compose 中的 ViewModel

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

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

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

状态可信来源

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

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

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

如需了解详情,请参阅附带效应文档。

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