整合 Compose 與現有的應用程式架構

單向資料流程 (UDF) 架構模式可與 Compose 完美搭配運作。不過,如果應用程式使用 Model View Presenter (MVP) 等其他類型的架構模式,建議您在使用 Compose 之前或使用期間,將該部分的 UI 遷移至 UDF。

Compose 中的 ViewModel

如果您使用架構元件 ViewModel 程式庫,可以呼叫 viewModel() 函式以存取任何可組合項中的 ViewModel。想瞭解如何將 Compose 與常用程式庫整合,請參閱這份說明文件

如果採用 Compose,在不同可組合項中使用相同的 ViewModel 類型時請務必謹慎,因為 ViewModel 元件會按照 View 生命週期的範圍調整。如果使用導覽資料庫,範圍將為代管活動、片段或導覽圖。

舉例來說,如果可組合項在活動中代管,則 viewModel() 一律會回傳只在活動完成時清除的相同執行個體。在以下範例中,系統會歡迎相同的使用者 ("user1") 兩次,原因在於代管活動下的所有可組合項重複使用了相同的 GreetingViewModel 執行個體。第一個建立的 ViewModel 執行個體會重複用於其他可組合項。

class GreetingActivity : AppCompatActivity() {
    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") ?: "")
        }
    }
}

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

    Text(messageUser)
}

狀態真實資訊來源

當您在使用者介面的某一部分中採用 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
}

詳情請參閱副作用說明文件

以檢視畫面系統為真實資訊來源

如果 View 系統擁有狀態,並與 Compose 共用,建議您將其狀態納入 mutableStateOf 物件中,讓該程式碼在 Compose 中安全無虞。這個方法可簡化可組合函式,原因在於這些函式不再具有真實資訊來源,但 View 系統必須更新可變動狀態以及使用該狀態的 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
    }
}