Move all functionality to viewmodel. Use simple button format.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2023-01-07 19:30:56 +01:00
parent ae6210073d
commit 5f6da1d7d4
6 changed files with 192 additions and 183 deletions

View File

@@ -97,6 +97,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.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.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"

View File

@@ -1,6 +1,5 @@
package me.zobrist.tichucounter.domain package me.zobrist.tichucounter.domain
import me.zobrist.tichucounter.data.Round
import javax.inject.Inject import javax.inject.Inject
class Tichu @Inject constructor() { class Tichu @Inject constructor() {

View File

@@ -13,8 +13,6 @@ import kotlinx.coroutines.launch
import me.zobrist.tichucounter.R import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.data.Round
import me.zobrist.tichucounter.databinding.FragmentCounterBinding 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.repository.GameRepository
import me.zobrist.tichucounter.ui.FragmentBase import me.zobrist.tichucounter.ui.FragmentBase
import javax.inject.Inject import javax.inject.Inject

View File

@@ -1,6 +1,5 @@
package me.zobrist.tichucounter.ui.counter package me.zobrist.tichucounter.ui.counter
import android.icu.number.FormattedNumber
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -8,8 +7,14 @@ import android.view.ViewGroup
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester 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.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn import me.zobrist.tichucounter.R
import kotlinx.coroutines.flow.onEach
import me.zobrist.tichucounter.databinding.FragmentKeyboardBinding import me.zobrist.tichucounter.databinding.FragmentKeyboardBinding
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.ui.FragmentBase import me.zobrist.tichucounter.ui.FragmentBase
import me.zobrist.tichucounter.ui.history.IHistoryFragmentViewModel
@AndroidEntryPoint @AndroidEntryPoint
class Keyboard : FragmentBase<FragmentKeyboardBinding>() { class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
@@ -38,28 +40,12 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
private val viewModel: KeyboardViewModel by activityViewModels() 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
snapshotFlow { viewModel.scoreA.value }
.onEach {
enableSubmitOnValidRound(viewModel)
}
snapshotFlow { viewModel.scoreB.value }
.onEach {
enableSubmitOnValidRound(viewModel)
}
return ComposeView(requireContext()).apply { return ComposeView(requireContext()).apply {
// Dispose of the Composition when the view's LifecycleOwner // Dispose of the Composition when the view's LifecycleOwner
// is destroyed // is destroyed
@@ -72,110 +58,6 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
} }
} }
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<String> {
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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Preview @Preview
@Composable @Composable
@@ -195,9 +77,9 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
modifier = Modifier modifier = Modifier
.onFocusChanged { .onFocusChanged {
keyboardController?.hide() keyboardController?.hide()
isAFocused = it.isFocused viewModel.isAFocused.value = it.isFocused
} }
.focusRequester(requester) .focusRequester(viewModel.requestFocusA.value)
.weight(1f), .weight(1f),
colors = TextFieldDefaults.textFieldColors( colors = TextFieldDefaults.textFieldColors(
cursorColor = Color.Transparent, cursorColor = Color.Transparent,
@@ -212,7 +94,7 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
modifier = Modifier modifier = Modifier
.onFocusChanged { .onFocusChanged {
keyboardController?.hide() keyboardController?.hide()
isBFocused = it.isFocused viewModel.isBFocused.value = it.isFocused
} }
.weight(1f), .weight(1f),
colors = TextFieldDefaults.textFieldColors( colors = TextFieldDefaults.textFieldColors(
@@ -224,70 +106,79 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
} }
Row { Row {
Button( TextButton(
onClick = { appendToFocusedScore("1", viewModel) }, onClick = { viewModel.appendToFocusedScore("1") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("1") } ) { Text("1") }
Button( TextButton(
onClick = { appendToFocusedScore("2", viewModel ) }, onClick = { viewModel.appendToFocusedScore("2") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("2") } ) { Text("2") }
Button( TextButton(
onClick = { appendToFocusedScore("3", viewModel) }, onClick = { viewModel.appendToFocusedScore("3") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("3") } ) { 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 { Row {
Button( TextButton(
onClick = { appendToFocusedScore("4", viewModel) }, onClick = { viewModel.appendToFocusedScore("4") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("4") } ) { Text("4") }
Button( TextButton(
onClick = { appendToFocusedScore("5", viewModel) }, onClick = { viewModel.appendToFocusedScore("5") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("5") } ) { Text("5") }
Button( TextButton(
onClick = { appendToFocusedScore("6", viewModel) }, onClick = { viewModel.appendToFocusedScore("6") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("6") } ) { 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 { Row {
Button( TextButton(
onClick = { appendToFocusedScore("7", viewModel) }, onClick = { viewModel.appendToFocusedScore("7") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("7") } ) { Text("7") }
Button( TextButton(
onClick = { appendToFocusedScore("8", viewModel) }, onClick = { viewModel.appendToFocusedScore("8") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("8") } ) { Text("8") }
Button( TextButton(
onClick = { appendToFocusedScore("9", viewModel) }, onClick = { viewModel.appendToFocusedScore("9") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("9") } ) { 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 { Row {
Button(onClick = { negateActiveInput(viewModel) }, modifier = Modifier.weight(1F)) { Text("+/-") } TextButton(
Button( onClick = { viewModel.negateActiveInput() },
onClick = { appendToFocusedScore("0", viewModel) }, modifier = Modifier.weight(1F)
) { Text("+/-") }
TextButton(
onClick = { viewModel.appendToFocusedScore("0") },
modifier = Modifier.weight(1F) modifier = Modifier.weight(1F)
) { Text("0") } ) { Text("0") }
Spacer(modifier = Modifier.weight(1F)) 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 { object EmptyTextToolbar : TextToolbar {
override val status: TextToolbarStatus = TextToolbarStatus.Hidden override val status: TextToolbarStatus = TextToolbarStatus.Hidden
@@ -306,8 +197,29 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
internal class DefaultViewModel : IKeyboardViewModel { internal class DefaultViewModel : IKeyboardViewModel {
override var scoreA: MutableState<String> = mutableStateOf("") override var scoreA: MutableState<String> = mutableStateOf("")
override var scoreB: MutableState<String> = mutableStateOf("") override var scoreB: MutableState<String> = 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")
} }
} }

View File

@@ -1,16 +1,11 @@
package me.zobrist.tichucounter.ui.counter package me.zobrist.tichucounter.ui.counter
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.focus.FocusRequester
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.repository.GameRepository
@@ -19,19 +14,124 @@ import javax.inject.Inject
interface IKeyboardViewModel { interface IKeyboardViewModel {
var scoreA: MutableState<String> var scoreA: MutableState<String>
var scoreB: MutableState<String> var scoreB: MutableState<String>
fun submitScore(scoreA: Int, scoreB: Int) var enableSubmit: MutableState<Boolean>
var isAFocused: MutableState<Boolean>
var isBFocused: MutableState<Boolean>
var requestFocusA: MutableState<FocusRequester>
fun appendToFocusedScore(toAppend: String)
fun negateActiveInput()
fun addToActiveInput(toAdd: Int)
fun removeLastCharFromActive()
fun submitScore()
} }
@HiltViewModel @HiltViewModel
class KeyboardViewModel @Inject constructor(private val gameRepository: GameRepository) : class KeyboardViewModel @Inject constructor(private val gameRepository: GameRepository) :
ViewModel(), IKeyboardViewModel { ViewModel(), IKeyboardViewModel {
override var scoreA = mutableStateOf( "") override var scoreA = mutableStateOf("")
override var scoreB = 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() {
override fun submitScore(scoreA: Int, scoreB: Int) {
viewModelScope.launch { 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<String> {
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
}
}
} }

View File

@@ -1,6 +1,5 @@
package me.zobrist.tichucounter package me.zobrist.tichucounter
import me.zobrist.tichucounter.data.Round
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
@@ -37,13 +36,13 @@ class TichuUnitTest {
assertGeneratedRound(tichu, 400, 0) assertGeneratedRound(tichu, 400, 0)
//Good rounds trough Tichu //Good rounds trough Tichu
assertValidRound(tichu, 0, 0) assertValidRound(tichu, 0, 0)
assertValidRound(tichu, -100, 0) assertValidRound(tichu, -100, 0)
//Bad rounds //Bad rounds
assertInvalidRound(tichu, 5, 12) assertInvalidRound(tichu, 5, 12)
assertInvalidRound(tichu, 12, 5) assertInvalidRound(tichu, 12, 5)
assertInvalidRound(tichu, 5, 55) assertInvalidRound(tichu, 5, 55)
} }
private fun assertGeneratedRound(tichu: Tichu, scoreA: Int, expectedScoreB: Int) { private fun assertGeneratedRound(tichu: Tichu, scoreA: Int, expectedScoreB: Int) {