mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Big viewmodel refactor
(squashed)
This commit is contained in:
parent
b1c8780fc1
commit
78f9d59fd8
@ -1,55 +0,0 @@
|
||||
package com.sadellie.unitto.screen
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.sadellie.unitto.data.preferences.DataStoreModule
|
||||
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
|
||||
import com.sadellie.unitto.data.units.AllUnitsRepository
|
||||
import com.sadellie.unitto.data.units.MyUnitIDS
|
||||
import com.sadellie.unitto.data.units.database.MyBasedUnitDatabase
|
||||
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
|
||||
import com.sadellie.unitto.screens.main.MainViewModel
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SwapUnitsTest {
|
||||
|
||||
private lateinit var viewModel: MainViewModel
|
||||
private val allUnitsRepository = AllUnitsRepository()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
viewModel = MainViewModel(
|
||||
UserPreferencesRepository(DataStoreModule().provideUserPreferencesDataStore(context)),
|
||||
MyBasedUnitsRepository(
|
||||
Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
MyBasedUnitDatabase::class.java
|
||||
).build().myBasedUnitDao()
|
||||
),
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
allUnitsRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swapUnits() {
|
||||
val mile = allUnitsRepository.getById(MyUnitIDS.mile)
|
||||
val kilometer = allUnitsRepository.getById(MyUnitIDS.kilometer)
|
||||
|
||||
viewModel.changeUnitFrom(kilometer)
|
||||
viewModel.changeUnitTo(mile)
|
||||
viewModel.swapUnits()
|
||||
|
||||
assertEquals(mile, viewModel.unitFrom)
|
||||
assertEquals(kilometer,viewModel.unitTo)
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
@ -122,30 +123,36 @@ fun UnittoApp(
|
||||
}
|
||||
|
||||
composable(LEFT_LIST_SCREEN) {
|
||||
val mainUiState = mainViewModel.uiStateFlow.collectAsState()
|
||||
val unitFrom = mainUiState.value.unitFrom ?: return@composable
|
||||
// Initial group
|
||||
secondViewModel.setSelectedChip(mainViewModel.unitFrom.group, true)
|
||||
secondViewModel.setSelectedChip(unitFrom.group, true)
|
||||
|
||||
LeftSideScreen(
|
||||
viewModel = secondViewModel,
|
||||
currentUnit = mainViewModel.unitFrom,
|
||||
currentUnit = unitFrom,
|
||||
navigateUp = { navController.navigateUp() },
|
||||
navigateToSettingsAction = { navController.navigate(UNIT_GROUPS_SCREEN) },
|
||||
selectAction = { mainViewModel.changeUnitFrom(it) }
|
||||
selectAction = { mainViewModel.updateUnitFrom(it) }
|
||||
)
|
||||
}
|
||||
|
||||
composable(RIGHT_LIST_SCREEN) {
|
||||
val mainUiState = mainViewModel.uiStateFlow.collectAsState()
|
||||
val unitFrom = mainUiState.value.unitFrom ?: return@composable
|
||||
val unitTo = mainUiState.value.unitTo ?: return@composable
|
||||
|
||||
// Initial group
|
||||
secondViewModel.setSelectedChip(mainViewModel.unitFrom.group, false)
|
||||
secondViewModel.setSelectedChip(unitFrom.group, false)
|
||||
|
||||
RightSideScreen(
|
||||
viewModel = secondViewModel,
|
||||
currentUnit = mainViewModel.unitTo,
|
||||
currentUnit = unitTo,
|
||||
navigateUp = { navController.navigateUp() },
|
||||
navigateToSettingsAction = { navController.navigate(UNIT_GROUPS_SCREEN) },
|
||||
selectAction = { mainViewModel.changeUnitTo(it) },
|
||||
inputValue = mainViewModel.inputValue(),
|
||||
unitFrom = mainViewModel.unitFrom
|
||||
selectAction = { mainViewModel.updateUnitTo(it) },
|
||||
inputValue = mainViewModel.getInputValue(),
|
||||
unitFrom = unitFrom
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,16 +21,17 @@ package com.sadellie.unitto.data
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T1, T2, T3, T4, T5, T6, R> combine(
|
||||
fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
flow4: Flow<T4>,
|
||||
flow5: Flow<T5>,
|
||||
flow6: Flow<T6>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
|
||||
flow7: Flow<T7>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
|
||||
): Flow<R> =
|
||||
kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
|
||||
kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
|
||||
transform(
|
||||
args[0] as T1,
|
||||
args[1] as T2,
|
||||
@ -38,5 +39,6 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
|
||||
args[3] as T4,
|
||||
args[4] as T5,
|
||||
args[5] as T6,
|
||||
args[6] as T7,
|
||||
)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.screens
|
||||
|
||||
import com.sadellie.unitto.data.INTERNAL_DISPLAY
|
||||
import com.sadellie.unitto.data.KEY_COMMA
|
||||
import com.sadellie.unitto.data.KEY_DOT
|
||||
import com.sadellie.unitto.data.KEY_E
|
||||
@ -81,6 +82,10 @@ object Formatter {
|
||||
output = output.replace(it, formatNumber(it))
|
||||
}
|
||||
|
||||
INTERNAL_DISPLAY.forEach {
|
||||
output = output.replace(it.key, it.value)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
|
@ -39,8 +39,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.sadellie.unitto.R
|
||||
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
|
||||
import com.sadellie.unitto.data.units.AbstractUnit
|
||||
import com.sadellie.unitto.data.units.UnitGroup
|
||||
import com.sadellie.unitto.screens.common.AnimatedTopBarText
|
||||
import com.sadellie.unitto.screens.main.components.Keyboard
|
||||
import com.sadellie.unitto.screens.main.components.TopScreenPart
|
||||
@ -52,7 +50,7 @@ fun MainScreen(
|
||||
viewModel: MainViewModel = viewModel()
|
||||
) {
|
||||
var launched: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||
val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle()
|
||||
val mainScreenUIState = viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier,
|
||||
@ -76,15 +74,12 @@ fun MainScreen(
|
||||
content = { padding ->
|
||||
MainScreenContent(
|
||||
modifier = Modifier.padding(padding),
|
||||
unitFrom = viewModel.unitFrom,
|
||||
unitTo = viewModel.unitTo,
|
||||
mainScreenUIState = mainScreenUIState.value,
|
||||
navControllerAction = { navControllerAction(it) },
|
||||
swapMeasurements = { viewModel.swapUnits() },
|
||||
processInput = { viewModel.processInput(it) },
|
||||
deleteDigit = { viewModel.deleteDigit() },
|
||||
clearInput = { viewModel.clearInput() },
|
||||
baseConverterMode = viewModel.unitFrom.group == UnitGroup.NUMBER_BASE
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -103,15 +98,12 @@ fun MainScreen(
|
||||
@Composable
|
||||
private fun MainScreenContent(
|
||||
modifier: Modifier,
|
||||
unitFrom: AbstractUnit,
|
||||
unitTo: AbstractUnit,
|
||||
mainScreenUIState: MainScreenUIState = MainScreenUIState(),
|
||||
mainScreenUIState: MainScreenUIState,
|
||||
navControllerAction: (String) -> Unit = {},
|
||||
swapMeasurements: () -> Unit = {},
|
||||
processInput: (String) -> Unit = {},
|
||||
deleteDigit: () -> Unit = {},
|
||||
clearInput: () -> Unit = {},
|
||||
baseConverterMode: Boolean,
|
||||
) {
|
||||
PortraitLandscape(
|
||||
modifier = modifier,
|
||||
@ -121,14 +113,13 @@ private fun MainScreenContent(
|
||||
inputValue = mainScreenUIState.inputValue,
|
||||
calculatedValue = mainScreenUIState.calculatedValue,
|
||||
outputValue = mainScreenUIState.resultValue,
|
||||
unitFrom = unitFrom,
|
||||
unitTo = unitTo,
|
||||
loadingDatabase = mainScreenUIState.isLoadingDatabase,
|
||||
loadingNetwork = mainScreenUIState.isLoadingNetwork,
|
||||
unitFrom = mainScreenUIState.unitFrom,
|
||||
unitTo = mainScreenUIState.unitTo,
|
||||
loadingNetwork = mainScreenUIState.showLoading,
|
||||
networkError = mainScreenUIState.showError,
|
||||
onUnitSelectionClick = navControllerAction,
|
||||
swapUnits = swapMeasurements,
|
||||
baseConverterMode = baseConverterMode,
|
||||
converterMode = mainScreenUIState.mode,
|
||||
)
|
||||
},
|
||||
content2 = {
|
||||
@ -137,7 +128,7 @@ private fun MainScreenContent(
|
||||
addDigit = processInput,
|
||||
deleteDigit = deleteDigit,
|
||||
clearInput = clearInput,
|
||||
baseConverter = baseConverterMode,
|
||||
converterMode = mainScreenUIState.mode,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -19,24 +19,33 @@
|
||||
package com.sadellie.unitto.screens.main
|
||||
|
||||
import com.sadellie.unitto.data.KEY_0
|
||||
import com.sadellie.unitto.data.units.AbstractUnit
|
||||
|
||||
/**
|
||||
* Represents current state of the MainScreen
|
||||
*
|
||||
* @property inputValue Current input value. Can be expression or a simple number.
|
||||
* @property resultValue Current output value
|
||||
* @property calculatedValue Currently calculated value. Can be null if not needed (same as input or
|
||||
* expression in input is invalid)
|
||||
* @property isLoadingDatabase Whether we are loading data from Database. Need on app launch, once
|
||||
* we are done loading list on Units list can be sorted by usage properly/
|
||||
* @property isLoadingNetwork Whether we are loading data from network
|
||||
* expression in input is invalid).
|
||||
* @property resultValue Current output value.
|
||||
* @property showLoading Whether we are loading data from network.
|
||||
* @property showError Whether there was an error while loading data from network
|
||||
* @property unitFrom Unit on the left.
|
||||
* @property unitTo Unit on the right.
|
||||
* @property mode
|
||||
*/
|
||||
data class MainScreenUIState(
|
||||
var inputValue: String = KEY_0,
|
||||
var resultValue: String = KEY_0,
|
||||
var isLoadingDatabase: Boolean = true,
|
||||
var isLoadingNetwork: Boolean = false,
|
||||
var showError: Boolean = false,
|
||||
var calculatedValue: String? = null
|
||||
val inputValue: String = KEY_0,
|
||||
val calculatedValue: String? = null,
|
||||
val resultValue: String = KEY_0,
|
||||
val showLoading: Boolean = true,
|
||||
val showError: Boolean = false,
|
||||
val unitFrom: AbstractUnit? = null,
|
||||
val unitTo: AbstractUnit? = null,
|
||||
val mode: ConverterMode = ConverterMode.DEFAULT,
|
||||
)
|
||||
|
||||
enum class ConverterMode {
|
||||
DEFAULT,
|
||||
BASE,
|
||||
}
|
||||
|
@ -18,17 +18,12 @@
|
||||
|
||||
package com.sadellie.unitto.screens.main
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.keelar.exprk.ExpressionException
|
||||
import com.github.keelar.exprk.Expressions
|
||||
import com.sadellie.unitto.FirebaseHelper
|
||||
import com.sadellie.unitto.data.DIGITS
|
||||
import com.sadellie.unitto.data.INTERNAL_DISPLAY
|
||||
import com.sadellie.unitto.data.KEY_0
|
||||
import com.sadellie.unitto.data.KEY_1
|
||||
import com.sadellie.unitto.data.KEY_2
|
||||
@ -71,6 +66,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
@ -84,77 +81,345 @@ import javax.inject.Inject
|
||||
class MainViewModel @Inject constructor(
|
||||
private val userPrefsRepository: UserPreferencesRepository,
|
||||
private val basedUnitRepository: MyBasedUnitsRepository,
|
||||
private val mContext: Application,
|
||||
private val allUnitsRepository: AllUnitsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val input: MutableStateFlow<String> = MutableStateFlow(KEY_0)
|
||||
private val _calculated: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
private val _result: MutableStateFlow<String> = MutableStateFlow(KEY_0)
|
||||
private val _latestInputStack: MutableList<String> = mutableListOf(KEY_0)
|
||||
private val _inputDisplay: MutableStateFlow<String> = MutableStateFlow(KEY_0)
|
||||
private val _isLoadingDatabase: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
private val _isLoadingNetwork: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
private val _showError: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
private val _userPrefs = userPrefsRepository.userPreferencesFlow.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5000), UserPreferences()
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
UserPreferences()
|
||||
)
|
||||
|
||||
val mainFlow: StateFlow<MainScreenUIState> = combine(
|
||||
_inputDisplay,
|
||||
/**
|
||||
* Unit on the left, the one we convert from. Initially null, meaning we didn't restore it yet.
|
||||
*/
|
||||
private val _unitFrom: MutableStateFlow<AbstractUnit?> = MutableStateFlow(null)
|
||||
|
||||
/**
|
||||
* Unit on the right, the one we convert to. Initially null, meaning we didn't restore it yet.
|
||||
*/
|
||||
private val _unitTo: MutableStateFlow<AbstractUnit?> = MutableStateFlow(null)
|
||||
|
||||
/**
|
||||
* Current input. Used when converting units.
|
||||
*/
|
||||
private val _input: MutableStateFlow<String> = MutableStateFlow(KEY_0)
|
||||
|
||||
/**
|
||||
* Calculation result. Null when [_input] is not an expression.
|
||||
*/
|
||||
private val _calculated: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
|
||||
/**
|
||||
* List of latest symbols that were entered.
|
||||
*/
|
||||
private val _latestInputStack: MutableList<String> = mutableListOf(_input.value)
|
||||
|
||||
/**
|
||||
* Conversion result.
|
||||
*/
|
||||
private val _result: MutableStateFlow<String> = MutableStateFlow(KEY_0)
|
||||
|
||||
/**
|
||||
* True when loading something from network.
|
||||
*/
|
||||
private val _showLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* True if there was error while loading data.
|
||||
*/
|
||||
private val _showError: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* Current state of UI.
|
||||
*/
|
||||
val uiStateFlow: StateFlow<MainScreenUIState> = combine(
|
||||
_input,
|
||||
_unitFrom,
|
||||
_unitTo,
|
||||
_calculated,
|
||||
_result,
|
||||
_isLoadingNetwork,
|
||||
_isLoadingDatabase,
|
||||
_showError,
|
||||
) { inputValue: String,
|
||||
calculatedValue: String?,
|
||||
resultValue: String,
|
||||
showLoadingNetwork: Boolean,
|
||||
showLoadingDatabase: Boolean,
|
||||
showError: Boolean ->
|
||||
_showLoading,
|
||||
_showError
|
||||
) { inputValue, unitFromValue, unitToValue, calculatedValue, resultValue, showLoadingValue, showErrorValue ->
|
||||
return@combine MainScreenUIState(
|
||||
inputValue = inputValue,
|
||||
calculatedValue = calculatedValue,
|
||||
resultValue = resultValue,
|
||||
isLoadingNetwork = showLoadingNetwork,
|
||||
isLoadingDatabase = showLoadingDatabase,
|
||||
showError = showError,
|
||||
showLoading = showLoadingValue,
|
||||
showError = showErrorValue,
|
||||
unitFrom = unitFromValue,
|
||||
unitTo = unitToValue,
|
||||
/**
|
||||
* If there will be more modes, this should be a separate value which we update when
|
||||
* changing units.
|
||||
*/
|
||||
mode = if (_unitFrom.value is NumberBaseUnit) ConverterMode.BASE else ConverterMode.DEFAULT
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
MainScreenUIState()
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainScreenUIState())
|
||||
|
||||
/**
|
||||
* Unit we converting from (left side)
|
||||
* Process input with rules. Makes sure that user input is corrected when needed.
|
||||
*
|
||||
* @param symbolToAdd Use 'ugly' version of symbols.
|
||||
*/
|
||||
var unitFrom: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.kilometer))
|
||||
private set
|
||||
fun processInput(symbolToAdd: String) {
|
||||
// We are still loading data from network, don't accept any input yet
|
||||
if (_showLoading.value) return
|
||||
|
||||
val lastTwoSymbols = _latestInputStack.takeLast(2)
|
||||
val lastSymbol: String = lastTwoSymbols.getOrNull(1) ?: lastTwoSymbols[0]
|
||||
val lastSecondSymbol: String? = lastTwoSymbols.getOrNull(0)
|
||||
|
||||
when (symbolToAdd) {
|
||||
KEY_PLUS, KEY_DIVIDE, KEY_MULTIPLY, KEY_EXPONENT -> {
|
||||
when {
|
||||
// Don't need expressions that start with zero
|
||||
(_input.value == KEY_0) -> {}
|
||||
(_input.value == KEY_MINUS) -> {}
|
||||
(lastSymbol == KEY_LEFT_BRACKET) -> {}
|
||||
(lastSymbol == KEY_SQRT) -> {}
|
||||
/**
|
||||
* For situations like "50+-", when user clicks "/" we delete "-" so it becomes
|
||||
* "50+". We don't add "/' here. User will click "/" second time and the input
|
||||
* will be "50/".
|
||||
*/
|
||||
(lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_MINUS) -> {
|
||||
deleteDigit()
|
||||
}
|
||||
// Don't allow multiple operators near each other
|
||||
(lastSymbol in OPERATORS) -> {
|
||||
deleteDigit()
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_0 -> {
|
||||
when {
|
||||
// Don't add zero if the input is already a zero
|
||||
(_input.value == KEY_0) -> {}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
// Prevents things like "-00" and "4+000"
|
||||
((lastSecondSymbol in OPERATORS + KEY_LEFT_BRACKET) and (lastSymbol == KEY_0)) -> {}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 -> {
|
||||
// Replace single zero (default input) if it's here
|
||||
when {
|
||||
(_input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_MINUS -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(_input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
// Don't allow multiple minuses near each other
|
||||
(lastSymbol.compareTo(KEY_MINUS) == 0) -> {}
|
||||
// Don't allow plus and minus be near each other
|
||||
(lastSymbol == KEY_PLUS) -> {
|
||||
deleteDigit()
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_DOT -> {
|
||||
if (!_input.value
|
||||
.takeLastWhile { it.toString() !in OPERATORS.minus(KEY_DOT) }
|
||||
.contains(KEY_DOT)
|
||||
) {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
KEY_LEFT_BRACKET -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(_input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) || (lastSymbol in DIGITS) || (lastSymbol == KEY_DOT) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_RIGHT_BRACKET -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(_input.value == KEY_0) -> {}
|
||||
(lastSymbol == KEY_LEFT_BRACKET) -> {}
|
||||
(
|
||||
_latestInputStack.filter { it == KEY_LEFT_BRACKET }.size ==
|
||||
_latestInputStack.filter { it == KEY_RIGHT_BRACKET }.size
|
||||
) -> {
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_SQRT -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(_input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) || (lastSymbol in DIGITS) || (lastSymbol == KEY_DOT) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(_input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit we are converting to (right side)
|
||||
* Update [_unitFrom] and set [_unitTo] from pair. Also updates stats for this [unit].
|
||||
*/
|
||||
var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile))
|
||||
private set
|
||||
fun updateUnitFrom(unit: AbstractUnit) {
|
||||
// We change from something to base converter or the other way around
|
||||
if ((_unitFrom.value?.group != UnitGroup.NUMBER_BASE) xor (unit.group == UnitGroup.NUMBER_BASE)) {
|
||||
_calculated.update { null }
|
||||
clearInput()
|
||||
}
|
||||
|
||||
_unitFrom.update { unit }
|
||||
|
||||
/**
|
||||
* Update pair [_unitTo] if it exists
|
||||
*/
|
||||
val pair = unit.pairedUnit
|
||||
if (pair != null) {
|
||||
_unitTo.update { allUnitsRepository.getById(pair) }
|
||||
} else {
|
||||
// No pair, get something from same group
|
||||
_unitTo.update { allUnitsRepository.getCollectionByGroup(unit.group).first() }
|
||||
}
|
||||
incrementCounter(unit)
|
||||
updateCurrenciesRatesIfNeeded()
|
||||
saveLatestPairOfUnits()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update [_unitTo] and update pair for [_unitFrom].
|
||||
*/
|
||||
fun updateUnitTo(unit: AbstractUnit) {
|
||||
_unitTo.update { unit }
|
||||
_unitFrom.value?.pairedUnit = unit.unitId
|
||||
updatePairedUnit(_unitFrom.value ?: return)
|
||||
incrementCounter(unit)
|
||||
saveLatestPairOfUnits()
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap [_unitFrom] and [_unitTo].
|
||||
*/
|
||||
fun swapUnits() {
|
||||
_unitFrom
|
||||
.getAndUpdate { _unitTo.value }
|
||||
.also { oldUnitFrom -> _unitTo.update { oldUnitFrom } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete last symbol from [_input].
|
||||
*/
|
||||
fun deleteDigit() {
|
||||
// Default input, don't delete
|
||||
if (_input.value == KEY_0) return
|
||||
|
||||
val lastSymbol = _latestInputStack.removeLast()
|
||||
|
||||
// If this value are same, it means that after deleting there will be no symbols left, set to default
|
||||
if (lastSymbol == _input.value) {
|
||||
setInputSymbols(KEY_0, false)
|
||||
} else {
|
||||
_input.update { it.removeSuffix(lastSymbol) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear [_input].
|
||||
*/
|
||||
fun clearInput() {
|
||||
setInputSymbols(KEY_0, false)
|
||||
}
|
||||
|
||||
fun getInputValue(): String {
|
||||
return _calculated.value ?: _input.value
|
||||
}
|
||||
|
||||
private suspend fun convertInput() {
|
||||
if (_unitFrom.value?.group == UnitGroup.NUMBER_BASE) {
|
||||
convertAsNumberBase()
|
||||
} else {
|
||||
convertAsExpression()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun convertAsNumberBase() {
|
||||
withContext(Dispatchers.Default) {
|
||||
while (isActive) {
|
||||
// Units are still loading, don't convert anything yet
|
||||
val unitFrom = _unitFrom.value ?: return@withContext
|
||||
val unitTo = _unitTo.value ?: return@withContext
|
||||
|
||||
val conversionResult = try {
|
||||
(unitFrom as NumberBaseUnit).convertToBase(
|
||||
input = input.value,
|
||||
toBase = (unitTo as NumberBaseUnit).base,
|
||||
input = _input.value,
|
||||
toBase = (unitTo as NumberBaseUnit).base
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is NumberFormatException, is IllegalArgumentException -> {
|
||||
""
|
||||
}
|
||||
is ClassCastException -> {
|
||||
cancel()
|
||||
return@withContext
|
||||
}
|
||||
else -> {
|
||||
throw e
|
||||
}
|
||||
is NumberFormatException, is IllegalArgumentException -> ""
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
_result.update { conversionResult }
|
||||
@ -166,13 +431,17 @@ class MainViewModel @Inject constructor(
|
||||
private suspend fun convertAsExpression() {
|
||||
withContext(Dispatchers.Default) {
|
||||
while (isActive) {
|
||||
// Units are still loading, don't convert anything yet
|
||||
val unitFrom = _unitFrom.value ?: return@withContext
|
||||
val unitTo = _unitTo.value ?: return@withContext
|
||||
|
||||
// First we clean the input from garbage at the end
|
||||
var cleanInput = input.value.dropLastWhile { !it.isDigit() }
|
||||
var cleanInput = _input.value.dropLastWhile { !it.isDigit() }
|
||||
|
||||
// Now we close open brackets that user didn't close
|
||||
// AUTOCLOSE ALL BRACKETS
|
||||
val leftBrackets = input.value.count { it.toString() == KEY_LEFT_BRACKET }
|
||||
val rightBrackets = input.value.count { it.toString() == KEY_RIGHT_BRACKET }
|
||||
val leftBrackets = _input.value.count { it.toString() == KEY_LEFT_BRACKET }
|
||||
val rightBrackets = _input.value.count { it.toString() == KEY_RIGHT_BRACKET }
|
||||
val neededBrackets = leftBrackets - rightBrackets
|
||||
if (neededBrackets > 0) cleanInput += KEY_RIGHT_BRACKET.repeat(neededBrackets)
|
||||
|
||||
@ -200,17 +469,23 @@ class MainViewModel @Inject constructor(
|
||||
// 123.456 will be true
|
||||
// -123.456 will be true
|
||||
// -123.456-123 will be false (first minus gets removed, ending with 123.456)
|
||||
if (input.value.removePrefix(KEY_MINUS).all { it.toString() !in OPERATORS }) {
|
||||
if (_input.value.removePrefix(KEY_MINUS).all { it.toString() !in OPERATORS }) {
|
||||
// No operators
|
||||
_calculated.update { null }
|
||||
} else {
|
||||
_calculated.update { evaluationResult.toStringWith(_userPrefs.value.outputFormat) }
|
||||
_calculated.update {
|
||||
evaluationResult.toStringWith(
|
||||
_userPrefs.value.outputFormat
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we just convert.
|
||||
// We can use evaluation result here, input is valid
|
||||
val conversionResult: BigDecimal = unitFrom.convert(
|
||||
unitTo, evaluationResult, _userPrefs.value.digitsPrecision
|
||||
unitTo,
|
||||
evaluationResult,
|
||||
_userPrefs.value.digitsPrecision
|
||||
)
|
||||
|
||||
// Converted
|
||||
@ -221,110 +496,53 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes local variables, converts values and then causes the UI to update
|
||||
*/
|
||||
private suspend fun convertInput() {
|
||||
if (unitFrom.group == UnitGroup.NUMBER_BASE) {
|
||||
convertAsNumberBase()
|
||||
private fun setInputSymbols(symbol: String, add: Boolean = true) {
|
||||
if (add) {
|
||||
_input.update { it + symbol }
|
||||
} else {
|
||||
convertAsExpression()
|
||||
// We don't need previous input, clear entirely
|
||||
_latestInputStack.clear()
|
||||
_input.update { symbol }
|
||||
}
|
||||
_latestInputStack.add(symbol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change left side unit. Unit to convert from
|
||||
*
|
||||
* @param clickedUnit Unit we need to change to
|
||||
*/
|
||||
fun changeUnitFrom(clickedUnit: AbstractUnit) {
|
||||
// Do we change to NumberBase?
|
||||
if ((unitFrom.group != UnitGroup.NUMBER_BASE) and (clickedUnit.group == UnitGroup.NUMBER_BASE)) {
|
||||
// It was not NUMBER_BASE, but now we change to it. Clear input.
|
||||
clearInput()
|
||||
}
|
||||
|
||||
if ((unitFrom.group == UnitGroup.NUMBER_BASE) and (clickedUnit.group != UnitGroup.NUMBER_BASE)) {
|
||||
// It was NUMBER_BASE, but now we change to something else. Clear input.
|
||||
clearInput()
|
||||
}
|
||||
|
||||
// First we change unit
|
||||
unitFrom = clickedUnit
|
||||
|
||||
// Now we change to positive if the group we switched to supports negate
|
||||
if (!clickedUnit.group.canNegate) {
|
||||
input.update { input.value.removePrefix(KEY_MINUS) }
|
||||
}
|
||||
|
||||
// Now setting up right unit (pair for the left one)
|
||||
unitTo = if (unitFrom.pairedUnit == null) {
|
||||
allUnitsRepository.getCollectionByGroup(unitFrom.group).first()
|
||||
} else {
|
||||
allUnitsRepository.getById(unitFrom.pairedUnit!!)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
// We need to increment counter for the clicked unit
|
||||
incrementCounter(clickedUnit)
|
||||
// Currencies require us to get data from the internet
|
||||
updateCurrenciesBasicUnits()
|
||||
// Saving latest pair
|
||||
saveLatestPairOfUnits()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change right side unit. Unit to convert to
|
||||
*
|
||||
* @param clickedUnit Unit we need to change to
|
||||
*/
|
||||
fun changeUnitTo(clickedUnit: AbstractUnit) {
|
||||
// First we change unit
|
||||
unitTo = clickedUnit
|
||||
// Updating paired unit for left side unit in memory (same thing for database below)
|
||||
unitFrom.pairedUnit = unitTo.unitId
|
||||
|
||||
viewModelScope.launch {
|
||||
// Updating paired unit for left side unit in database
|
||||
private fun incrementCounter(unit: AbstractUnit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
basedUnitRepository.insertUnits(
|
||||
MyBasedUnit(
|
||||
unitId = unitFrom.unitId,
|
||||
isFavorite = unitFrom.isFavorite,
|
||||
pairedUnitId = unitFrom.pairedUnit,
|
||||
frequency = unitFrom.counter
|
||||
)
|
||||
)
|
||||
// We also need to increment counter for the selected unit
|
||||
incrementCounter(clickedUnit)
|
||||
// Saving latest pair
|
||||
saveLatestPairOfUnits()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun incrementCounter(unit: AbstractUnit) {
|
||||
basedUnitRepository.insertUnits(
|
||||
MyBasedUnit(
|
||||
unitId = unit.unitId, isFavorite = unit.isFavorite, pairedUnitId = unit.pairedUnit,
|
||||
unitId = unit.unitId,
|
||||
isFavorite = unit.isFavorite,
|
||||
pairedUnitId = unit.pairedUnit,
|
||||
// This will increment counter on unit in list too
|
||||
frequency = ++unit.counter
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates basic units properties for all currencies, BUT only when [unitFrom]'s group is set
|
||||
* to [UnitGroup.CURRENCY].
|
||||
*/
|
||||
private suspend fun updateCurrenciesBasicUnits() {
|
||||
// Resetting error and network loading states in case we are not gonna do anything below
|
||||
_isLoadingNetwork.update { false }
|
||||
private fun updatePairedUnit(unit: AbstractUnit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
basedUnitRepository.insertUnits(
|
||||
MyBasedUnit(
|
||||
unitId = unit.unitId,
|
||||
isFavorite = unit.isFavorite,
|
||||
pairedUnitId = unit.pairedUnit,
|
||||
frequency = unit.counter
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCurrenciesRatesIfNeeded() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_showError.update { false }
|
||||
// We update currencies only when needed
|
||||
if (unitFrom.group != UnitGroup.CURRENCY) return
|
||||
|
||||
_showLoading.update { false }
|
||||
// Units are still loading, don't convert anything yet
|
||||
val unitFrom = _unitFrom.value ?: return@launch
|
||||
if (_unitFrom.value?.group != UnitGroup.CURRENCY) return@launch
|
||||
// Starting to load stuff
|
||||
_isLoadingNetwork.update { true }
|
||||
_showLoading.update { true }
|
||||
|
||||
try {
|
||||
val pairs: CurrencyUnitResponse =
|
||||
@ -341,278 +559,53 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
_showError.update { true }
|
||||
} finally {
|
||||
// Loaded
|
||||
_isLoadingNetwork.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps measurement, left to right and vice versa
|
||||
*/
|
||||
fun swapUnits() {
|
||||
unitFrom = unitTo.also {
|
||||
unitTo = unitFrom
|
||||
}
|
||||
viewModelScope.launch {
|
||||
updateCurrenciesBasicUnits()
|
||||
saveLatestPairOfUnits()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to process input when we click keyboard. Make sure that digits/symbols will be
|
||||
* added properly
|
||||
* @param[symbolToAdd] Digit/Symbol we want to add, can be any digit 0..9 or a dot symbol
|
||||
*/
|
||||
fun processInput(symbolToAdd: String) {
|
||||
val lastTwoSymbols = _latestInputStack.takeLast(2)
|
||||
val lastSymbol: String = lastTwoSymbols.getOrNull(1) ?: lastTwoSymbols[0]
|
||||
val lastSecondSymbol: String? = lastTwoSymbols.getOrNull(0)
|
||||
|
||||
when (symbolToAdd) {
|
||||
KEY_PLUS, KEY_DIVIDE, KEY_MULTIPLY, KEY_EXPONENT -> {
|
||||
when {
|
||||
// Don't need expressions that start with zero
|
||||
(input.value == KEY_0) -> {}
|
||||
(input.value == KEY_MINUS) -> {}
|
||||
(lastSymbol == KEY_LEFT_BRACKET) -> {}
|
||||
(lastSymbol == KEY_SQRT) -> {}
|
||||
/**
|
||||
* For situations like "50+-", when user clicks "/" we delete "-" so it becomes
|
||||
* "50+". We don't add "/' here. User will click "/" second time and the input
|
||||
* will be "50/".
|
||||
*/
|
||||
(lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_MINUS) -> {
|
||||
deleteDigit()
|
||||
}
|
||||
// Don't allow multiple operators near each other
|
||||
(lastSymbol in OPERATORS) -> {
|
||||
deleteDigit()
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_0 -> {
|
||||
when {
|
||||
// Don't add zero if the input is already a zero
|
||||
(input.value == KEY_0) -> {}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
// Prevents things like "-00" and "4+000"
|
||||
((lastSecondSymbol in OPERATORS + KEY_LEFT_BRACKET) and (lastSymbol == KEY_0)) -> {}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 -> {
|
||||
// Replace single zero (default input) if it's here
|
||||
when {
|
||||
(input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_MINUS -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
// Don't allow multiple minuses near each other
|
||||
(lastSymbol.compareTo(KEY_MINUS) == 0) -> {}
|
||||
// Don't allow plus and minus be near each other
|
||||
(lastSymbol == KEY_PLUS) -> {
|
||||
deleteDigit()
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_DOT -> {
|
||||
if (canEnterDot()) {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
KEY_LEFT_BRACKET -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) || (lastSymbol in DIGITS) || (lastSymbol == KEY_DOT) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_RIGHT_BRACKET -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(input.value == KEY_0) -> {}
|
||||
(lastSymbol == KEY_LEFT_BRACKET) -> {}
|
||||
(_latestInputStack.filter { it == KEY_LEFT_BRACKET }.size ==
|
||||
_latestInputStack.filter { it == KEY_RIGHT_BRACKET }.size) -> {
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
KEY_SQRT -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
(lastSymbol == KEY_RIGHT_BRACKET) || (lastSymbol in DIGITS) || (lastSymbol == KEY_DOT) -> {
|
||||
processInput(KEY_MULTIPLY)
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
// Replace single zero with minus (to support negative numbers)
|
||||
(input.value == KEY_0) -> {
|
||||
setInputSymbols(symbolToAdd, false)
|
||||
}
|
||||
else -> {
|
||||
setInputSymbols(symbolToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes last symbol from input and handles buttons state (enabled/disabled)
|
||||
*/
|
||||
fun deleteDigit() {
|
||||
// Default input, don't delete
|
||||
if (input.value == KEY_0) return
|
||||
|
||||
val lastSymbol = _latestInputStack.removeLast()
|
||||
|
||||
// We will need to delete last symbol from both values
|
||||
val displayRepresentation: String = INTERNAL_DISPLAY[lastSymbol] ?: lastSymbol
|
||||
|
||||
// If this value are same, it means that after deleting there will be no symbols left, set to default
|
||||
if (lastSymbol == input.value) {
|
||||
setInputSymbols(KEY_0, false)
|
||||
} else {
|
||||
input.update { it.removeSuffix(lastSymbol) }
|
||||
_inputDisplay.update { it.removeSuffix(displayRepresentation) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds given [symbol] to [input] and [_inputDisplay] and updates [_latestInputStack].
|
||||
*
|
||||
* By default add symbol, but if [add] is False, will replace current input (when replacing
|
||||
* default [KEY_0] input).
|
||||
*/
|
||||
private fun setInputSymbols(symbol: String, add: Boolean = true) {
|
||||
val displaySymbol: String = INTERNAL_DISPLAY[symbol] ?: symbol
|
||||
|
||||
when {
|
||||
add -> {
|
||||
input.update { it + symbol }
|
||||
_inputDisplay.update { it + displaySymbol }
|
||||
_latestInputStack.add(symbol)
|
||||
}
|
||||
else -> {
|
||||
_latestInputStack.clear()
|
||||
input.update { symbol }
|
||||
_inputDisplay.update { displaySymbol }
|
||||
_latestInputStack.add(symbol)
|
||||
_showLoading.update { false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears input value and sets it to default (ZERO)
|
||||
*/
|
||||
fun clearInput() {
|
||||
setInputSymbols(KEY_0, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value to be used when converting value on the right side screen (unit selection)
|
||||
*/
|
||||
fun inputValue(): String {
|
||||
return mainFlow.value.calculatedValue ?: mainFlow.value.inputValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns True if can be placed.
|
||||
*/
|
||||
private fun canEnterDot(): Boolean = !input.value.takeLastWhile {
|
||||
it.toString() !in OPERATORS.minus(KEY_DOT)
|
||||
}.contains(KEY_DOT)
|
||||
|
||||
/**
|
||||
* Saves latest pair of units into datastore
|
||||
*/
|
||||
private suspend fun saveLatestPairOfUnits() {
|
||||
private fun saveLatestPairOfUnits() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Units are still loading, don't convert anything yet
|
||||
val unitFrom = _unitFrom.value ?: return@launch
|
||||
val unitTo = _unitTo.value ?: return@launch
|
||||
userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startObserving() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
_userPrefs.collectLatest { convertInput() }
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
input.collectLatest { convertInput() }
|
||||
merge(_input, _unitFrom, _unitTo, _userPrefs).collectLatest { convertInput() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
private fun loadInitialUnitPair() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first()
|
||||
|
||||
// First we load latest pair of units
|
||||
unitFrom = try {
|
||||
_unitFrom.update {
|
||||
try {
|
||||
allUnitsRepository.getById(initialUserPrefs.latestLeftSideUnit)
|
||||
} catch (e: java.util.NoSuchElementException) {
|
||||
allUnitsRepository.getById(MyUnitIDS.kilometer)
|
||||
}
|
||||
}
|
||||
|
||||
unitTo = try {
|
||||
_unitTo.update {
|
||||
try {
|
||||
allUnitsRepository.getById(initialUserPrefs.latestRightSideUnit)
|
||||
} catch (e: java.util.NoSuchElementException) {
|
||||
allUnitsRepository.getById(MyUnitIDS.mile)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we load units data from database
|
||||
val allBasedUnits = basedUnitRepository.getAll()
|
||||
allUnitsRepository.loadFromDatabase(mContext, allBasedUnits)
|
||||
|
||||
// User is free to convert values and units on units screen can be sorted properly
|
||||
_isLoadingDatabase.update { false }
|
||||
updateCurrenciesBasicUnits()
|
||||
updateCurrenciesRatesIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
loadInitialUnitPair()
|
||||
startObserving()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.screens.main.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@ -55,6 +56,7 @@ import com.sadellie.unitto.data.KEY_PLUS
|
||||
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
|
||||
import com.sadellie.unitto.data.KEY_SQRT
|
||||
import com.sadellie.unitto.screens.Formatter
|
||||
import com.sadellie.unitto.screens.main.ConverterMode
|
||||
|
||||
/**
|
||||
* Keyboard with button that looks like a calculator
|
||||
@ -63,7 +65,7 @@ import com.sadellie.unitto.screens.Formatter
|
||||
* @param addDigit Function that is called when clicking number and dot buttons
|
||||
* @param deleteDigit Function that is called when clicking delete "<" button
|
||||
* @param clearInput Function that is called when clicking clear "AC" button
|
||||
* @param baseConverter When True will use layout for base conversion.
|
||||
* @param converterMode
|
||||
*/
|
||||
@Composable
|
||||
fun Keyboard(
|
||||
@ -71,7 +73,23 @@ fun Keyboard(
|
||||
addDigit: (String) -> Unit = {},
|
||||
deleteDigit: () -> Unit = {},
|
||||
clearInput: () -> Unit = {},
|
||||
baseConverter: Boolean = false,
|
||||
converterMode: ConverterMode,
|
||||
) {
|
||||
Crossfade(converterMode) {
|
||||
when (it) {
|
||||
ConverterMode.DEFAULT -> DefaultKeyboard(modifier, addDigit, clearInput, deleteDigit)
|
||||
ConverterMode.BASE -> BaseKeyboard(modifier, addDigit, clearInput, deleteDigit)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultKeyboard(
|
||||
modifier: Modifier,
|
||||
addDigit: (String) -> Unit,
|
||||
clearInput: () -> Unit,
|
||||
deleteDigit: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
@ -83,37 +101,6 @@ fun Keyboard(
|
||||
.padding(4.dp)
|
||||
// Column modifier
|
||||
val cModifier = Modifier.weight(1f)
|
||||
if (baseConverter) {
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_BASE_A, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_B, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_C, isPrimary = false, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_BASE_D, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_E, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_F, isPrimary = false, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_7, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_8, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_9, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_4, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_5, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_6, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_1, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_2, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_3, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_0, onClick = addDigit)
|
||||
KeyboardButton(Modifier.fillMaxSize().weight(2f).padding(4.dp), KEY_CLEAR, onLongClick = clearInput) { deleteDigit() }
|
||||
}
|
||||
} else {
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_LEFT_BRACKET, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_RIGHT_BRACKET, isPrimary = false, onClick = addDigit)
|
||||
@ -146,4 +133,59 @@ fun Keyboard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BaseKeyboard(
|
||||
modifier: Modifier,
|
||||
addDigit: (String) -> Unit,
|
||||
clearInput: () -> Unit,
|
||||
deleteDigit: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
// Button modifier
|
||||
val bModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f)
|
||||
.padding(4.dp)
|
||||
// Column modifier
|
||||
val cModifier = Modifier.weight(1f)
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_BASE_A, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_B, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_C, isPrimary = false, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_BASE_D, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_E, isPrimary = false, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_BASE_F, isPrimary = false, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_7, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_8, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_9, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_4, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_5, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_6, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_1, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_2, onClick = addDigit)
|
||||
KeyboardButton(bModifier, KEY_3, onClick = addDigit)
|
||||
}
|
||||
Row(cModifier) {
|
||||
KeyboardButton(bModifier, KEY_0, onClick = addDigit)
|
||||
KeyboardButton(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.weight(2f)
|
||||
.padding(4.dp),
|
||||
KEY_CLEAR,
|
||||
onLongClick = clearInput
|
||||
) { deleteDigit() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,24 +45,24 @@ import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN
|
||||
import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN
|
||||
import com.sadellie.unitto.data.units.AbstractUnit
|
||||
import com.sadellie.unitto.screens.Formatter
|
||||
import com.sadellie.unitto.screens.main.ConverterMode
|
||||
|
||||
/**
|
||||
* Top of the main screen. Contains input and output TextFields, and unit selection row of buttons.
|
||||
* It's a separate composable, so that we support album orientation (this element will be on the left)
|
||||
*
|
||||
* @param modifier Modifier that is applied to Column
|
||||
* @param inputValue Current input value (like big decimal)
|
||||
* @param modifier Modifier that is applied to Column.
|
||||
* @param inputValue Current input value (like big decimal).
|
||||
* @param calculatedValue Current calculated value (like big decimal), will be shown under input when it
|
||||
* has an expression in it.
|
||||
* @param outputValue Current output value (like big decimal)
|
||||
* @param unitFrom [AbstractUnit] on the left
|
||||
* @param unitTo [AbstractUnit] on the right
|
||||
* @param loadingDatabase Are we still loading units usage stats from database? Disables unit
|
||||
* selection buttons.
|
||||
* @param loadingNetwork Are we loading data from network? Shows loading text in TextFields
|
||||
* @param networkError Did we got errors while trying to get data from network
|
||||
* @param onUnitSelectionClick Function that is called when clicking unit selection buttons
|
||||
* @param swapUnits Method to swap units
|
||||
* @param outputValue Current output value (like big decimal).
|
||||
* @param unitFrom [AbstractUnit] on the left.
|
||||
* @param unitTo [AbstractUnit] on the right.
|
||||
* @param loadingNetwork Are we loading data from network? Shows loading text in TextFields.
|
||||
* @param networkError Did we got errors while trying to get data from network.
|
||||
* @param onUnitSelectionClick Function that is called when clicking unit selection buttons.
|
||||
* @param swapUnits Method to swap units.
|
||||
* @param converterMode [ConverterMode.BASE] doesn't use formatting for input/output.
|
||||
*/
|
||||
@Composable
|
||||
fun TopScreenPart(
|
||||
@ -70,14 +70,13 @@ fun TopScreenPart(
|
||||
inputValue: String,
|
||||
calculatedValue: String?,
|
||||
outputValue: String,
|
||||
unitFrom: AbstractUnit,
|
||||
unitTo: AbstractUnit,
|
||||
loadingDatabase: Boolean,
|
||||
unitFrom: AbstractUnit?,
|
||||
unitTo: AbstractUnit?,
|
||||
loadingNetwork: Boolean,
|
||||
networkError: Boolean,
|
||||
onUnitSelectionClick: (String) -> Unit,
|
||||
swapUnits: () -> Unit,
|
||||
baseConverterMode: Boolean,
|
||||
converterMode: ConverterMode,
|
||||
) {
|
||||
var swapped by remember { mutableStateOf(false) }
|
||||
val swapButtonRotation: Float by animateFloatAsState(
|
||||
@ -93,28 +92,28 @@ fun TopScreenPart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
primaryText = {
|
||||
when {
|
||||
loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
|
||||
loadingNetwork -> stringResource(R.string.loading_label)
|
||||
networkError -> stringResource(R.string.error_label)
|
||||
baseConverterMode -> inputValue.uppercase()
|
||||
converterMode == ConverterMode.BASE -> inputValue.uppercase()
|
||||
else -> Formatter.format(inputValue)
|
||||
}
|
||||
},
|
||||
secondaryText = calculatedValue?.let { Formatter.format(it) },
|
||||
helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitFrom.shortName),
|
||||
helperText = stringResource(unitFrom?.shortName ?: R.string.loading_label),
|
||||
textToCopy = calculatedValue ?: inputValue,
|
||||
)
|
||||
MyTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
primaryText = {
|
||||
when {
|
||||
loadingDatabase || loadingNetwork -> stringResource(R.string.loading_label)
|
||||
loadingNetwork -> stringResource(R.string.loading_label)
|
||||
networkError -> stringResource(R.string.error_label)
|
||||
baseConverterMode -> outputValue.uppercase()
|
||||
converterMode == ConverterMode.BASE -> outputValue.uppercase()
|
||||
else -> Formatter.format(outputValue)
|
||||
}
|
||||
},
|
||||
secondaryText = null,
|
||||
helperText = stringResource(if (loadingDatabase) R.string.loading_label else unitTo.shortName),
|
||||
helperText = stringResource(unitTo?.shortName ?: R.string.loading_label),
|
||||
textToCopy = outputValue,
|
||||
)
|
||||
// Unit selection buttons
|
||||
@ -127,15 +126,14 @@ fun TopScreenPart(
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
onClick = { onUnitSelectionClick(LEFT_LIST_SCREEN) },
|
||||
label = unitFrom.displayName,
|
||||
isLoading = loadingDatabase
|
||||
label = unitFrom?.displayName ?: R.string.loading_label,
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
swapUnits()
|
||||
swapped = !swapped
|
||||
},
|
||||
enabled = !loadingDatabase
|
||||
enabled = unitFrom != null
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.rotate(swapButtonRotation),
|
||||
@ -148,8 +146,7 @@ fun TopScreenPart(
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
onClick = { onUnitSelectionClick(RIGHT_LIST_SCREEN) },
|
||||
label = unitTo.displayName,
|
||||
isLoading = loadingDatabase
|
||||
label = unitTo?.displayName ?: R.string.loading_label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -43,26 +43,24 @@ import com.sadellie.unitto.R
|
||||
* @param modifier Modifier that is applied to a [Button]
|
||||
* @param onClick Function to call when button is clicked (navigate to a unit selection screen)
|
||||
* @param label Text on button
|
||||
* @param isLoading Show "Loading" text and disable button
|
||||
*/
|
||||
@Composable
|
||||
fun UnitSelectionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
label: Int,
|
||||
isLoading: Boolean
|
||||
label: Int?,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = { onClick() },
|
||||
enabled = !isLoading,
|
||||
enabled = label != null,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 16.dp, horizontal = 8.dp)
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = label,
|
||||
targetState = label ?: 0,
|
||||
transitionSpec = {
|
||||
if (targetState > initialState) {
|
||||
slideInVertically { height -> height } + fadeIn() with
|
||||
@ -76,7 +74,7 @@ fun UnitSelectionButton(
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(if (isLoading) R.string.loading_label else label),
|
||||
text = stringResource(label ?: R.string.loading_label),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.screens.second
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sadellie.unitto.data.units.AbstractUnit
|
||||
@ -41,7 +42,8 @@ import javax.inject.Inject
|
||||
class SecondViewModel @Inject constructor(
|
||||
private val basedUnitRepository: MyBasedUnitsRepository,
|
||||
private val allUnitsRepository: AllUnitsRepository,
|
||||
unitGroupsRepository: UnitGroupsRepository
|
||||
private val mContext: Application,
|
||||
unitGroupsRepository: UnitGroupsRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _favoritesOnly = MutableStateFlow(false)
|
||||
@ -145,4 +147,16 @@ class SecondViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBasedUnits() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Now we load units data from database
|
||||
val allBasedUnits = basedUnitRepository.getAll()
|
||||
allUnitsRepository.loadFromDatabase(mContext, allBasedUnits)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
loadBasedUnits()
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class FormatterTest {
|
||||
assertEquals("123 456.", formatter.format(INCOMPLETE_VALUE))
|
||||
assertEquals("123 456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||
assertEquals("50+123 456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||
assertEquals("50+123 456÷8×0.8–12+0-√9*4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("50+123 456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ class FormatterTest {
|
||||
assertEquals("123,456.", formatter.format(INCOMPLETE_VALUE))
|
||||
assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||
assertEquals("50+123,456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||
assertEquals("50+123,456÷8×0.8–12+0-√9*4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("50+123,456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ class FormatterTest {
|
||||
assertEquals("123.456,", formatter.format(INCOMPLETE_VALUE))
|
||||
assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||
assertEquals("50+123.456÷8×0,8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||
assertEquals("50+123.456÷8×0,8–12+0-√9*4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("50+123.456÷8×0,8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||
}
|
||||
|
||||
|
@ -46,10 +46,13 @@ import com.sadellie.unitto.data.units.AllUnitsRepository
|
||||
import com.sadellie.unitto.data.units.database.MyBasedUnitDatabase
|
||||
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
|
||||
import com.sadellie.unitto.screens.main.MainViewModel
|
||||
import com.sadellie.unitto.testInViewModel
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.junit.Assert.assertEquals
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -58,7 +61,6 @@ import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@ -70,114 +72,144 @@ class MainViewModelTest {
|
||||
|
||||
private lateinit var viewModel: MainViewModel
|
||||
private val allUnitsRepository = AllUnitsRepository()
|
||||
private val database = Room.inMemoryDatabaseBuilder(
|
||||
RuntimeEnvironment.getApplication(),
|
||||
MyBasedUnitDatabase::class.java
|
||||
).build()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
viewModel = MainViewModel(
|
||||
UserPreferencesRepository(
|
||||
userPrefsRepository = UserPreferencesRepository(
|
||||
DataStoreModule().provideUserPreferencesDataStore(
|
||||
RuntimeEnvironment.getApplication()
|
||||
)
|
||||
), MyBasedUnitsRepository(
|
||||
Room.inMemoryDatabaseBuilder(
|
||||
RuntimeEnvironment.getApplication(), MyBasedUnitDatabase::class.java
|
||||
).build().myBasedUnitDao()
|
||||
), RuntimeEnvironment.getApplication(), allUnitsRepository
|
||||
),
|
||||
basedUnitRepository = MyBasedUnitsRepository(
|
||||
database.myBasedUnitDao()
|
||||
),
|
||||
allUnitsRepository = allUnitsRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun processInputTest() = testInViewModel { coroutineScope ->
|
||||
viewModel.mainFlow.launchIn(coroutineScope)
|
||||
|
||||
/**
|
||||
* For simplicity comments will have structure:
|
||||
* currentInput | userInput | processed internal input | processed display output
|
||||
*/
|
||||
`test 0`()
|
||||
`test digits from 1 to 9`()
|
||||
`test plus, divide, multiply and exponent operators`()
|
||||
`test dot`()
|
||||
`test minus`()
|
||||
`test brackets`()
|
||||
`test square root`()
|
||||
}
|
||||
|
||||
private fun `test 0`() {
|
||||
inputOutputTest("0", "0", "0")
|
||||
inputOutputTest("123000", "123000", "123000")
|
||||
inputOutputTest("123.000", "123.000", "123.000")
|
||||
inputOutputTest("-000", "-0", "–0")
|
||||
inputOutputTest("12+000", "12+0", "12+0")
|
||||
inputOutputTest("√000", "√0", "√0")
|
||||
inputOutputTest("(000", "(0", "(0")
|
||||
inputOutputTest("(1+12)000", "(1+12)*0", "(1+12)×0")
|
||||
inputOutputTest("(1.002+120)000", "(1.002+120)*0", "(1.002+120)×0")
|
||||
}
|
||||
|
||||
private fun `test digits from 1 to 9`() {
|
||||
inputOutputTest("123456789", "123456789", "123456789")
|
||||
inputOutputTest("(1+1)111", "(1+1)*111", "(1+1)×111")
|
||||
}
|
||||
|
||||
private fun `test plus, divide, multiply and exponent operators`() {
|
||||
inputOutputTest("0+++", "0", "0")
|
||||
inputOutputTest("123+++", "123+", "123+")
|
||||
inputOutputTest("1-***", "1*", "1×")
|
||||
inputOutputTest("1/-+++", "1+", "1+")
|
||||
inputOutputTest("0^^^", "0", "0")
|
||||
inputOutputTest("12^^^", "12^", "12^")
|
||||
inputOutputTest("(^^^", "(", "(")
|
||||
inputOutputTest("(8+9)^^^", "(8+9)^", "(8+9)^")
|
||||
}
|
||||
|
||||
private fun `test dot`() {
|
||||
inputOutputTest("0...", "0.", "0.")
|
||||
inputOutputTest("1...", "1.", "1.")
|
||||
inputOutputTest("1+...", "1+.", "1+.")
|
||||
inputOutputTest("√...", "√.", "√.")
|
||||
inputOutputTest("√21...", "√21.", "√21.")
|
||||
inputOutputTest("√21+1.01-.23...", "√21+1.01-.23", "√21+1.01–.23")
|
||||
}
|
||||
|
||||
private fun `test minus`() {
|
||||
inputOutputTest("0---", "-", "–")
|
||||
inputOutputTest("12---", "12-", "12–")
|
||||
inputOutputTest("12+---", "12-", "12–")
|
||||
inputOutputTest("12/---", "12/-", "12÷–")
|
||||
inputOutputTest("√---", "√-", "√–")
|
||||
inputOutputTest("√///", "√", "√")
|
||||
inputOutputTest("12^----", "12^-", "12^–")
|
||||
}
|
||||
|
||||
private fun `test brackets`() {
|
||||
inputOutputTest("0)))", "0", "0")
|
||||
inputOutputTest("0(((", "(((", "(((")
|
||||
inputOutputTest("√(10+2)(", "√(10+2)*(", "√(10+2)×(")
|
||||
inputOutputTest("√(10+2./(", "√(10+2./(", "√(10+2.÷(")
|
||||
inputOutputTest("0()()))((", "((((", "((((")
|
||||
inputOutputTest("√(10+2)^(", "√(10+2)^(", "√(10+2)^(")
|
||||
}
|
||||
|
||||
private fun `test square root`() {
|
||||
inputOutputTest("0√√√", "√√√", "√√√")
|
||||
inputOutputTest("123√√√", "123*√√√", "123×√√√")
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymbolTest() = testInViewModel { coroutineScope ->
|
||||
viewModel.mainFlow.launchIn(coroutineScope)
|
||||
fun `test 0`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
|
||||
inputOutputTest("0", "0")
|
||||
inputOutputTest("123000", "123000")
|
||||
inputOutputTest("123.000", "123.000")
|
||||
inputOutputTest("-000", "-0")
|
||||
inputOutputTest("12+000", "12+0")
|
||||
inputOutputTest("√000", "√0")
|
||||
inputOutputTest("(000", "(0")
|
||||
inputOutputTest("(1+12)000", "(1+12)*0")
|
||||
inputOutputTest("(1.002+120)000", "(1.002+120)*0")
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test digits from 1 to 9`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("123456789", "123456789")
|
||||
inputOutputTest("(1+1)111", "(1+1)*111")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test plus, divide, multiply and exponent operators`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("0+++", "0")
|
||||
inputOutputTest("123+++", "123+")
|
||||
inputOutputTest("1-***", "1*")
|
||||
inputOutputTest("1/-+++", "1+")
|
||||
inputOutputTest("0^^^", "0")
|
||||
inputOutputTest("12^^^", "12^")
|
||||
inputOutputTest("(^^^", "(")
|
||||
inputOutputTest("(8+9)^^^", "(8+9)^")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test dot`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("0...", "0.")
|
||||
inputOutputTest("1...", "1.")
|
||||
inputOutputTest("1+...", "1+.")
|
||||
inputOutputTest("√...", "√.")
|
||||
inputOutputTest("√21...", "√21.")
|
||||
inputOutputTest("√21+1.01-.23...", "√21+1.01-.23")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test minus`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("0---", "-")
|
||||
inputOutputTest("12---", "12-")
|
||||
inputOutputTest("12+---", "12-")
|
||||
inputOutputTest("12/---", "12/-")
|
||||
inputOutputTest("√---", "√-")
|
||||
inputOutputTest("√///", "√")
|
||||
inputOutputTest("12^----", "12^-")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test brackets`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("0)))", "0")
|
||||
inputOutputTest("0(((", "(((")
|
||||
inputOutputTest("√(10+2)(", "√(10+2)*(")
|
||||
inputOutputTest("√(10+2./(", "√(10+2./(")
|
||||
inputOutputTest("0()()))((", "((((")
|
||||
inputOutputTest("√(10+2)^(", "√(10+2)^(")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test square root`() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
inputOutputTest("0√√√", "√√√")
|
||||
inputOutputTest("123√√√", "123*√√√")
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymbolTest() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
|
||||
listOf(
|
||||
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0,
|
||||
KEY_DOT, KEY_COMMA, KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET,
|
||||
KEY_PLUS, KEY_MINUS, KEY_DIVIDE, KEY_MULTIPLY, KEY_EXPONENT, KEY_SQRT,
|
||||
KEY_PLUS, KEY_MINUS, KEY_DIVIDE, KEY_MULTIPLY, KEY_EXPONENT, KEY_SQRT
|
||||
).forEach {
|
||||
// We enter one symbol and delete it, should be default as a result
|
||||
viewModel.processInput(it)
|
||||
viewModel.deleteDigit()
|
||||
assertEquals("0", viewModel.mainFlow.value.inputValue)
|
||||
assertEquals("0", viewModel.input.value)
|
||||
assertEquals("0", viewModel.uiStateFlow.value.inputValue)
|
||||
}
|
||||
viewModel.clearInput()
|
||||
|
||||
@ -189,36 +221,53 @@ class MainViewModelTest {
|
||||
viewModel.processInput(KEY_SQRT)
|
||||
viewModel.processInput(KEY_9)
|
||||
viewModel.deleteDigit()
|
||||
assertEquals("3*√", viewModel.input.value)
|
||||
assertEquals("3×√", viewModel.mainFlow.value.inputValue)
|
||||
assertEquals("3*√", viewModel.uiStateFlow.value.inputValue)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearInputTest() = testInViewModel { coroutineScope ->
|
||||
viewModel.mainFlow.launchIn(coroutineScope)
|
||||
fun clearInputTest() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
|
||||
viewModel.processInput(KEY_3)
|
||||
viewModel.clearInput()
|
||||
assertEquals(null, viewModel.mainFlow.value.calculatedValue)
|
||||
assertEquals(null, viewModel.uiStateFlow.value.calculatedValue)
|
||||
|
||||
viewModel.mainFlow.launchIn(coroutineScope)
|
||||
viewModel.processInput(KEY_3)
|
||||
viewModel.processInput(KEY_MULTIPLY)
|
||||
viewModel.clearInput()
|
||||
assertEquals(null, viewModel.mainFlow.value.calculatedValue)
|
||||
assertEquals(null, viewModel.uiStateFlow.value.calculatedValue)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swapUnitsTest() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.uiStateFlow.collect()
|
||||
}
|
||||
val initialFrom = viewModel.uiStateFlow.value.unitFrom?.unitId
|
||||
val initialTo = viewModel.uiStateFlow.value.unitTo?.unitId
|
||||
|
||||
viewModel.swapUnits()
|
||||
assertEquals(initialTo, viewModel.uiStateFlow.value.unitFrom?.unitId)
|
||||
assertEquals(initialFrom, viewModel.uiStateFlow.value.unitTo?.unitId)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes [input] sequence as a single string (e.g. "123-23") and compares it with [output]
|
||||
* (internal) and [outputDisplay] (the that user sees).
|
||||
* Takes [input] sequence as a single string (e.g. "123-23") and compares it with [output].
|
||||
*/
|
||||
private fun inputOutputTest(input: String, output: String, outputDisplay: String) {
|
||||
private fun inputOutputTest(input: String, output: String) {
|
||||
// Enter everything
|
||||
input.forEach {
|
||||
viewModel.processInput(it.toString())
|
||||
}
|
||||
assertEquals(output, viewModel.input.value)
|
||||
assertEquals(outputDisplay, viewModel.mainFlow.value.inputValue)
|
||||
assertEquals(output, viewModel.uiStateFlow.value.inputValue)
|
||||
viewModel.clearInput()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user