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")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
implementation("androidx.navigation:navigation-compose:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01")
// Material Design 3
implementation("androidx.compose.material3:material3:1.0.0-alpha16")

View File

@ -135,7 +135,7 @@ fun UnittoApp(
navigateUp = { navController.navigateUp() },
navigateToSettingsActtion = { navController.navigate(UNIT_GROUPS_SCREEN) },
selectAction = { mainViewModel.changeUnitTo(it) },
inputValue = mainViewModel.mainUIState.inputValue.toBigDecimal(),
inputValue = mainViewModel.mainFlow.value.inputValue.toBigDecimal(),
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
@ -59,6 +60,8 @@ fun MainScreen(
) {
var launched: Boolean by rememberSaveable { mutableStateOf(false) }
val mainScreenUIState = viewModel.mainFlow.collectAsStateWithLifecycle()
Scaffold(
modifier = Modifier,
topBar = {
@ -84,7 +87,7 @@ fun MainScreen(
unitFrom = viewModel.unitFrom,
unitTo = viewModel.unitTo,
portrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
mainScreenUIState = viewModel.mainUIState,
mainScreenUIState = mainScreenUIState.value,
navControllerAction = { navControllerAction(it) },
swapMeasurements = { viewModel.swapUnits() },
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.CurrencyUnitResponse
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.stateIn
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
@ -57,6 +61,11 @@ class MainViewModel @Inject constructor(
) : ViewModel() {
private var userPrefs = UserPreferences()
/**
* UI state
*/
private val _mainUIState = MutableStateFlow(MainScreenUIState())
/**
* Unit we converting from (left side)
*/
@ -69,11 +78,16 @@ class MainViewModel @Inject constructor(
var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile))
private set
/**
* UI state
*/
var mainUIState: MainScreenUIState by mutableStateOf(MainScreenUIState())
private set
val mainFlow = combine(_mainUIState, userPrefsRepository.userPreferencesFlow) { UIState, prefs ->
userPrefs = prefs
convertValue()
return@combine UIState
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MainScreenUIState()
)
/**
* 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 =
unitFrom.convert(
unitTo,
mainUIState.inputValue.toBigDecimal(),
_mainUIState.value.inputValue.toBigDecimal(),
userPrefs.digitsPrecision
)
@ -93,7 +107,11 @@ class MainViewModel @Inject constructor(
* is zero, than we make sure there are no trailing zeros.
*/
val resultValue =
if (convertedValue == BigDecimal.ZERO.setScale(userPrefs.digitsPrecision, RoundingMode.HALF_EVEN)) {
if (convertedValue == BigDecimal.ZERO.setScale(
userPrefs.digitsPrecision,
RoundingMode.HALF_EVEN
)
) {
KEY_0
} else {
// Setting result value using a specified OutputFormat
@ -103,7 +121,7 @@ class MainViewModel @Inject constructor(
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
// 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
if (!clickedUnit.group.canNegate) {
mainUIState =
mainUIState.copy(inputValue = mainUIState.inputValue.removePrefix(KEY_MINUS))
_mainUIState.value =
_mainUIState.value.copy(
inputValue = _mainUIState.value.inputValue.removePrefix(
KEY_MINUS
)
)
}
// Now setting up right unit (pair for the left one)
@ -192,12 +215,12 @@ class MainViewModel @Inject constructor(
*/
private suspend fun updateCurrenciesBasicUnits() {
// 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
if (unitFrom.group != UnitGroup.CURRENCY) return
// Starting to load stuff
mainUIState = mainUIState.copy(isLoadingNetwork = true)
_mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = true)
try {
val pairs: CurrencyUnitResponse =
@ -223,10 +246,10 @@ class MainViewModel @Inject constructor(
FirebaseHelper().recordException(e)
}
}
mainUIState = mainUIState.copy(showError = true)
_mainUIState.value = _mainUIState.value.copy(showError = true)
} finally {
// 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
// Disabling dot button to avoid multiple dots in input value
// Enabling delete button to so that we can delete this dot from input
mainUIState = mainUIState.copy(
inputValue = mainUIState.inputValue + digitToAdd,
_mainUIState.value = _mainUIState.value.copy(
inputValue = _mainUIState.value.inputValue + digitToAdd,
dotButtonEnabled = false,
deleteButtonEnabled = true
)
}
KEY_0 -> {
// We shouldn't add zero to another zero in input, i.e. 00
if (mainUIState.inputValue != KEY_0) {
mainUIState = mainUIState.copy(inputValue = mainUIState.inputValue + digitToAdd)
if (_mainUIState.value.inputValue != KEY_0) {
_mainUIState.value =
_mainUIState.value.copy(inputValue = _mainUIState.value.inputValue + digitToAdd)
}
}
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,
avoids input to be like 03 (with this check it will be just 3)
*/
mainUIState = mainUIState.copy(
inputValue = if (mainUIState.inputValue == KEY_0) digitToAdd else mainUIState.inputValue + digitToAdd,
_mainUIState.value = _mainUIState.value.copy(
inputValue = if (_mainUIState.value.inputValue == KEY_0) digitToAdd else _mainUIState.value.inputValue + digitToAdd,
deleteButtonEnabled = true
)
}
@ -289,12 +313,13 @@ class MainViewModel @Inject constructor(
fun deleteDigit() {
// Last symbol is a dot
// We enable DOT button
if (mainUIState.inputValue.endsWith(KEY_DOT)) {
mainUIState = mainUIState.copy(dotButtonEnabled = true)
if (_mainUIState.value.inputValue.endsWith(KEY_DOT)) {
_mainUIState.value = _mainUIState.value.copy(dotButtonEnabled = true)
}
// 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
@ -304,9 +329,10 @@ class MainViewModel @Inject constructor(
Skipping this block means that we are left we acceptable value, i.e. 123.03
*/
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
@ -317,7 +343,7 @@ class MainViewModel @Inject constructor(
* Clears input value and sets it to default (ZERO)
*/
fun clearInput() {
mainUIState = mainUIState.copy(
_mainUIState.value = _mainUIState.value.copy(
inputValue = KEY_0,
deleteButtonEnabled = false,
dotButtonEnabled = true
@ -329,13 +355,13 @@ class MainViewModel @Inject constructor(
* Changes input from positive to negative and vice versa
*/
fun negateInput() {
mainUIState = mainUIState.copy(
inputValue = if (mainUIState.inputValue.getOrNull(0) != KEY_MINUS.single()) {
_mainUIState.value = _mainUIState.value.copy(
inputValue = if (_mainUIState.value.inputValue.getOrNull(0) != KEY_MINUS.single()) {
// If input doesn't have minus at the beginning, we give it to it
KEY_MINUS + mainUIState.inputValue
KEY_MINUS + _mainUIState.value.inputValue
} else {
// Input has minus, meaning we need to remove it
mainUIState.inputValue.removePrefix(KEY_MINUS)
_mainUIState.value.inputValue.removePrefix(KEY_MINUS)
}
)
convertValue()
@ -348,20 +374,6 @@ class MainViewModel @Inject constructor(
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 {
viewModelScope.launch {
userPrefs = userPrefsRepository.userPreferencesFlow.first()
@ -381,17 +393,17 @@ class MainViewModel @Inject constructor(
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
val allBasedUnits = basedUnitRepository.getAll()
allUnitsRepository.loadFromDatabase(application, allBasedUnits)
// 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()
convertValue()
observePreferenceChanges()
}
}
}

View File

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