Better approach for Flows

This commit is contained in:
Sad Ellie 2022-08-20 13:10:11 +03:00
parent 5b56c60e32
commit d758712413
5 changed files with 67 additions and 50 deletions

View File

@ -156,6 +156,7 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion") implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
implementation("androidx.navigation:navigation-compose:2.5.1") implementation("androidx.navigation:navigation-compose:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01")
// Material Design 3 // Material Design 3
implementation("androidx.compose.material3:material3:1.0.0-alpha16") implementation("androidx.compose.material3:material3:1.0.0-alpha16")

View File

@ -135,7 +135,7 @@ fun UnittoApp(
navigateUp = { navController.navigateUp() }, navigateUp = { navController.navigateUp() },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) }, navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
selectAction = { mainViewModel.changeUnitTo(it) }, selectAction = { mainViewModel.changeUnitTo(it) },
inputValue = mainViewModel.mainUIState.inputValue.toBigDecimal(), inputValue = mainViewModel.mainFlow.value.inputValue.toBigDecimal(),
unitFrom = mainViewModel.unitFrom unitFrom = mainViewModel.unitFrom
) )
} }

View File

@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
@ -59,6 +60,8 @@ fun MainScreen(
) { ) {
var launched: Boolean by rememberSaveable { mutableStateOf(false) } var launched: Boolean by rememberSaveable { mutableStateOf(false) }
val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle()
Scaffold( Scaffold(
modifier = Modifier, modifier = Modifier,
topBar = { topBar = {
@ -84,7 +87,7 @@ fun MainScreen(
unitFrom = viewModel.unitFrom, unitFrom = viewModel.unitFrom,
unitTo = viewModel.unitTo, unitTo = viewModel.unitTo,
portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT, portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
mainScreenUIState = viewModel.mainUIState, mainScreenUIState = mainScreenUIState.value,
navControllerAction = { navControllerAction(it) }, navControllerAction = { navControllerAction(it) },
swapMeasurements = { viewModel.swapUnits() }, swapMeasurements = { viewModel.swapUnits() },
processInput = { viewModel.processInput(it) }, processInput = { viewModel.processInput(it) },

View File

@ -42,7 +42,11 @@ import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
import com.sadellie.unitto.data.units.remote.CurrencyApi import com.sadellie.unitto.data.units.remote.CurrencyApi
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@ -57,6 +61,11 @@ class MainViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private var userPrefs = UserPreferences() private var userPrefs = UserPreferences()
/**
* UI state
*/
private val _mainUIState = MutableStateFlow(MainScreenUIState())
/** /**
* Unit we converting from (left side) * Unit we converting from (left side)
*/ */
@ -69,11 +78,16 @@ class MainViewModel @Inject constructor(
var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile)) var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile))
private set private set
/** val mainFlow = combine(_mainUIState, userPrefsRepository.userPreferencesFlow) { UIState, prefs ->
* UI state userPrefs = prefs
*/ convertValue()
var mainUIState: MainScreenUIState by mutableStateOf(MainScreenUIState()) return@combine UIState
private set }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MainScreenUIState()
)
/** /**
* This function takes local variables, converts values and then causes the UI to update * This function takes local variables, converts values and then causes the UI to update
@ -83,7 +97,7 @@ class MainViewModel @Inject constructor(
val convertedValue: BigDecimal = val convertedValue: BigDecimal =
unitFrom.convert( unitFrom.convert(
unitTo, unitTo,
mainUIState.inputValue.toBigDecimal(), _mainUIState.value.inputValue.toBigDecimal(),
userPrefs.digitsPrecision userPrefs.digitsPrecision
) )
@ -93,7 +107,11 @@ class MainViewModel @Inject constructor(
* is zero, than we make sure there are no trailing zeros. * is zero, than we make sure there are no trailing zeros.
*/ */
val resultValue = val resultValue =
if (convertedValue == BigDecimal.ZERO.setScale(userPrefs.digitsPrecision, RoundingMode.HALF_EVEN)) { if (convertedValue == BigDecimal.ZERO.setScale(
userPrefs.digitsPrecision,
RoundingMode.HALF_EVEN
)
) {
KEY_0 KEY_0
} else { } else {
// Setting result value using a specified OutputFormat // Setting result value using a specified OutputFormat
@ -103,7 +121,7 @@ class MainViewModel @Inject constructor(
else -> convertedValue.toPlainString() else -> convertedValue.toPlainString()
} }
} }
mainUIState = mainUIState.copy(resultValue = resultValue) _mainUIState.value = _mainUIState.value.copy(resultValue = resultValue)
} }
/** /**
@ -116,11 +134,16 @@ class MainViewModel @Inject constructor(
unitFrom = clickedUnit unitFrom = clickedUnit
// Now we check for negate button // Now we check for negate button
mainUIState = mainUIState.copy(negateButtonEnabled = clickedUnit.group.canNegate) _mainUIState.value =
_mainUIState.value.copy(negateButtonEnabled = clickedUnit.group.canNegate)
// Now we change to positive if the group we switched to supports negate // Now we change to positive if the group we switched to supports negate
if (!clickedUnit.group.canNegate) { if (!clickedUnit.group.canNegate) {
mainUIState = _mainUIState.value =
mainUIState.copy(inputValue = mainUIState.inputValue.removePrefix(KEY_MINUS)) _mainUIState.value.copy(
inputValue = _mainUIState.value.inputValue.removePrefix(
KEY_MINUS
)
)
} }
// Now setting up right unit (pair for the left one) // Now setting up right unit (pair for the left one)
@ -192,12 +215,12 @@ class MainViewModel @Inject constructor(
*/ */
private suspend fun updateCurrenciesBasicUnits() { private suspend fun updateCurrenciesBasicUnits() {
// Resetting error and network loading states in case we are not gonna do anything below // Resetting error and network loading states in case we are not gonna do anything below
mainUIState = mainUIState.copy(isLoadingNetwork = false, showError = false) _mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = false, showError = false)
// We update currencies only when needed // We update currencies only when needed
if (unitFrom.group != UnitGroup.CURRENCY) return if (unitFrom.group != UnitGroup.CURRENCY) return
// Starting to load stuff // Starting to load stuff
mainUIState = mainUIState.copy(isLoadingNetwork = true) _mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = true)
try { try {
val pairs: CurrencyUnitResponse = val pairs: CurrencyUnitResponse =
@ -223,10 +246,10 @@ class MainViewModel @Inject constructor(
FirebaseHelper().recordException(e) FirebaseHelper().recordException(e)
} }
} }
mainUIState = mainUIState.copy(showError = true) _mainUIState.value = _mainUIState.value.copy(showError = true)
} finally { } finally {
// Loaded // Loaded
mainUIState = mainUIState.copy(isLoadingNetwork = false) _mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = false)
} }
} }
@ -256,16 +279,17 @@ class MainViewModel @Inject constructor(
// Here we add a dot to input // Here we add a dot to input
// Disabling dot button to avoid multiple dots in input value // Disabling dot button to avoid multiple dots in input value
// Enabling delete button to so that we can delete this dot from input // Enabling delete button to so that we can delete this dot from input
mainUIState = mainUIState.copy( _mainUIState.value = _mainUIState.value.copy(
inputValue = mainUIState.inputValue + digitToAdd, inputValue = _mainUIState.value.inputValue + digitToAdd,
dotButtonEnabled = false, dotButtonEnabled = false,
deleteButtonEnabled = true deleteButtonEnabled = true
) )
} }
KEY_0 -> { KEY_0 -> {
// We shouldn't add zero to another zero in input, i.e. 00 // We shouldn't add zero to another zero in input, i.e. 00
if (mainUIState.inputValue != KEY_0) { if (_mainUIState.value.inputValue != KEY_0) {
mainUIState = mainUIState.copy(inputValue = mainUIState.inputValue + digitToAdd) _mainUIState.value =
_mainUIState.value.copy(inputValue = _mainUIState.value.inputValue + digitToAdd)
} }
} }
else -> { else -> {
@ -274,8 +298,8 @@ class MainViewModel @Inject constructor(
When there is just a zero, we should replace it with the digit we want to add, When there is just a zero, we should replace it with the digit we want to add,
avoids input to be like 03 (with this check it will be just 3) avoids input to be like 03 (with this check it will be just 3)
*/ */
mainUIState = mainUIState.copy( _mainUIState.value = _mainUIState.value.copy(
inputValue = if (mainUIState.inputValue == KEY_0) digitToAdd else mainUIState.inputValue + digitToAdd, inputValue = if (_mainUIState.value.inputValue == KEY_0) digitToAdd else _mainUIState.value.inputValue + digitToAdd,
deleteButtonEnabled = true deleteButtonEnabled = true
) )
} }
@ -289,12 +313,13 @@ class MainViewModel @Inject constructor(
fun deleteDigit() { fun deleteDigit() {
// Last symbol is a dot // Last symbol is a dot
// We enable DOT button // We enable DOT button
if (mainUIState.inputValue.endsWith(KEY_DOT)) { if (_mainUIState.value.inputValue.endsWith(KEY_DOT)) {
mainUIState = mainUIState.copy(dotButtonEnabled = true) _mainUIState.value = _mainUIState.value.copy(dotButtonEnabled = true)
} }
// Deleting last symbol // Deleting last symbol
mainUIState = mainUIState.copy(inputValue = mainUIState.inputValue.dropLast(1)) _mainUIState.value =
_mainUIState.value.copy(inputValue = _mainUIState.value.inputValue.dropLast(1))
/* /*
Now we check what we have left Now we check what we have left
@ -304,9 +329,10 @@ class MainViewModel @Inject constructor(
Skipping this block means that we are left we acceptable value, i.e. 123.03 Skipping this block means that we are left we acceptable value, i.e. 123.03
*/ */
if ( if (
mainUIState.inputValue in listOf(String(), KEY_MINUS, KEY_0) _mainUIState.value.inputValue in listOf(String(), KEY_MINUS, KEY_0)
) { ) {
mainUIState = mainUIState.copy(deleteButtonEnabled = false, inputValue = KEY_0) _mainUIState.value =
_mainUIState.value.copy(deleteButtonEnabled = false, inputValue = KEY_0)
} }
// We are sure that input has acceptable value, so we convert it // We are sure that input has acceptable value, so we convert it
@ -317,7 +343,7 @@ class MainViewModel @Inject constructor(
* Clears input value and sets it to default (ZERO) * Clears input value and sets it to default (ZERO)
*/ */
fun clearInput() { fun clearInput() {
mainUIState = mainUIState.copy( _mainUIState.value = _mainUIState.value.copy(
inputValue = KEY_0, inputValue = KEY_0,
deleteButtonEnabled = false, deleteButtonEnabled = false,
dotButtonEnabled = true dotButtonEnabled = true
@ -329,13 +355,13 @@ class MainViewModel @Inject constructor(
* Changes input from positive to negative and vice versa * Changes input from positive to negative and vice versa
*/ */
fun negateInput() { fun negateInput() {
mainUIState = mainUIState.copy( _mainUIState.value = _mainUIState.value.copy(
inputValue = if (mainUIState.inputValue.getOrNull(0) != KEY_MINUS.single()) { inputValue = if (_mainUIState.value.inputValue.getOrNull(0) != KEY_MINUS.single()) {
// If input doesn't have minus at the beginning, we give it to it // If input doesn't have minus at the beginning, we give it to it
KEY_MINUS + mainUIState.inputValue KEY_MINUS + _mainUIState.value.inputValue
} else { } else {
// Input has minus, meaning we need to remove it // Input has minus, meaning we need to remove it
mainUIState.inputValue.removePrefix(KEY_MINUS) _mainUIState.value.inputValue.removePrefix(KEY_MINUS)
} }
) )
convertValue() convertValue()
@ -348,20 +374,6 @@ class MainViewModel @Inject constructor(
userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo) userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo)
} }
/**
* Observes changes in user preferences and updated values in this ViewModel.
*
* Any change in user preferences will update mutableStateOf so composables, that rely on this
* values recompose when actually needed. For example, when you change output format, composable
* in MainActivity will not be recomposed.
*/
private suspend fun observePreferenceChanges() {
userPrefsRepository.userPreferencesFlow.collect {
userPrefs = it
convertValue()
}
}
init { init {
viewModelScope.launch { viewModelScope.launch {
userPrefs = userPrefsRepository.userPreferencesFlow.first() userPrefs = userPrefsRepository.userPreferencesFlow.first()
@ -381,17 +393,17 @@ class MainViewModel @Inject constructor(
allUnitsRepository.getById(MyUnitIDS.mile) allUnitsRepository.getById(MyUnitIDS.mile)
} }
mainUIState = mainUIState.copy(negateButtonEnabled = unitFrom.group.canNegate) _mainUIState.value =
_mainUIState.value.copy(negateButtonEnabled = unitFrom.group.canNegate)
// Now we load units data from database // Now we load units data from database
val allBasedUnits = basedUnitRepository.getAll() val allBasedUnits = basedUnitRepository.getAll()
allUnitsRepository.loadFromDatabase(application, allBasedUnits) allUnitsRepository.loadFromDatabase(application, allBasedUnits)
// User is free to convert values and units on units screen can be sorted properly // User is free to convert values and units on units screen can be sorted properly
mainUIState = mainUIState.copy(isLoadingDatabase = false) _mainUIState.value = _mainUIState.value.copy(isLoadingDatabase = false)
updateCurrenciesBasicUnits() updateCurrenciesBasicUnits()
convertValue() convertValue()
observePreferenceChanges()
} }
} }
} }

View File

@ -30,6 +30,7 @@ allprojects {
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi", "-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
"-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi"
) )
} }
} }