mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 00:35:26 +02:00
Better approach for Flows
This commit is contained in:
parent
5b56c60e32
commit
d758712413
@ -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")
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) },
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user