diff --git a/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt b/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt index 34bf1599..6eac2314 100644 --- a/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt +++ b/app/src/androidTest/java/com/sadellie/unitto/screen/SwapUnitsTest.kt @@ -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() viewModel = MainViewModel( - UserPreferences(context), + UserPreferencesRepository(context), MyBasedUnitsRepository( Room.inMemoryDatabaseBuilder( context, diff --git a/app/src/main/java/com/sadellie/unitto/MainActivity.kt b/app/src/main/java/com/sadellie/unitto/MainActivity.kt index 3fe79ab6..81f2bf4a 100644 --- a/app/src/main/java/com/sadellie/unitto/MainActivity.kt +++ b/app/src/main/java/com/sadellie/unitto/MainActivity.kt @@ -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() } } diff --git a/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt b/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt index f6c78067..ff872235 100644 --- a/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt +++ b/app/src/main/java/com/sadellie/unitto/data/preferences/UserPreferences.kt @@ -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,81 +17,142 @@ import javax.inject.Inject private val Context.settingsDataStore by preferencesDataStore("settings") /** - * Keys for DataStore + * 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 */ -object UserPreferenceKeys { - val CURRENT_APP_THEME = intPreferencesKey("CURRENT_APP_THEME") - val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY") - val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY") - val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY") - 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") -} +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 UserPreferences @Inject constructor(@ApplicationContext private val context: Context) { +class UserPreferencesRepository @Inject constructor(@ApplicationContext private val context: Context) { + + private val dataStore: DataStore = context.settingsDataStore + /** - * Gets string from datastore + * Keys for DataStore + */ + 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") + val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY") + 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") + } + + val userPreferencesFlow: Flow = 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 * - * @param[default] Value to return if didn't find anything on fresh install + * @param appTheme [AppTheme] to change to */ - fun getItem(key: Preferences.Key, default: String): Flow { - return context.settingsDataStore.data.map { - it[key] ?: default + suspend fun updateCurrentAppTheme(appTheme: Int) { + dataStore.edit { preferences -> + preferences[PrefsKeys.CURRENT_APP_THEME] = appTheme } } /** - * Gets int from datastore + * Update precision preference in DataStore * - * @param[default] Value to return if didn't find anything. Used on fresh install + * @param precision One of the [PRECISIONS] to change to */ - fun getItem(key: Preferences.Key, default: Int): Flow { - return context.settingsDataStore.data.map { - it[key] ?: default + suspend fun updateDigitsPrecision(precision: Int) { + dataStore.edit { preferences -> + preferences[PrefsKeys.DIGITS_PRECISION] = precision } } /** - * Gets boolean from datastore + * Update separator preference in DataStore * - * @param[default] Value to return if didn't find anything on fresh install + * @param separator One of the [Separator] to change to */ - fun getItem(key: Preferences.Key, default: Boolean): Flow { - return context.settingsDataStore.data.map { - it[key] ?: default + suspend fun updateSeparator(separator: Int) { + dataStore.edit { preferences -> + preferences[PrefsKeys.SEPARATOR] = separator } } /** - * Saves string value by key + * Update outputFormat preference in DataStore + * + * @param outputFormat One of the [OutputFormat] to change to */ - suspend fun saveItem(key: Preferences.Key, value: String) { - context.settingsDataStore.edit { - it[key] = value + suspend fun updateOutputFormat(outputFormat: Int) { + dataStore.edit { preferences -> + preferences[PrefsKeys.OUTPUT_FORMAT] = outputFormat } } /** - * Saves int 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, value: Int) { - context.settingsDataStore.edit { - it[key] = value + suspend fun updateEnableAnalytics(enableAnalytics: Boolean) { + dataStore.edit { preferences -> + preferences[PrefsKeys.ENABLE_ANALYTICS] = enableAnalytics } } /** - * Saves boolean 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, 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 } } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt b/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt index 07f0c935..1529be09 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/MainViewModel.kt @@ -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) } } } diff --git a/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt b/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt index 9dd14622..2b0a97b5 100644 --- a/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt +++ b/app/src/main/java/com/sadellie/unitto/screens/setttings/SettingsScreen.kt @@ -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( diff --git a/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt b/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt index abca4088..c1eefd7b 100644 --- a/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt +++ b/app/src/main/java/com/sadellie/unitto/ui/theme/Theme.kt @@ -10,7 +10,7 @@ import com.sadellie.unitto.data.preferences.AppTheme @Composable -fun AppTheme( +fun UnittoTheme( currentAppTheme: Int, content: @Composable () -> Unit ) {