Refactored repository for DataStore

Also renamed app theme from AppTheme to UnittoTheme so that it's less confusing
This commit is contained in:
Sad Ellie 2022-05-31 21:58:58 +03:00
parent 37826b7177
commit 941e260a15
6 changed files with 192 additions and 149 deletions

View File

@ -5,7 +5,7 @@ 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.UserPreferences
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
import com.sadellie.unitto.data.units.ALL_UNITS
import com.sadellie.unitto.data.units.MyUnitIDS
import com.sadellie.unitto.data.units.database.MyBasedUnitDatabase
@ -26,7 +26,7 @@ class SwapUnitsTest {
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
viewModel = MainViewModel(
UserPreferences(context),
UserPreferencesRepository(context),
MyBasedUnitsRepository(
Room.inMemoryDatabaseBuilder(
context,

View File

@ -1,6 +1,5 @@
package com.sadellie.unitto
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -8,8 +7,6 @@ import androidx.activity.viewModels
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
@ -24,7 +21,7 @@ import com.sadellie.unitto.screens.about.AboutScreen
import com.sadellie.unitto.screens.main.MainScreen
import com.sadellie.unitto.screens.second.SecondScreen
import com.sadellie.unitto.screens.setttings.SettingsScreen
import com.sadellie.unitto.ui.theme.AppTheme
import com.sadellie.unitto.ui.theme.UnittoTheme
import dagger.hilt.android.AndroidEntryPoint
@ -37,11 +34,11 @@ class MainActivity : ComponentActivity() {
setContent {
val navController = rememberNavController()
val currentAppTheme: Int by mainViewModel.currentAppTheme.collectAsState(AppTheme.NOT_SET)
val currentAppTheme: Int = mainViewModel.currentTheme
// We don't draw anything until we know what theme we need to use
if (currentAppTheme != AppTheme.NOT_SET) {
AppTheme(
UnittoTheme(
currentAppTheme = currentAppTheme
) {
UnittoApp(
@ -54,7 +51,7 @@ class MainActivity : ComponentActivity() {
}
override fun onPause() {
mainViewModel.saveMe()
mainViewModel.saveLatestPairOfUnits()
super.onPause()
}
}

View File

@ -1,8 +1,11 @@
package com.sadellie.unitto.data.preferences
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.MyUnitIDS
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ -14,9 +17,39 @@ import javax.inject.Inject
private val Context.settingsDataStore by preferencesDataStore("settings")
/**
* Represents user preferences that are user across the app
*
* @property currentAppTheme Current [AppTheme] to be used
* @property digitsPrecision Current [PRECISIONS]. Number of digits in fractional part
* @property separator Current [Separator] that used to separate thousands
* @property outputFormat Current [OutputFormat] that is applied to converted value (not input)
* @property latestLeftSideUnit Latest [AbstractUnit] that was on the left side
* @property latestRightSideUnit Latest [AbstractUnit] that was on the right side
* @property enableAnalytics Whether or not user wants to share application usage data
*/
data class UserPreferences(
val currentAppTheme: Int,
val digitsPrecision: Int,
val separator: Int,
val outputFormat: Int,
val latestLeftSideUnit: String,
val latestRightSideUnit: String,
val enableAnalytics: Boolean
)
/**
* Repository that works with DataStore
*
* @property context
*/
class UserPreferencesRepository @Inject constructor(@ApplicationContext private val context: Context) {
private val dataStore: DataStore<Preferences> = context.settingsDataStore
/**
* Keys for DataStore
*/
object UserPreferenceKeys {
private object PrefsKeys {
val CURRENT_APP_THEME = intPreferencesKey("CURRENT_APP_THEME")
val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY")
val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY")
@ -24,71 +57,102 @@ object UserPreferenceKeys {
val LATEST_LEFT_SIDE = stringPreferencesKey("LATEST_LEFT_SIDE_PREF_KEY")
val LATEST_RIGHT_SIDE = stringPreferencesKey("LATEST_RIGHT_SIDE_PREF_KEY")
val ENABLE_ANALYTICS = booleanPreferencesKey("ENABLE_ANALYTICS_PREF_KEY")
}
}
/**
* Repository that works with DataStore
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.map { preferences ->
val currentAppTheme: Int =
preferences[PrefsKeys.CURRENT_APP_THEME] ?: AppTheme.AUTO
val digitsPrecision: Int =
preferences[PrefsKeys.DIGITS_PRECISION] ?: 3
val separator: Int =
preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACES
val outputFormat: Int =
preferences[PrefsKeys.OUTPUT_FORMAT] ?: OutputFormat.PLAIN
val latestLeftSideUnit: String =
preferences[PrefsKeys.LATEST_LEFT_SIDE] ?: MyUnitIDS.kilometer
val latestRightSideUnit: String =
preferences[PrefsKeys.LATEST_RIGHT_SIDE] ?: MyUnitIDS.mile
val enableAnalytics: Boolean =
preferences[PrefsKeys.ENABLE_ANALYTICS] ?: true
UserPreferences(
currentAppTheme = currentAppTheme,
digitsPrecision = digitsPrecision,
separator = separator,
outputFormat = outputFormat,
latestLeftSideUnit = latestLeftSideUnit,
latestRightSideUnit = latestRightSideUnit,
enableAnalytics = enableAnalytics
)
}
/**
* Update current theme preference in DataStore
*
* @property context
* @param appTheme [AppTheme] to change to
*/
class UserPreferences @Inject constructor(@ApplicationContext private val context: Context) {
suspend fun updateCurrentAppTheme(appTheme: Int) {
dataStore.edit { preferences ->
preferences[PrefsKeys.CURRENT_APP_THEME] = appTheme
}
}
/**
* Gets string from datastore
* Update precision preference in DataStore
*
* @param[default] Value to return if didn't find anything on fresh install
* @param precision One of the [PRECISIONS] to change to
*/
fun getItem(key: Preferences.Key<String>, default: String): Flow<String> {
return context.settingsDataStore.data.map {
it[key] ?: default
suspend fun updateDigitsPrecision(precision: Int) {
dataStore.edit { preferences ->
preferences[PrefsKeys.DIGITS_PRECISION] = precision
}
}
/**
* Gets int from datastore
* Update separator preference in DataStore
*
* @param[default] Value to return if didn't find anything. Used on fresh install
* @param separator One of the [Separator] to change to
*/
fun getItem(key: Preferences.Key<Int>, default: Int): Flow<Int> {
return context.settingsDataStore.data.map {
it[key] ?: default
suspend fun updateSeparator(separator: Int) {
dataStore.edit { preferences ->
preferences[PrefsKeys.SEPARATOR] = separator
}
}
/**
* Gets boolean from datastore
* Update outputFormat preference in DataStore
*
* @param[default] Value to return if didn't find anything on fresh install
* @param outputFormat One of the [OutputFormat] to change to
*/
fun getItem(key: Preferences.Key<Boolean>, default: Boolean): Flow<Boolean> {
return context.settingsDataStore.data.map {
it[key] ?: default
suspend fun updateOutputFormat(outputFormat: Int) {
dataStore.edit { preferences ->
preferences[PrefsKeys.OUTPUT_FORMAT] = outputFormat
}
}
/**
* Saves string value by key
* Update analytics preference in DataStore
*
* @param enableAnalytics True if user wants to share data, False if not
*/
suspend fun saveItem(key: Preferences.Key<String>, value: String) {
context.settingsDataStore.edit {
it[key] = value
suspend fun updateEnableAnalytics(enableAnalytics: Boolean) {
dataStore.edit { preferences ->
preferences[PrefsKeys.ENABLE_ANALYTICS] = enableAnalytics
}
}
/**
* Saves int value by key
* Update latest used pair of [AbstractUnit] in DataStore. Need it so when user restarts the app,
* this pair will be already set.
*
* @param leftSideUnit [AbstractUnit] on the left
* @param rightSideUnit [AbstractUnit] on the right
*/
suspend fun saveItem(key: Preferences.Key<Int>, value: Int) {
context.settingsDataStore.edit {
it[key] = value
}
}
/**
* Saves boolean value by key
*/
suspend fun saveItem(key: Preferences.Key<Boolean>, value: Boolean) {
context.settingsDataStore.edit {
it[key] = value
suspend fun updateLatestPairOfUnits(leftSideUnit: AbstractUnit, rightSideUnit: AbstractUnit) {
dataStore.edit { preferences ->
preferences[PrefsKeys.LATEST_LEFT_SIDE] = leftSideUnit.unitId
preferences[PrefsKeys.LATEST_RIGHT_SIDE] = rightSideUnit.unitId
}
}
}

View File

@ -33,81 +33,67 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val mySettingsPrefs: UserPreferences,
private val userPrefsRepository: UserPreferencesRepository,
private val basedUnitRepository: MyBasedUnitsRepository,
private val application: Application
) : ViewModel() {
/**
* APP THEME
*/
val currentAppTheme =
mySettingsPrefs.getItem(UserPreferenceKeys.CURRENT_APP_THEME, AppTheme.AUTO)
fun saveCurrentAppTheme(value: Int) {
viewModelScope.launch {
mySettingsPrefs.saveItem(key = UserPreferenceKeys.CURRENT_APP_THEME, value)
}
}
/**
* CONVERSION PRECISION
*/
var precision: Int by mutableStateOf(0)
var currentTheme: Int by mutableStateOf(AppTheme.NOT_SET)
private set
fun setPrecisionPref(value: Int) {
viewModelScope.launch {
precision = value
mySettingsPrefs.saveItem(UserPreferenceKeys.DIGITS_PRECISION, value)
convertValue()
}
}
/**
* SEPARATOR
*/
var separator: Int by mutableStateOf(0)
var precision: Int by mutableStateOf(3)
private set
fun setSeparatorPref(value: Int) {
separator = value
viewModelScope.launch {
Formatter.setSeparator(value)
mySettingsPrefs.saveItem(UserPreferenceKeys.SEPARATOR, value)
convertValue()
}
}
/**
* OUTPUT FORMAT
*/
var outputFormat: Int by mutableStateOf(0)
var separator: Int by mutableStateOf(Separator.SPACES)
private set
var outputFormat: Int by mutableStateOf(OutputFormat.PLAIN)
private set
/**
* Sets given output format and saves it in user preference store
* @param value [OutputFormat] to set
*/
fun setOutputFormatPref(value: Int) {
// Updating value in memory
outputFormat = value
// Updating value on disk
viewModelScope.launch {
mySettingsPrefs.saveItem(UserPreferenceKeys.OUTPUT_FORMAT, value)
convertValue()
}
}
/**
* ANALYTICS
*/
var enableAnalytics: Boolean by mutableStateOf(false)
private set
fun setAnalyticsPref(value: Boolean) {
enableAnalytics = value
/**
* See [UserPreferencesRepository.updateCurrentAppTheme]
*/
fun updateCurrentAppTheme(appTheme: Int) {
viewModelScope.launch {
mySettingsPrefs.saveItem(UserPreferenceKeys.ENABLE_ANALYTICS, value)
userPrefsRepository.updateCurrentAppTheme(appTheme)
}
}
/**
* See [UserPreferencesRepository.updateDigitsPrecision]
*/
fun updatePrecision(precision: Int) {
viewModelScope.launch {
userPrefsRepository.updateDigitsPrecision(precision)
convertValue()
}
}
/**
* See [UserPreferencesRepository.updateSeparator]
*/
fun updateSeparator(separator: Int) {
viewModelScope.launch {
userPrefsRepository.updateSeparator(separator)
convertValue()
}
}
/**
* See [UserPreferencesRepository.updateOutputFormat]
*/
fun updateOutputFormat(outputFormat: Int) {
viewModelScope.launch {
userPrefsRepository.updateOutputFormat(outputFormat)
convertValue()
}
}
/**
* See [UserPreferencesRepository.updateEnableAnalytics]
*/
fun updateEnableAnalytics(enableAnalytics: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateEnableAnalytics(enableAnalytics)
FirebaseAnalytics.getInstance(application).setAnalyticsCollectionEnabled(enableAnalytics)
}
}
@ -412,10 +398,9 @@ class MainViewModel @Inject constructor(
/**
* Saves latest pair of units into datastore
*/
fun saveMe() {
fun saveLatestPairOfUnits() {
viewModelScope.launch {
mySettingsPrefs.saveItem(UserPreferenceKeys.LATEST_LEFT_SIDE, unitFrom.unitId)
mySettingsPrefs.saveItem(UserPreferenceKeys.LATEST_RIGHT_SIDE, unitTo.unitId)
userPrefsRepository.updateLatestPairOfUnits(unitFrom, unitTo)
}
}
@ -477,21 +462,34 @@ class MainViewModel @Inject constructor(
}
}
init {
/**
* 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 even though it needs currentTheme which is also in
* user preferences/
*/
private fun observePreferenceChanges() {
viewModelScope.launch {
val latestLeftSideUnitId = mySettingsPrefs.getItem(
UserPreferenceKeys.LATEST_LEFT_SIDE,
MyUnitIDS.kilometer
).first()
userPrefsRepository.userPreferencesFlow.collect { userPref ->
currentTheme = userPref.currentAppTheme
precision = userPref.digitsPrecision
separator = userPref.separator.also { Formatter.setSeparator(it) }
outputFormat = userPref.outputFormat
enableAnalytics = userPref.enableAnalytics
}
}
}
val latestRightSideUnitId = mySettingsPrefs.getItem(
UserPreferenceKeys.LATEST_RIGHT_SIDE,
MyUnitIDS.mile
).first()
init {
observePreferenceChanges()
viewModelScope.launch {
val snapshot = userPrefsRepository.userPreferencesFlow.first()
// First we load latest pair of units
unitFrom = try {
ALL_UNITS.first { it.unitId == latestLeftSideUnitId }
ALL_UNITS.first { it.unitId == snapshot.latestLeftSideUnit }
} catch (e: java.util.NoSuchElementException) {
Log.w("MainViewModel", "No unit with the given unitId")
ALL_UNITS.first { it.unitId == MyUnitIDS.kilometer }
@ -499,25 +497,12 @@ class MainViewModel @Inject constructor(
unitTo = try {
ALL_UNITS
.first { it.unitId == latestRightSideUnitId }
.first { it.unitId == snapshot.latestRightSideUnit }
} catch (e: java.util.NoSuchElementException) {
Log.w("MainViewModel", "No unit with the given unitId")
ALL_UNITS.first { it.unitId == MyUnitIDS.mile }
}
// Now we get the precision so we can convert values
precision = mySettingsPrefs.getItem(UserPreferenceKeys.DIGITS_PRECISION, 3).first()
// Getting separator and changing it in number formatter
separator =
mySettingsPrefs
.getItem(UserPreferenceKeys.SEPARATOR, Separator.SPACES).first()
.also { Formatter.setSeparator(it) }
// Getting output format
outputFormat =
mySettingsPrefs
.getItem(UserPreferenceKeys.OUTPUT_FORMAT, OutputFormat.PLAIN)
.first()
convertValue()
val allBasedUnits = basedUnitRepository.getAll()
@ -546,9 +531,6 @@ class MainViewModel @Inject constructor(
* He can choose another unit group and doesn't need to wait for network to appear.
* */
updateCurrenciesBasicUnits()
enableAnalytics = mySettingsPrefs.getItem(UserPreferenceKeys.ENABLE_ANALYTICS, true).first()
FirebaseAnalytics.getInstance(application).setAnalyticsCollectionEnabled(enableAnalytics)
}
}
}

View File

@ -40,7 +40,7 @@ fun SettingsScreen(
}
var currentDialogState: Int by rememberSaveable { mutableStateOf(0) }
val currentAppTheme: Int by mainViewModel.currentAppTheme.collectAsState(AppTheme.NOT_SET)
val currentAppTheme: Int = mainViewModel.currentTheme
Scaffold(
modifier = Modifier
@ -134,7 +134,7 @@ fun SettingsScreen(
label = stringResource(id = R.string.send_usage_statistics),
supportText = stringResource(id = R.string.send_usage_statistics_support),
switchState = mainViewModel.enableAnalytics,
onSwitchChange = { mainViewModel.setAnalyticsPref(!it) })
onSwitchChange = { mainViewModel.updateEnableAnalytics(!it) })
SettingsListItem(
label = stringResource(R.string.third_party_licenses),
onClick = { navControllerAction(ABOUT_SCREEN) }
@ -164,7 +164,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.precision_setting),
listItems = PRECISIONS,
selectedItemIndex = mainViewModel.precision,
selectAction = { mainViewModel.setPrecisionPref(it) },
selectAction = { mainViewModel.updatePrecision(it) },
dismissAction = { currentDialogState = 0 },
supportText = stringResource(id = R.string.precision_setting_info)
)
@ -174,7 +174,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.separator_setting),
listItems = SEPARATORS,
selectedItemIndex = mainViewModel.separator,
selectAction = { mainViewModel.setSeparatorPref(it) },
selectAction = { mainViewModel.updateSeparator(it) },
dismissAction = { currentDialogState = 0 }
)
}
@ -183,7 +183,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.output_format_setting),
listItems = OUTPUT_FORMAT,
selectedItemIndex = mainViewModel.outputFormat,
selectAction = { mainViewModel.setOutputFormatPref(it) },
selectAction = { mainViewModel.updateOutputFormat(it) },
dismissAction = { currentDialogState = 0 },
supportText = stringResource(id = R.string.output_format_setting_info)
)
@ -193,7 +193,7 @@ fun SettingsScreen(
title = stringResource(id = R.string.theme_setting),
listItems = APP_THEMES,
selectedItemIndex = currentAppTheme,
selectAction = { mainViewModel.saveCurrentAppTheme(it) },
selectAction = { mainViewModel.updateCurrentAppTheme(it) },
dismissAction = { currentDialogState = 0 },
// Show note for users with devices that support custom Dynamic theming
supportText = if (Build.VERSION.SDK_INT in (Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.R)) stringResource(

View File

@ -10,7 +10,7 @@ import com.sadellie.unitto.data.preferences.AppTheme
@Composable
fun AppTheme(
fun UnittoTheme(
currentAppTheme: Int,
content: @Composable () -> Unit
) {