iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Mobile Development

從零開始學習 Jetpack Compose系列 第 27

從零開始學習 Jetpack Compose Day26 - 詳細頁ViewModel建立

  • 分享至 

  • xImage
  •  

今天會建立詳細頁的 ViewModel 。

畫面狀態建立

這邊我們會定義畫面的幾個狀態,根據不同狀態來顯示不同畫面。

Empty:表示初始狀態
Create :表示新增
Edit :表示編輯
Error:顯示錯誤畫面

sealed class DetailState {
    data object Empty: DetailState()
    data object Create: DetailState()
    data object Error: DetailState()
    data class Edit(val data: SubscriptionViewData): DetailState()
}

ViewModel 建立

由於畫面一打開就需要資料,我們會在畫面初始化時呼叫 getData(),然後依據當前的操作來判斷是「新增」還是「編輯」模式。畫面內容會根據這個判斷來顯示不同的 UI,例如新增時顯示空白的輸入表單,編輯時則預填現有資料。

class DetailViewModel(private val appDataManager: AppDataManager) : BaseViewModel() {

    companion object {
        fun createFactory(appDataManager: AppDataManager) = object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
                return DetailViewModel(appDataManager) as T
            }
        }
    }

    val state: StateFlow<DetailState>
        get() = mState

    private var mState = MutableStateFlow<DetailState>(DetailState.Empty)


    fun getData(id: String? = null) {
        MainScope().launch {
            if (id.isNullOrEmpty()) {
                mState.value = DetailState.Create
            } else {
                val list = appDataManager.getData()
                val item = list.find { it.id == id }
                if (item == null) {
                    mState.value = DetailState.Error
                } else {
                    mState.value = DetailState.Edit(item)
                }
            }

        }
    }

    fun saveData(data: SubscriptionViewData) {
        if (data.id.isEmpty()) {
            MainScope().launch {
                appDataManager.setData(data)
            }
        } else {
            MainScope().launch {
                appDataManager.updateData(data)
            }
        }
    }
}

畫面調整

由於編輯和新增畫面相同,只差在是否有預設資料,因此我們將畫面抽取為共用的 DetailForm。透過傳遞參數來判斷是否有預設資料,並根據此參數顯示對應的內容。同時,我們新增一個按鈕,用來通知 ViewModel 執行資料更新,無論是新增還是編輯模式都可以共用這個流程。

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(
    viewModel: DetailViewModel = viewModel(factory = DetailViewModel.createFactory((LocalContext.current.applicationContext as AppApplication).getAppDataManager())),
    productId: String,
    navController: NavController? = null,
    modifier: Modifier = Modifier
) {
    val state by viewModel.state.collectAsState()
    viewModel.getData(productId)
    Scaffold(
        topBar = {
            CenterAlignedTopAppBar(
                title = {
                    Text("產品")
                },
                navigationIcon = {
                    IconButton(onClick = {
                        navController?.popBackStack()
                    }) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = ""
                        )
                    }
                },
                actions = {

                }
            )
        }
    ) { innerPadding ->
        when (state) {
            is DetailState.Empty -> {}
            is DetailState.Create -> {
                Column(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxWidth()
                        .fillMaxHeight()
                ) {
                    DetailForm(null, onProductValueChange = {
                        viewModel.saveData(it)
                    })
                }
            }
            is DetailState.Edit -> {
                val data = (state as DetailState.Edit).data
                Column(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxWidth()
                        .fillMaxHeight()
                ) {
                    DetailForm(data, onProductValueChange = {
                        viewModel.saveData(it)
                    })
                }

            }
            is DetailState.Error -> {}
        }

    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailForm(item: SubscriptionViewData? = null, onProductValueChange: (data: SubscriptionViewData) -> Unit) {
    Column {
        val options = listOf("月訂閱", "年訂閱", "一次性購買")
        var selectedOptionText by remember { mutableStateOf(item?.cycle ?: options[0]) }
        var product by remember { mutableStateOf(item?.name ?: "") }
        var price by remember { mutableStateOf(item?.price ?: "") }
        var expanded by remember { mutableStateOf(false) }

        OutlinedTextField(
            value = product,
            onValueChange = {product = it},
            label = {
                Text("訂閱產品")
            },
            leadingIcon = {
                Icon(
                    imageVector = Icons.Outlined.Face,
                    contentDescription = ""
                )
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(10.dp)
        )

        OutlinedTextField(
            value = price,
            onValueChange = {price = it},
            label = {
                Text("價格")
            },
            prefix = {
                Text("$")
            },
            leadingIcon = {
                Icon(
                    imageVector = Icons.Outlined.Face,
                    contentDescription = ""
                )
            },
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            modifier = Modifier
                .fillMaxWidth()
                .padding(10.dp)
        )

        ExposedDropdownMenuBox(
            expanded = expanded,
            onExpandedChange = {
                expanded = !expanded
            }
        ) {
            OutlinedTextField(
                value = selectedOptionText,
                readOnly = true,
                onValueChange = {selectedOptionText = it},
                label = {
                    Text("訂閱類型")
                },
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Outlined.Face,
                        contentDescription = ""
                    )
                },
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(10.dp)
                    .menuAnchor(),
                trailingIcon = {
                    ExposedDropdownMenuDefaults.TrailingIcon(
                        expanded = expanded
                    )
                },
            )
            ExposedDropdownMenu(
                modifier = Modifier.fillMaxWidth(),
                expanded = expanded,
                onDismissRequest = {
                    expanded = false
                }
            ) {
                options.forEach { selectionOption ->
                    DropdownMenuItem(
                        text = {
                            Text(text = selectionOption)
                        },
                        onClick = {
                            selectedOptionText = selectionOption
                            expanded = false
                        }
                    )
                }
            }
        }
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(
                onClick = {
                    val newData = SubscriptionViewData(
                        id = item?.id ?: "",
                        name = product,
                        price = price,
                        cycle = selectedOptionText
                    )
                    onProductValueChange.invoke(newData)
                }
            ) {
                Text("儲存")
            }
        }
    }
}

編輯畫面
https://raw.githubusercontent.com/jian-fu-hung/ithelp-2024/refs/heads/main/Images/Day26/%E6%88%AA%E5%9C%96%202024-10-11%20%E6%99%9A%E4%B8%8A11.41.35.png
https://raw.githubusercontent.com/jian-fu-hung/ithelp-2024/refs/heads/main/Images/Day26/%E6%88%AA%E5%9C%96%202024-10-11%20%E6%99%9A%E4%B8%8A11.41.44.png

新增畫面
https://raw.githubusercontent.com/jian-fu-hung/ithelp-2024/refs/heads/main/Images/Day26/%E6%88%AA%E5%9C%96%202024-10-11%20%E6%99%9A%E4%B8%8A11.42.04.png
https://raw.githubusercontent.com/jian-fu-hung/ithelp-2024/refs/heads/main/Images/Day26/%E6%88%AA%E5%9C%96%202024-10-11%20%E6%99%9A%E4%B8%8A11.42.10.png

以上為今天的主題,明天預計建立Room。


上一篇
從零開始學習 Jetpack Compose Day25 - 專案實作(3)主畫面 ViewModel 建立
下一篇
從零開始學習 Jetpack Compose Day27 - 專案實作(5)Room建立
系列文
從零開始學習 Jetpack Compose30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言