今天會建立詳細頁的 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()
}
由於畫面一打開就需要資料,我們會在畫面初始化時呼叫 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("儲存")
}
}
}
}
編輯畫面
新增畫面
以上為今天的主題,明天預計建立Room。