Reputation: 51
I'm new to Android and I was making my hands dirty with Jetpack Compose. My first assignment is to develop a Wordle-like game where a user has to find out a hidden work in N number of guesses.
println
, shows that state is updated with the desired new value but OutlinedTextField sometimes how this data and not most other time.Following is the data class that holds all the states of the game.
// state of the Wordle Game
data class WordleState(
val currentGuess: ArrayList<CharacterToGuess>, //holds the word being guessed, on value change, UI is supposed to get recompose
var gameState: GameState = GameState.ONGOING,
val wordToGuess: String = getWordFromWordBank(),
var remainingAttempts: Int = MAX_GUESSES_ALLOWED,
/*var currentGuess: MutableList<MutableList<String>> = MutableList(MAX_GUESSES_ALLOWED) {
MutableList(MAX_LETTERS_LENGTH) {""}
},*/
var selectedRowIndex: Int = 0
) {
companion object {
fun getWordFromWordBank(): String { //TODO: move to the WordFactory
val wordBank: List<String> = mutableListOf("AUDIO", "VIDEO", "KITES" )
return wordBank[Random.nextInt(0, wordBank.size-1)]
}
var MAX_GUESSES_ALLOWED: Int = 1
var MAX_LETTERS_LENGTH: Int = 5
}
override fun equals(other: Any?): Boolean { //Android studio suggested this override
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WordleState
if (gameState != other.gameState) return false
if (wordToGuess != other.wordToGuess) return false
if (remainingAttempts != other.remainingAttempts) return false
if (!currentGuess.contentEquals(other.currentGuess)) return false
if (selectedRowIndex != other.selectedRowIndex) return false
return true
}
override fun hashCode(): Int { //Android studio suggested this override
var result = gameState.hashCode()
result = 31 * result + wordToGuess.hashCode()
result = 31 * result + remainingAttempts
result = 31 * result + currentGuess.contentHashCode()
result = 31 * result + selectedRowIndex
return result
}
}
ViewModel code that gets called on OutlinedTextField valueChange
as follows,
class WordleViewModel: ViewModel() {
private val _wordleState = MutableLiveData(WordleState())
var state: LiveData<WordleState> = _wordleState
...
fun updateCurrentGuessLetter(it: String, col: Int) {
val newGuessLetter = it.first()
println("onValueChange before: " +
"Letter:`$it`" +
" currentGuess:${state.value!!.currentGuess.joinToString(separator = "")}," +
" states:${state.value}")
val updatedGuess = _wordleState.value!!.copy().currentGuess
updatedGuess[col] = newGuessLetter
_wordleState.value = _wordleState.value!!.copy(
currentGuess = updatedGuess
)
println("onValueChange after: " +
"Letter:`$it`" +
" currentGuess:${state.value!!.currentGuess.joinToString(separator = "")}," +
" states:${state.value}")
}
...
}
MainActivity.kt code as follows,
fun WordleGrid(viewModel: WordleViewModel, state: WordleState) {
val context = LocalContext.current.applicationContext
val focusManager = LocalFocusManager.current
Column(modifier = Modifier.fillMaxWidth()) {
for (row in 0 until WordleState.MAX_GUESSES_ALLOWED) {
Row(modifier = Modifier
.fillMaxWidth()
.height(50.dp)) {
for (col in 0 until WordleState.MAX_LETTERS_LENGTH) {
MyOutlinedTextField(
modifier = Modifier.weight(1f),
viewModel = viewModel,
state = state,
row = row, col = col
)
}
}
}
CheckButton(
onClick = { viewModel.onSubmitWord() }
)
}
}
@Composable
fun MyOutlinedTextField(
modifier: Modifier,
viewModel: WordleViewModel,
state: WordleState,
row: Int,
col: Int,
) {
val isEnabled = (row == state.selectedRowIndex) //state.currentGuess[col].toString(),
val focusManager = LocalFocusManager.current
OutlinedTextField(
//value = state.currentGuess[row][col], //for state as 2D string array
value = state.currentGuess[col].toString(),
enabled = isEnabled,
onValueChange = {
if (it.isNotBlank() && it.isNotEmpty()) {
//viewModel.updateCurrentGuessLetter(newLetter.first().toString(), row, col)
viewModel.updateCurrentGuessLetter(it, col)
if(col == WordleState.MAX_LETTERS_LENGTH - 1) {
viewModel.onSubmitWord()
} else {
focusManager.moveFocus(FocusDirection.Right)
}
} else {
println("onValueChange: Ignoring blank/empty letter")
}
},
singleLine = true,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.then(modifier),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.DarkGray
),
textStyle = TextStyle(
color = Color.Red,
textAlign = TextAlign.Center,
//fontSize = 28.sp,
),
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Characters,
autoCorrect = false,
keyboardType = KeyboardType.Text,
/*imeAction = (
if (col == WordleState.MAX_LETTERS_LENGTH - 1) ImeAction.Done
else ImeAction.Next
)*/
),
keyboardActions = KeyboardActions(
onNext = {
// Move focus to the next outlined text field
focusManager.moveFocus(FocusDirection.Right)
},
onDone = {
// Submit the word when the last outlined text field is completed
viewModel.onSubmitWord()
}
),
)
}
Let me know if any more details need to be added. Appreciate your time and efforts for this help.
Upvotes: 1
Views: 88
Reputation: 51
As @Tenfour04 mentioned the issue was
Here is the modified code for the references -
fun WordleGrid(viewModel: WordleViewModel, state: WordleState) {
//val context = LocalContext.current.applicationContext
val focusManager = LocalFocusManager.current
Column(modifier = Modifier.fillMaxWidth()) {
for (row in 0 until WordleState.MAX_GUESSES_ALLOWED) {
Row(modifier = Modifier
.fillMaxWidth()
.height(75.dp)
) {
for (col in 0 until WordleState.MAX_LETTERS_LENGTH) {
val fieldValue = state.currentGuess[row][col]
val backgroundColorValue = state.backgroundColor[row][col]
val fontColorValue = state.fontColor[row][col]
val isEnabled = (row == state.selectedRowIndex)
println("Row:$row, Column:$col, isEnabled:$isEnabled")
val modifier = Modifier
.weight(1f)
.fillMaxSize()
.padding(2.dp)
.background(color = backgroundColorValue /*if(state.selectedRowIndex<=row) {Color.White} else {Color.DarkGray}*/)
val onValueChange = { it: String ->
println("MyOutlinedTextField before: " +
"old letter:`${viewModel.state.value!!.currentGuess[row][col]}`, " +
"new letter: `$it`, row:`$row`, col:`$col`, isEnabled:$isEnabled`")
if (it.isNotBlank() && it.isNotEmpty()) {
viewModel.onValueChange(it, row, col)
if(col != WordleState.MAX_LETTERS_LENGTH - 1) {
focusManager.moveFocus(FocusDirection.Right)
}
} else {
println("MyOutlinedTextField-onValueChange: Ignoring blank/empty letter")
}
}
.
.
.
.
}
MyOutlinedTextField(
value = fieldValue,
onValueChange = onValueChange,
isEnabled = isEnabled,
modifier = modifier,
colors = colors,
textStyle = textStyle,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
viewModel = viewModel
)
ViewModel Code
fun onValueChange(newGuessLetter: String, row: Int, col: Int) {
//create new copy of the MutableList and then update the content
val tempCurrentGuess = _wordleState.value!!.currentGuess
.map {
outerIt -> outerIt.map { it }.toMutableList()
}.toMutableList()
tempCurrentGuess[row][col] = newGuessLetter
_wordleState.value = _wordleState.value!!.copy(
currentGuess = tempCurrentGuess
)
}
Upvotes: 0
Reputation: 93834
There's no reason to override equals
or hashcode
in your data class. They are already automatically going to include all the properties in your constructor. By overriding them, you are only inviting the possibility of getting them out of sync with your properties design, thereby breaking the class. You didn't show your code for CharacterToGuess
, but the same is true for that.
Having any var
properties in your class that represents State will break your functionality. Compose State has to be able to compare old and new instances of a class to determine if state has changed. But if you modify a mutable property, the old and new state are the same instance and Compose will consider the state unchanged. This prevents re-composition from happening when it should.
Other than those problems, just a comment on your design of MyOutlinedTextField()
. It's kind of a mess of spaghetti. It has a view model parameter, then a WorldState parameter which is something it could also retrieve from that view model, then row and column, which is something that from the point of view of a specific UI element should be irrelevant. You can hoist all of that stuff out of there and just pass it a Char and a Color. The parent can determine the Char and Color. That would be a cleaner and more robust design.
I didn't check the logic of how you game works.
Upvotes: 2