From 5f6da1d7d4200e6a8aa6fd5aa438af705c19c790 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 7 Jan 2023 19:30:56 +0100 Subject: [PATCH] Move all functionality to viewmodel. Use simple button format. --- app/build.gradle | 1 + .../me/zobrist/tichucounter/domain/Tichu.kt | 1 - .../ui/counter/CounterFragment.kt | 2 - .../tichucounter/ui/counter/Keyboard.kt | 238 ++++++------------ .../ui/counter/KeyboardViewModel.kt | 122 ++++++++- .../me/zobrist/tichucounter/TichuUnitTest.kt | 11 +- 6 files changed, 192 insertions(+), 183 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 02b4465..29a0b57 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.compose.material:material-icons-extended:1.3.1' implementation 'androidx.activity:activity-compose:1.3.1' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt b/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt index 3d8dc77..06607eb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/Tichu.kt @@ -1,6 +1,5 @@ package me.zobrist.tichucounter.domain -import me.zobrist.tichucounter.data.Round import javax.inject.Inject class Tichu @Inject constructor() { diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterFragment.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterFragment.kt index 3268251..675dc6e 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterFragment.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterFragment.kt @@ -13,8 +13,6 @@ import kotlinx.coroutines.launch import me.zobrist.tichucounter.R import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.databinding.FragmentCounterBinding -import me.zobrist.tichucounter.domain.Tichu -import me.zobrist.tichucounter.domain.getAbsoluteDifference import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.ui.FragmentBase import javax.inject.Inject diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/Keyboard.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/Keyboard.kt index 314f142..da0b54b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/Keyboard.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/Keyboard.kt @@ -1,6 +1,5 @@ package me.zobrist.tichucounter.ui.counter -import android.icu.number.FormattedNumber import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,8 +7,14 @@ import android.view.ViewGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Backspace +import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -18,16 +23,13 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.fragment.app.activityViewModels -import androidx.lifecycle.viewModelScope import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import me.zobrist.tichucounter.R import me.zobrist.tichucounter.databinding.FragmentKeyboardBinding -import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.ui.FragmentBase -import me.zobrist.tichucounter.ui.history.IHistoryFragmentViewModel @AndroidEntryPoint class Keyboard : FragmentBase() { @@ -38,28 +40,12 @@ class Keyboard : FragmentBase() { private val viewModel: KeyboardViewModel by activityViewModels() - private val requester = FocusRequester() - private var isAFocused: Boolean = false - private var isBFocused: Boolean = false - - var enableSubmit = mutableStateOf(false) - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - snapshotFlow { viewModel.scoreA.value } - .onEach { - enableSubmitOnValidRound(viewModel) - } - - snapshotFlow { viewModel.scoreB.value } - .onEach { - enableSubmitOnValidRound(viewModel) - } - return ComposeView(requireContext()).apply { // Dispose of the Composition when the view's LifecycleOwner // is destroyed @@ -72,110 +58,6 @@ class Keyboard : FragmentBase() { } } - private fun enableSubmitOnValidRound(viewModel: IKeyboardViewModel) { - enableSubmit.value = try { - val tichu = Tichu() - if(tichu.isValidRound(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt())) - { - true - } - false - } catch(_:Exception) - { - false - } - } - - private fun giveFocusToAIfNone() { - if(!isAFocused && !isBFocused) - { - requester.requestFocus() - } - } - - private fun appendToFocusedScore(toAppend: String, viewModel: IKeyboardViewModel) { - val value = getActiveValue(viewModel) - value.value += toAppend - updateOtherScore(viewModel) - } - - private fun updateOtherScore(viewModel: IKeyboardViewModel) { - val value = getActiveValue(viewModel) - - try { - val tichu = Tichu() - val myScore = value.value.toInt() - val hisScore = tichu.calculateOtherScore(myScore) - if(tichu.isValidRound(myScore, hisScore)) - { - updateInactiveValue(hisScore?.toString() ?: "", viewModel) - } else - { - updateInactiveValue("", viewModel) - } - } catch(_: Exception) { - updateInactiveValue("", viewModel) - } - } - - private fun negateActiveInput(viewModel: IKeyboardViewModel) { - val value = getActiveValue(viewModel) - - if(value.value.contains("-")) - { - value.value = value.value.replace("-", "") - } else { - value.value = "-" + value.value - } - - updateOtherScore(viewModel) - } - - private fun addToActiveInput(toAdd: Int, viewModel: IKeyboardViewModel) { - val value = getActiveValue(viewModel) - - try { - val temp = value.value.toInt() + toAdd - value.value = temp.toString() - } catch (e: Exception) { - value.value = toAdd.toString() - } - } - - private fun removeFromActiveInput(viewModel: IKeyboardViewModel) { - val value = getActiveValue(viewModel) - if(value.value != "") { - value.value = value.value.dropLast(1) - } - updateOtherScore(viewModel) - } - - private fun getActiveValue(viewModel: IKeyboardViewModel): MutableState { - giveFocusToAIfNone() - if (isBFocused) { - return viewModel.scoreB - } - return viewModel.scoreA - } - - private fun updateInactiveValue(value: String, viewModel: IKeyboardViewModel){ - giveFocusToAIfNone() - if (isBFocused) { - viewModel.scoreA.value = value - } else - { - viewModel.scoreB.value = value - } - - try { - val tichu = Tichu() - enableSubmit.value = tichu.isValidRound(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt()) - } catch(_: java.lang.NumberFormatException) { - enableSubmit.value = false - } - } - - @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Preview @Composable @@ -195,9 +77,9 @@ class Keyboard : FragmentBase() { modifier = Modifier .onFocusChanged { keyboardController?.hide() - isAFocused = it.isFocused + viewModel.isAFocused.value = it.isFocused } - .focusRequester(requester) + .focusRequester(viewModel.requestFocusA.value) .weight(1f), colors = TextFieldDefaults.textFieldColors( cursorColor = Color.Transparent, @@ -212,7 +94,7 @@ class Keyboard : FragmentBase() { modifier = Modifier .onFocusChanged { keyboardController?.hide() - isBFocused = it.isFocused + viewModel.isBFocused.value = it.isFocused } .weight(1f), colors = TextFieldDefaults.textFieldColors( @@ -224,70 +106,79 @@ class Keyboard : FragmentBase() { } Row { - Button( - onClick = { appendToFocusedScore("1", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("1") }, modifier = Modifier.weight(1F) ) { Text("1") } - Button( - onClick = { appendToFocusedScore("2", viewModel ) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("2") }, modifier = Modifier.weight(1F) ) { Text("2") } - Button( - onClick = { appendToFocusedScore("3", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("3") }, modifier = Modifier.weight(1F) ) { Text("3") } - Button(onClick = { addToActiveInput(100, viewModel) }, modifier = Modifier.weight(1F)) { Text("+100") } + TextButton( + onClick = { viewModel.addToActiveInput(100) }, + modifier = Modifier.weight(1F) + ) { Text("+100") } } Row { - Button( - onClick = { appendToFocusedScore("4", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("4") }, modifier = Modifier.weight(1F) ) { Text("4") } - Button( - onClick = { appendToFocusedScore("5", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("5") }, modifier = Modifier.weight(1F) ) { Text("5") } - Button( - onClick = { appendToFocusedScore("6", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("6") }, modifier = Modifier.weight(1F) ) { Text("6") } - Button(onClick = { addToActiveInput(-100, viewModel) }, modifier = Modifier.weight(1F)) { Text("-100") } + TextButton( + onClick = { viewModel.addToActiveInput(-100) }, + modifier = Modifier.weight(1F) + ) { Text("-100") } } Row { - Button( - onClick = { appendToFocusedScore("7", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("7") }, modifier = Modifier.weight(1F) ) { Text("7") } - Button( - onClick = { appendToFocusedScore("8", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("8") }, modifier = Modifier.weight(1F) ) { Text("8") } - Button( - onClick = { appendToFocusedScore("9", viewModel) }, + TextButton( + onClick = { viewModel.appendToFocusedScore("9") }, modifier = Modifier.weight(1F) ) { Text("9") } - Button(onClick = { removeFromActiveInput(viewModel) }, modifier = Modifier.weight(1F)) { Text("DEL") } + IconButton( + onClick = { viewModel.removeLastCharFromActive() }, + modifier = Modifier.weight(1F) + ) { Icon(Icons.Outlined.Backspace, stringResource(R.string.submit)) } } Row { - Button(onClick = { negateActiveInput(viewModel) }, modifier = Modifier.weight(1F)) { Text("+/-") } - Button( - onClick = { appendToFocusedScore("0", viewModel) }, + TextButton( + onClick = { viewModel.negateActiveInput() }, + modifier = Modifier.weight(1F) + ) { Text("+/-") } + TextButton( + onClick = { viewModel.appendToFocusedScore("0") }, modifier = Modifier.weight(1F) ) { Text("0") } Spacer(modifier = Modifier.weight(1F)) - Button(onClick = { submit(viewModel) }, modifier = Modifier.weight(1F), enabled = enableSubmit.value) { Text("ENTER") } + IconButton( + onClick = { viewModel.submitScore() }, + modifier = Modifier.weight(1F), + enabled = viewModel.enableSubmit.value + ) { Icon(Icons.Outlined.Check, stringResource(R.string.submit)) } } } } - private fun submit(viewModel: IKeyboardViewModel) { - viewModel.submitScore(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt()) - viewModel.scoreA.value = "" - viewModel.scoreB.value = "" - enableSubmit.value = false - } - object EmptyTextToolbar : TextToolbar { override val status: TextToolbarStatus = TextToolbarStatus.Hidden @@ -306,8 +197,29 @@ class Keyboard : FragmentBase() { internal class DefaultViewModel : IKeyboardViewModel { override var scoreA: MutableState = mutableStateOf("") override var scoreB: MutableState = mutableStateOf("") + override var enableSubmit = mutableStateOf(false) + override var isAFocused = mutableStateOf(false) + override var isBFocused = mutableStateOf(false) + override var requestFocusA = mutableStateOf(FocusRequester()) - override fun submitScore(scoreA: Int, scoreB: Int) { + override fun appendToFocusedScore(toAppend: String) { + TODO("Not yet implemented") + } + + override fun negateActiveInput() { + TODO("Not yet implemented") + } + + override fun addToActiveInput(toAdd: Int) { + TODO("Not yet implemented") + } + + override fun removeLastCharFromActive() { + TODO("Not yet implemented") + } + + override fun submitScore() { + TODO("Not yet implemented") } } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardViewModel.kt index c3a8b4f..90ad766 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardViewModel.kt @@ -1,16 +1,11 @@ package me.zobrist.tichucounter.ui.counter import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.repository.GameRepository @@ -19,19 +14,124 @@ import javax.inject.Inject interface IKeyboardViewModel { var scoreA: MutableState var scoreB: MutableState - fun submitScore(scoreA: Int, scoreB: Int) + var enableSubmit: MutableState + var isAFocused: MutableState + var isBFocused: MutableState + var requestFocusA: MutableState + + fun appendToFocusedScore(toAppend: String) + + fun negateActiveInput() + + fun addToActiveInput(toAdd: Int) + + fun removeLastCharFromActive() + + fun submitScore() } @HiltViewModel class KeyboardViewModel @Inject constructor(private val gameRepository: GameRepository) : ViewModel(), IKeyboardViewModel { - override var scoreA = mutableStateOf( "") + override var scoreA = mutableStateOf("") override var scoreB = mutableStateOf("") + override var enableSubmit = mutableStateOf(false) + override var isAFocused = mutableStateOf(false) + override var isBFocused = mutableStateOf(false) + override var requestFocusA = mutableStateOf(FocusRequester()) - - override fun submitScore(scoreA: Int, scoreB: Int) { + override fun submitScore() { viewModelScope.launch { - gameRepository.addRoundToActiveGame(scoreA, scoreB) + gameRepository.addRoundToActiveGame(scoreA.value.toInt(), scoreB.value.toInt()) + } + scoreA.value = "" + scoreB.value = "" + enableSubmit.value = false + } + + override fun appendToFocusedScore(toAppend: String) { + val value = getActiveValue() + value.value += toAppend + updateOtherScore() + } + + override fun negateActiveInput() { + val value = getActiveValue() + + if (value.value.contains("-")) { + value.value = value.value.replace("-", "") + } else { + value.value = "-" + value.value + } + + updateOtherScore() + } + + override fun addToActiveInput(toAdd: Int) { + val value = getActiveValue() + + try { + val temp = value.value.toInt() + toAdd + value.value = temp.toString() + } catch (e: Exception) { + value.value = toAdd.toString() } } + + override fun removeLastCharFromActive() { + val value = getActiveValue() + if (value.value != "") { + value.value = value.value.dropLast(1) + } + updateOtherScore() + } + + + private fun giveFocusToAIfNone() { + if (!isAFocused.value && !isBFocused.value) { + requestFocusA.value.requestFocus() + } + } + + private fun updateOtherScore() { + val value = getActiveValue() + + try { + val tichu = Tichu() + val myScore = value.value.toInt() + val hisScore = tichu.calculateOtherScore(myScore) + if (tichu.isValidRound(myScore, hisScore)) { + updateInactiveValue(hisScore?.toString() ?: "") + } else { + updateInactiveValue("") + } + } catch (_: Exception) { + updateInactiveValue("") + } + } + + private fun getActiveValue(): MutableState { + giveFocusToAIfNone() + if (isBFocused.value) { + return scoreB + } + return scoreA + } + + private fun updateInactiveValue(value: String) { + giveFocusToAIfNone() + if (isBFocused.value) { + scoreA.value = value + } else { + scoreB.value = value + } + + try { + val tichu = Tichu() + enableSubmit.value = tichu.isValidRound(scoreA.value.toInt(), scoreB.value.toInt()) + } catch (_: java.lang.NumberFormatException) { + enableSubmit.value = false + } + } + } \ No newline at end of file diff --git a/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt b/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt index 8004796..c42f7e5 100644 --- a/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt +++ b/app/src/test/java/me/zobrist/tichucounter/TichuUnitTest.kt @@ -1,6 +1,5 @@ package me.zobrist.tichucounter -import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.domain.Tichu import org.junit.Assert.* import org.junit.Test @@ -37,13 +36,13 @@ class TichuUnitTest { assertGeneratedRound(tichu, 400, 0) //Good rounds trough Tichu - assertValidRound(tichu, 0, 0) - assertValidRound(tichu, -100, 0) + assertValidRound(tichu, 0, 0) + assertValidRound(tichu, -100, 0) //Bad rounds - assertInvalidRound(tichu, 5, 12) - assertInvalidRound(tichu, 12, 5) - assertInvalidRound(tichu, 5, 55) + assertInvalidRound(tichu, 5, 12) + assertInvalidRound(tichu, 12, 5) + assertInvalidRound(tichu, 5, 55) } private fun assertGeneratedRound(tichu: Tichu, scoreA: Int, expectedScoreB: Int) {