【Compose multiplatform教程23】在通用代码中使用视图模型(ViewModel)

使用 Compose 多平台(Compose Multiplatform)可以在通用代码中实现安卓(Android)中那种通过视图模型(ViewModelicon-default.png?t=O83Ahttps://developer.android.com/topic/libraries/architecture/viewmodel)构建用户界面(UI)的方法。

在 Compose 多平台环境中,对通用视图模型(ViewModel)的支持仍处于实验阶段。

Stability of supported platforms | Kotlin Multiplatform Development Documentation

将通用视图模型(ViewModel)添加到你的项目中
要使用多平台的视图模型(ViewModel)实现,需将以下依赖项添加到你的 commonMain 源集中:

kotlin {
    // ...
    sourceSets {
        // ...
        commonMain.dependencies {
            // ...
            implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
        }
        // ...
    }
}

在通用代码中使用视图模型(ViewModel)
Compose 多平台实现了通用的 ViewModelStoreOwner 接口,因此,总体而言,在通用代码中使用 ViewModel 类与安卓(Android)最佳实践并没有太大差异。

以导航示例来说明:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

class OrderViewModel : ViewModel() {
   private val _uiState = MutableStateFlow(OrderUiState(pickupOptions = pickupOptions()))
   val uiState: StateFlow = _uiState.asStateFlow()
   // ...
}

声明 ViewModel 类:

@Composable
fun CupcakeApp(
   viewModel: OrderViewModel = viewModel { OrderViewModel() },
) {
   // ...
}

在视图模型(ViewModel)中运行协程时,要记住 ViewModel.viewModelScope 的值是与 Dispatchers.Main.immediate 的值绑定的,而在桌面端默认情况下,Dispatchers.Main.immediate 可能不可用。

为了使视图模型中的协程能在 Compose 多平台环境下正确运行,需将 kotlinx-coroutines-swing 依赖项添加到你的项目中。详情请参阅 Dispatchers.Main 的相关文档。

在使用 Kotlin 协程时,如何确保在不同平台上正确调度?

除了`kotlinx-coroutines-swing`,还有哪些与协程相关的库适用于 Compose Multiplatform?

在 Compose Multiplatform 中,如何处理与平台相关的差异?

,请参阅 Dispatchers.Main 文档。ViewModelViewModel.viewModelScopeDispatchers.Main.immediatekotlinx-coroutines-swing

在非 JVM 平台上,无法使用类型反射来实例化对象。因此,在通用代码中,你不能无参数地调用 viewModel() 函数:每次创建视图模型(ViewModel)实例时,都至少需要提供一个初始化器作为参数。

如果仅提供了一个初始化器,该库会在底层创建一个默认的工厂。不过,你可以自行实现工厂,并调用更明确版本的通用 viewModel(...) 函数,这与使用 jetpack-composeicon-default.png?t=O83Ahttps://developer.android.com/topic/libraries/architecture/viewmodel#jetpack-compose 时的情况类似。

由于官方示例写的太长了,模拟订单相关的业务逻辑,它管理着订单的各项关键信息(如数量、口味、取货日期等),并且能够基于这些信息来计算订单的总价,同时提供了一系列方法用于更新订单状态以及获取相关的可选信息。以下是效果和代码示例

 【Compose multiplatform教程23】在通用代码中使用视图模型(ViewModel)_第1张图片

 Bean对象

/**
 * Data class that represents the current UI state in terms of [quantity], [flavor],
 * [dateOptions], selected pickup [date] and [price]
 */
data class OrderUiState(
    /** Selected cupcake quantity (1, 6, 12) */
    val quantity: Int = 0,
    /** Flavor of the cupcakes in the order (such as "Chocolate", "Vanilla", etc..) */
    val flavor: String = "",
    /** Selected date for pickup (such as "Jan 1") */
    val date: String = "",
    /** Total price for the order */
    val price: String = "",
    /** Available pickup dates for the order*/
    val pickupOptions: List = listOf()
)

ViewModel  

** Price for a single cupcake */
private const val PRICE_PER_CUPCAKE = 2.00

/** Additional cost for same day pickup of an order */
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00

/**
 * [OrderViewModel] holds information about a cupcake order in terms of quantity, flavor, and
 * pickup date. It also knows how to calculate the total price based on these order details.
 */
class OrderViewModel : ViewModel() {

    /**
     * Cupcake state for this order
     */
    private val _uiState = MutableStateFlow(OrderUiState(pickupOptions = pickupOptions()))
    val uiState: StateFlow = _uiState.asStateFlow()

    /**
     * Set the quantity [numberCupcakes] of cupcakes for this order's state and update the price
     */
    fun setQuantity(numberCupcakes: Int) {
        _uiState.update { currentState ->
            currentState.copy(
                quantity = numberCupcakes,
                price = calculatePrice(quantity = numberCupcakes)
            )
        }
    }

    /**
     * Set the [desiredFlavor] of cupcakes for this order's state.
     * Only 1 flavor can be selected for the whole order.
     */
    fun setFlavor(desiredFlavor: String) {
        _uiState.update { currentState ->
            currentState.copy(flavor = desiredFlavor)
        }
    }

    /**
     * Set the [pickupDate] for this order's state and update the price
     */
    fun setDate(pickupDate: String) {
        _uiState.update { currentState ->
            currentState.copy(
                date = pickupDate,
                price = calculatePrice(pickupDate = pickupDate)
            )
        }
    }

    /**
     * Reset the order state
     */
    fun resetOrder() {
        _uiState.value = OrderUiState(pickupOptions = pickupOptions())
    }

    /**
     * Returns the calculated price based on the order details.
     */
    fun calculatePrice(
        quantity: Int = _uiState.value.quantity,
        pickupDate: String = _uiState.value.date
    ): String {
        var calculatedPrice = quantity * PRICE_PER_CUPCAKE
        // If the user selected the first option (today) for pickup, add the surcharge
        if (pickupOptions()[0] == pickupDate) {
            calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
        }
        return "$calculatedPrice€"
    }

    /**
     * Returns a list of date options starting with the current date and the following 3 dates.
     */
    fun pickupOptions(): List {
        val dateOptions = mutableListOf()
        val now = Clock.System.now()
        val timeZone = TimeZone.currentSystemDefault()
        // add current date and the following 3 dates.
        repeat(4) {
            val day= now.plus(0, DateTimeUnit.DAY,timeZone)
            dateOptions.add(day.toLocalDateTime(timeZone).date.toString())
        }
        return dateOptions
    }
}

UI交互

import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*

import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun OrderScreen() {
    val viewModel: OrderViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = "Cupcake Order", style = MaterialTheme.typography.h5)

        // 选择数量下拉框
        QuantityDropdown(
            quantity = uiState.quantity,
            onQuantitySelected = viewModel::setQuantity
        )

        // 选择口味下拉框
        FlavorDropdown(
            flavor = uiState.flavor,
            onFlavorSelected = viewModel::setFlavor
        )

        // 选择取货日期下拉框
        DateDropdown(
            viewModel,
            date = uiState.date,
            onDateSelected = viewModel::setDate
        )

        // 显示总价
        Text(text = "Total Price: ${uiState.price}", style = MaterialTheme.typography.h6)

        // 重置订单按钮
        Button(
            onClick = viewModel::resetOrder,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Text(text = "Reset Order")
        }
    }
}

