update to MVVM

This commit is contained in:
moon 2026-06-10 21:57:14 +06:30
parent 582b08cead
commit 3a63c65e8f
3 changed files with 71 additions and 38 deletions

View File

@ -25,15 +25,15 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mob.utsmyanmar.ui.components.NumericKeypad import com.mob.utsmyanmar.ui.components.NumericKeypad
import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P2Preview
@ -52,8 +52,6 @@ private val passwords = mapOf(
PasswordType.SETTLEMENT to "123456" PasswordType.SETTLEMENT to "123456"
) )
private const val PASSWORD_LENGTH = 6
private val passwordKeys = listOf( private val passwordKeys = listOf(
listOf("1", "2", "3"), listOf("1", "2", "3"),
listOf("4", "5", "6"), listOf("4", "5", "6"),
@ -65,11 +63,13 @@ private val passwordKeys = listOf(
fun InputPassword( fun InputPassword(
passwordType: String, passwordType: String,
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onPasswordCorrect: () -> Unit = {} onPasswordCorrect: () -> Unit = {},
viewModel: PasswordInputViewModel = viewModel()
) { ) {
val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!! val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!!
var input by remember { mutableStateOf("") } val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var errorMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) { viewModel.reset() }
Scaffold( Scaffold(
topBar = { topBar = {
@ -85,6 +85,7 @@ fun InputPassword(
) { ) {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
// Display area
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -92,11 +93,8 @@ fun InputPassword(
) { ) {
Card( Card(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.TopEnd)
.clickable(enabled = input.isNotEmpty()) { .clickable(enabled = uiState.canDelete, onClick = viewModel::onDelete),
input = input.dropLast(1)
errorMessage = null
},
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White), colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
@ -104,7 +102,7 @@ fun InputPassword(
Icon( Icon(
imageVector = Icons.Rounded.Backspace, imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete", contentDescription = "Delete",
tint = if (input.isNotEmpty()) Color.LegacyRed else Color.Gray, tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
) )
} }
@ -114,23 +112,19 @@ fun InputPassword(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(text = "Enter Password", color = Color.Gray, fontSize = 18.sp)
text = "Enter Password",
color = Color.Gray,
fontSize = 18.sp
)
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
PasswordDots(filledCount = input.length, totalCount = PASSWORD_LENGTH) PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
text = errorMessage ?: "", text = uiState.errorMessage ?: "",
color = Color.LegacyRed, color = Color.LegacyRed,
fontSize = 14.sp fontSize = 14.sp
) )
} }
} }
// Keypad — mirrors InputAmountScreen weight(3f) section // Keypad — stable lambda: viewModel::onKey never changes between recompositions
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -140,18 +134,13 @@ fun InputPassword(
NumericKeypad( NumericKeypad(
keys = passwordKeys, keys = passwordKeys,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onKeyClick = { digit -> onKeyClick = viewModel::onKey
if (input.length < PASSWORD_LENGTH) {
input += digit
errorMessage = null
}
}
) )
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
// Action buttons — mirrors InputAmountScreen weight(0.5f) section // Action buttons
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -174,16 +163,9 @@ fun InputPassword(
} }
Button( Button(
onClick = { onClick = { viewModel.validate(expectedPassword, onPasswordCorrect) },
if (input == expectedPassword) {
onPasswordCorrect()
} else {
errorMessage = "Incorrect password"
input = ""
}
},
modifier = Modifier.weight(1f).height(56.dp), modifier = Modifier.weight(1f).height(56.dp),
enabled = input.length == PASSWORD_LENGTH, enabled = uiState.isConfirmEnabled,
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed, containerColor = Color.LegacyRed,

View File

@ -0,0 +1,11 @@
package com.mob.utsmyanmar.ui.password_input
internal const val PASSWORD_LENGTH = 6
data class PasswordInputUiState(
val input: String = "",
val errorMessage: String? = null
) {
val isConfirmEnabled: Boolean get() = input.length == PASSWORD_LENGTH
val canDelete: Boolean get() = input.isNotEmpty()
}

View File

@ -0,0 +1,40 @@
package com.mob.utsmyanmar.ui.password_input
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class PasswordInputViewModel : ViewModel() {
private val _uiState = MutableStateFlow(PasswordInputUiState())
val uiState: StateFlow<PasswordInputUiState> = _uiState.asStateFlow()
fun onKey(digit: String) {
_uiState.update { state ->
if (state.input.length < PASSWORD_LENGTH)
state.copy(input = state.input + digit, errorMessage = null)
else
state
}
}
fun onDelete() {
_uiState.update { state ->
state.copy(input = state.input.dropLast(1), errorMessage = null)
}
}
fun validate(expectedPassword: String, onCorrect: () -> Unit) {
if (_uiState.value.input == expectedPassword) {
onCorrect()
} else {
_uiState.update { it.copy(errorMessage = "Incorrect password", input = "") }
}
}
fun reset() {
_uiState.value = PasswordInputUiState()
}
}