diff --git a/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt b/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt deleted file mode 100644 index aa2cee3e..00000000 --- a/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt +++ /dev/null @@ -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() - 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) - } -} diff --git a/app/src/main/java/com/sadellie/unitto/MainActivity.kt b/app/src/main/java/com/sadellie/unitto/MainActivity.kt index 8bb35bf9..f2c48dea 100644 --- a/app/src/main/java/com/sadellie/unitto/MainActivity.kt +++ b/app/src/main/java/com/sadellie/unitto/MainActivity.kt @@ -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 ) } diff --git a/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt index 77b2533c..5c580253 100644 --- a/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt +++ b/app/src/main/java/com/sadellie/unitto/data/FlowUtils.kt @@ -21,16 +21,17 @@ package com.sadellie.unitto.data import kotlinx.coroutines.flow.Flow @Suppress("UNCHECKED_CAST") -fun combine( +fun combine( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R + flow7: Flow, + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R ): Flow = - 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 combine( args[3] as T4, args[4] as T5, args[5] as T6, + args[6] as T7, ) } diff --git a/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt index c94b1a63..a80d4945 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/Formatter.kt @@ -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 } diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt index 85830544..f3a8de26 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreen.kt @@ -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, ) } ) diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreenUIState.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreenUIState.kt index b32e77ac..666909dd 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainScreenUIState.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainScreenUIState.kt @@ -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, +} diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt index 59dbaf9d..b5f3a724 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/MainViewModel.kt @@ -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 = MutableStateFlow(KEY_0) - private val _calculated: MutableStateFlow = MutableStateFlow(null) - private val _result: MutableStateFlow = MutableStateFlow(KEY_0) - private val _latestInputStack: MutableList = mutableListOf(KEY_0) - private val _inputDisplay: MutableStateFlow = MutableStateFlow(KEY_0) - private val _isLoadingDatabase: MutableStateFlow = MutableStateFlow(true) - private val _isLoadingNetwork: MutableStateFlow = MutableStateFlow(false) - private val _showError: MutableStateFlow = MutableStateFlow(false) private val _userPrefs = userPrefsRepository.userPreferencesFlow.stateIn( - viewModelScope, SharingStarted.WhileSubscribed(5000), UserPreferences() + viewModelScope, + SharingStarted.WhileSubscribed(5000), + UserPreferences() ) - val mainFlow: StateFlow = combine( - _inputDisplay, + /** + * Unit on the left, the one we convert from. Initially null, meaning we didn't restore it yet. + */ + private val _unitFrom: MutableStateFlow = MutableStateFlow(null) + + /** + * Unit on the right, the one we convert to. Initially null, meaning we didn't restore it yet. + */ + private val _unitTo: MutableStateFlow = MutableStateFlow(null) + + /** + * Current input. Used when converting units. + */ + private val _input: MutableStateFlow = MutableStateFlow(KEY_0) + + /** + * Calculation result. Null when [_input] is not an expression. + */ + private val _calculated: MutableStateFlow = MutableStateFlow(null) + + /** + * List of latest symbols that were entered. + */ + private val _latestInputStack: MutableList = mutableListOf(_input.value) + + /** + * Conversion result. + */ + private val _result: MutableStateFlow = MutableStateFlow(KEY_0) + + /** + * True when loading something from network. + */ + private val _showLoading: MutableStateFlow = MutableStateFlow(false) + + /** + * True if there was error while loading data. + */ + private val _showError: MutableStateFlow = MutableStateFlow(false) + + /** + * Current state of UI. + */ + val uiStateFlow: StateFlow = 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,398 +496,116 @@ 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 + unitId = unit.unitId, + isFavorite = unit.isFavorite, + pairedUnitId = unit.pairedUnit, + // This will increment counter on unit in list too + frequency = ++unit.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, - // This will increment counter on unit in list too - frequency = ++unit.counter + private fun updatePairedUnit(unit: AbstractUnit) { + viewModelScope.launch(Dispatchers.IO) { + basedUnitRepository.insertUnits( + MyBasedUnit( + unitId = unit.unitId, + isFavorite = unit.isFavorite, + pairedUnitId = unit.pairedUnit, + 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 } - _showError.update { false } - // We update currencies only when needed - if (unitFrom.group != UnitGroup.CURRENCY) return - - // Starting to load stuff - _isLoadingNetwork.update { true } - - try { - val pairs: CurrencyUnitResponse = - CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId) - allUnitsRepository.updateBasicUnitsForCurrencies(pairs.currency) - } catch (e: Exception) { - when (e) { - // 403, Network and Adapter exceptions can be ignored - is retrofit2.HttpException, is java.net.UnknownHostException, is com.squareup.moshi.JsonDataException -> {} - else -> { - // Unexpected exception, should report it - FirebaseHelper().recordException(e) - } - } - _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() - } - } + private fun updateCurrenciesRatesIfNeeded() { + viewModelScope.launch(Dispatchers.IO) { + _showError.update { false } + _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 + _showLoading.update { true } - /** - * 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) - } + try { + val pairs: CurrencyUnitResponse = + CurrencyApi.retrofitService.getCurrencyPairs(unitFrom.unitId) + allUnitsRepository.updateBasicUnitsForCurrencies(pairs.currency) + } catch (e: Exception) { + when (e) { + // 403, Network and Adapter exceptions can be ignored + is retrofit2.HttpException, is java.net.UnknownHostException, is com.squareup.moshi.JsonDataException -> {} 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) + // Unexpected exception, should report it + FirebaseHelper().recordException(e) } } + _showError.update { true } + } finally { + _showLoading.update { false } } } } - /** - * 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) } + 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) } } - /** - * 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) - } - } - } - - /** - * 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() { - userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo) - } - private fun startObserving() { viewModelScope.launch(Dispatchers.Default) { - _userPrefs.collectLatest { convertInput() } + merge(_input, _unitFrom, _unitTo, _userPrefs).collectLatest { convertInput() } } - viewModelScope.launch(Dispatchers.Default) { - input.collectLatest { convertInput() } + } + + private fun loadInitialUnitPair() { + viewModelScope.launch(Dispatchers.IO) { + val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first() + + // First we load latest pair of units + _unitFrom.update { + try { + allUnitsRepository.getById(initialUserPrefs.latestLeftSideUnit) + } catch (e: java.util.NoSuchElementException) { + allUnitsRepository.getById(MyUnitIDS.kilometer) + } + } + + _unitTo.update { + try { + allUnitsRepository.getById(initialUserPrefs.latestRightSideUnit) + } catch (e: java.util.NoSuchElementException) { + allUnitsRepository.getById(MyUnitIDS.mile) + } + } + + updateCurrenciesRatesIfNeeded() } } init { - viewModelScope.launch { - val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first() - - // First we load latest pair of units - unitFrom = try { - allUnitsRepository.getById(initialUserPrefs.latestLeftSideUnit) - } catch (e: java.util.NoSuchElementException) { - allUnitsRepository.getById(MyUnitIDS.kilometer) - } - - unitTo = 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() - - startObserving() - } + loadInitialUnitPair() + startObserving() } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/Keyboard.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/Keyboard.kt index 93e2dfc6..5d664cca 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/Keyboard.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/Keyboard.kt @@ -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,67 +101,91 @@ 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) - KeyboardButton(bModifier, KEY_EXPONENT, isPrimary = false, onClick = { addDigit(KEY_EXPONENT) }) - KeyboardButton(bModifier, KEY_SQRT, isPrimary = false, onClick = { addDigit(KEY_SQRT) }) - } - Row(cModifier) { - KeyboardButton(bModifier, KEY_7, onClick = addDigit) - KeyboardButton(bModifier, KEY_8, onClick = addDigit) - KeyboardButton(bModifier, KEY_9, onClick = addDigit) - KeyboardButton(bModifier, KEY_DIVIDE_DISPLAY, isPrimary = false) { addDigit(KEY_DIVIDE) } - } - Row(cModifier) { - KeyboardButton(bModifier, KEY_4, onClick = addDigit) - KeyboardButton(bModifier, KEY_5, onClick = addDigit) - KeyboardButton(bModifier, KEY_6, onClick = addDigit) - KeyboardButton(bModifier, KEY_MULTIPLY_DISPLAY, isPrimary = false) { addDigit(KEY_MULTIPLY) } - } - Row(cModifier) { - KeyboardButton(bModifier, KEY_1, onClick = addDigit) - KeyboardButton(bModifier, KEY_2, onClick = addDigit) - KeyboardButton(bModifier, KEY_3, onClick = addDigit) - KeyboardButton(bModifier, KEY_MINUS_DISPLAY, isPrimary = false) { addDigit(KEY_MINUS) } - } - Row(cModifier) { - KeyboardButton(bModifier, KEY_0, onClick = addDigit) - KeyboardButton(bModifier, Formatter.fractional) { addDigit(KEY_DOT) } - KeyboardButton(bModifier, KEY_CLEAR, onLongClick = clearInput) { deleteDigit() } - KeyboardButton(bModifier, KEY_PLUS, isPrimary = false) { addDigit(KEY_PLUS) } - } + Row(cModifier) { + KeyboardButton(bModifier, KEY_LEFT_BRACKET, isPrimary = false, onClick = addDigit) + KeyboardButton(bModifier, KEY_RIGHT_BRACKET, isPrimary = false, onClick = addDigit) + KeyboardButton(bModifier, KEY_EXPONENT, isPrimary = false, onClick = { addDigit(KEY_EXPONENT) }) + KeyboardButton(bModifier, KEY_SQRT, isPrimary = false, onClick = { addDigit(KEY_SQRT) }) + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_7, onClick = addDigit) + KeyboardButton(bModifier, KEY_8, onClick = addDigit) + KeyboardButton(bModifier, KEY_9, onClick = addDigit) + KeyboardButton(bModifier, KEY_DIVIDE_DISPLAY, isPrimary = false) { addDigit(KEY_DIVIDE) } + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_4, onClick = addDigit) + KeyboardButton(bModifier, KEY_5, onClick = addDigit) + KeyboardButton(bModifier, KEY_6, onClick = addDigit) + KeyboardButton(bModifier, KEY_MULTIPLY_DISPLAY, isPrimary = false) { addDigit(KEY_MULTIPLY) } + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_1, onClick = addDigit) + KeyboardButton(bModifier, KEY_2, onClick = addDigit) + KeyboardButton(bModifier, KEY_3, onClick = addDigit) + KeyboardButton(bModifier, KEY_MINUS_DISPLAY, isPrimary = false) { addDigit(KEY_MINUS) } + } + Row(cModifier) { + KeyboardButton(bModifier, KEY_0, onClick = addDigit) + KeyboardButton(bModifier, Formatter.fractional) { addDigit(KEY_DOT) } + KeyboardButton(bModifier, KEY_CLEAR, onLongClick = clearInput) { deleteDigit() } + KeyboardButton(bModifier, KEY_PLUS, isPrimary = false) { addDigit(KEY_PLUS) } + } + } +} + +@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() } } } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt index 83449ac6..126d56e9 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/TopScreen.kt @@ -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, ) } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/main/components/UnitSelectionButton.kt b/app/src/main/java/com/sadellie/unitto/screens/main/components/UnitSelectionButton.kt index 927b2724..51756e50 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/main/components/UnitSelectionButton.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/main/components/UnitSelectionButton.kt @@ -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 diff --git a/app/src/main/java/com/sadellie/unitto/screens/second/SecondViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/second/SecondViewModel.kt index 168277e6..5a65f448 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/second/SecondViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/second/SecondViewModel.kt @@ -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() + } } diff --git a/app/src/test/java/com/sadellie/unitto/screens/FormatterTest.kt b/app/src/test/java/com/sadellie/unitto/screens/FormatterTest.kt index a3553ce2..bdb39d37 100644 --- a/app/src/test/java/com/sadellie/unitto/screens/FormatterTest.kt +++ b/app/src/test/java/com/sadellie/unitto/screens/FormatterTest.kt @@ -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)) } diff --git a/app/src/test/java/com/sadellie/unitto/screens/MainViewModelTest.kt b/app/src/test/java/com/sadellie/unitto/screens/MainViewModelTest.kt index 41aff90d..f0ad64a9 100644 --- a/app/src/test/java/com/sadellie/unitto/screens/MainViewModelTest.kt +++ b/app/src/test/java/com/sadellie/unitto/screens/MainViewModelTest.kt @@ -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() } }