// 数量下拉框组件
@Composable
fun QuantityDropdown(
    quantity: Int,
    onQuantitySelected: (Int) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    var selectedQuantity by remember { mutableStateOf(quantity) }

    Column(modifier = Modifier.fillMaxWidth()) {
        TextField(
            value = selectedQuantity.toString(),
            onValueChange = {
                if (it.isNotEmpty()) {
                    selectedQuantity = it.toInt()
                }
            },
            label = { Text(text = "Quantity") },
            readOnly = true,
            trailingIcon = {
                IconButton(onClick = { expanded = true }) {
                    Icon(
                        imageVector = Icons.Default.ArrowDropDown,
                        contentDescription = "Expand Quantity Dropdown"
                    )
                }
            },
            modifier = Modifier.fillMaxWidth()
        )

        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            listOf(1, 6, 12).forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        onQuantitySelected(option)
                        selectedQuantity = option
                        expanded = false
                    }
                ) {
                    Text(text = option.toString())
                }
            }
        }
    }
}

// 选择口味下拉框组件
@Composable
fun FlavorDropdown(
    flavor: String,
    onFlavorSelected: (String) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    var selectedFlavor by remember { mutableStateOf(flavor) }

    Column(modifier = Modifier.fillMaxWidth()) {
        TextField(
            value = selectedFlavor,
            onValueChange = { selectedFlavor = it },
            label = { Text(text = "Flavor") },
            readOnly = true,
            trailingIcon = {
                IconButton(onClick = { expanded = true }) {
                    Icon(
                        imageVector = Icons.Default.ArrowDropDown,
                        contentDescription = "Expand Flavor Dropdown"
                    )
                }
            },
            modifier = Modifier.fillMaxWidth()
        )

        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            listOf("Chocolate", "Vanilla", "Strawberry").forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        onFlavorSelected(option)
                        selectedFlavor = option
                        expanded = false
                    }
                ) {
                    Text(text = option)
                }
            }
        }
    }
}

// 选择取货日期下拉框组件
@Composable
fun DateDropdown(
    viewModel: OrderViewModel,
    date: String,
    onDateSelected: (String) -> Unit
) {
    var expanded by remember { mutableStateOf(false)  }
        var selectedDate by remember { mutableStateOf(date) }

        Column(modifier = Modifier.fillMaxWidth()) {
            TextField(
                value = selectedDate,
                onValueChange = { selectedDate = it },
                label = { Text(text = "Pickup Date") },
                readOnly = true,
                trailingIcon = {
                    IconButton(onClick = { expanded = true }) {
                        Icon(
                            imageVector = Icons.Default.ArrowDropDown,
                            contentDescription = "Expand Date Dropdown"
                        )
                    }
                },
                modifier = Modifier.fillMaxWidth()
            )

            DropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                viewModel.pickupOptions().forEach { option ->
                    DropdownMenuItem(
                        onClick = {
                            onDateSelected(option)
                            selectedDate = option
                            expanded = false
                        }
                    ) {
                        Text(text = option)
                    }
                }
            }
        }
    }

你可能感兴趣的:(Compose,android,多平台,kotlin,前端,框架)