I'm learning Compose by the article.
A stateless composable is a composable that doesn't hold any state. An easy way to achieve stateless is by using state hoisting, so I replace Code B with Code A, it's great!
The article tell me:
By hoisting the state out of HelloContent, it's easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen, you don't have to change how HelloContent is implemented.
So I write Code C, it stores the value of name in a SharedPreferences, I think that Code C is just like Code A, but in fact, I can't input any letter with Code C, what wrong with the Code C ?
Code A
#Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
Code B
#Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Code C
#Composable
fun HelloScreen() {
var name: String by PreferenceTool( LocalContext.current ,"zipCode", "World")
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: default
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
Om is completely right about the reasons why your code doesn't work, and his answer will work.
To understand why you need a MutableState in compose I suggest you start with documentation, including this youtube video which explains the basic principles.
But PreferenceManager is deprecated and now you can use DataStore instead.
With compose in can be used like this:
#Composable
fun <T> rememberPreference(
key: Preferences.Key<T>,
defaultValue: T,
): MutableState<T> {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val state = remember {
context.dataStore.data
.map {
it[key] ?: defaultValue
}
}.collectAsState(initial = defaultValue)
return remember {
object : MutableState<T> {
override var value: T
get() = state.value
set(value) {
coroutineScope.launch {
context.dataStore.edit {
it[key] = value
}
}
}
override fun component1() = value
override fun component2(): (T) -> Unit = { value = it }
}
}
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
Usage:
var name by rememberPreference(stringPreferencesKey("zipCode"), "World")
One key piece is missing in Code C, which is MutableState.
In Compose the only way to modify content UI is by mutation of its corresponding state.
Your code doesn't have a mutable state object backing up PreferenceTool. So use of setValue by property delegation only modifies the SharedPreference (by calling putPreference(name, value)) but change is not propagated to UI.
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
In order to correct the behavior, add a MutableState object within PreferenceTool.
This way the updates are detected by Compose and UI is updated accordingly.
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private val state = mutableStateOf(findPreference(name, default))
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
state.value = value
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = { ... }
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = { ... }
}
Related
I am trying to create a livedata with firebase but for some reason I recieve an error while using this statement "_loginFlow.value = result" - it says: "Required: FirebaseUser>?, Found: FirebaseUser>
What would cause this ? Any help is appreciated!
My code:
private val _loginFlow = MutableStateFlow<Resource<FirebaseUser>?>(null)
val loginFlow: StateFlow<Resource<FirebaseUser>?> = _loginFlow
fun loginUser(email: String, password: String) = viewModelScope.launch {
_loginFlow.value = Resource.Loading
val result = repository.login(email, password)
_loginFlow.value = result
}
#Composable
fun LoginButton(onClick: () -> Unit) {
// LOGIN BUTTON
Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
Button(
onClick = {
viewModel?.loginUser(email, password)
},
shape = RoundedCornerShape(50.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = "Login")
}
}
AuthRepository:
interface AuthRepository {
val currentUser: FirebaseUser?
suspend fun login(email: String, password: String): Resource<FirebaseUser>
suspend fun signup(name: String, email: String, password: String): Resource<FirebaseUser>
fun logout()
}
Resource:
sealed class Resource<out R> {
data class Success<out R>(val result: R) : Resource<R>()
data class Failure(val exception: Exception) : Resource<Nothing>()
object Loading : Resource<Nothing>()
}
First of all you don't need to add '?' in MutableStateFlow<Resource<FirebaseUser>?>, as Resource should never be null, it's always one of the values you defined inside it. So just remove it and then check.
I'm currently learning about the new Android stack (MVVM, compose, kotlin Flow/StateFlow), and I'm having trouble debugging a StateFlow where the value is updated, but I have no sign of collection from the composable.
It's a generic question, but I didn't find any solution to my problem by searching on my own.
Does anybody have an idea about what could disturb a StateFlow? I'm letting my code below:
ViewModel:
#HiltViewModel
class AuthViewModel #Inject constructor(
private val navigationManager: NavigationManager,
private val interactor: AuthInteractor
): BaseViewModel() {
companion object {
val TAG: String = AuthViewModel::class.java.simpleName
}
private val _uiState = MutableStateFlow(AuthenticationState())
val uiState: StateFlow<AuthenticationState> = _uiState
fun handleEvent(event: AuthenticationEvent) {
Log.v(TAG, "new event: $event")
when (event) {
is AuthenticationEvent.GoToRegistering -> navigateToRegistering()
is AuthenticationEvent.Register -> registerAccount(event)
is AuthenticationEvent.SnackbarMessage -> showSnackBar(event.message, event.type)
}
}
private fun navigateToRegistering() {
navigationManager.navigate(NavigationDirections.Authentication.registering)
}
private fun registerAccount(event: AuthenticationEvent.Register) {
Log.v(TAG, "register account")
_uiState.value.build {
isLoading = true
}
viewModelScope.launch {
Log.v(TAG, "launching request")
val exceptionMessage = interactor.registerUser(event.login, event.password)
Log.v(TAG, "response received, launching state from viewmodel")
_uiState.value.build {
exceptionMessage?.let {
Log.v(TAG, "Exception is not null")
isFailure = true
snackbarMessage = interactor.selectRegisterError(it)
}
}
}
}
}
UI Component:
#Composable
fun Authentication(
modifier: Modifier = Modifier,
viewModel: AuthViewModel,
type: AuthenticationType
) {
val state by viewModel.uiState.collectAsState()
Log.v("Authentication", "new state: $state, type = $type")
BackHandler(enabled = true) {
viewModel.handleEvent(AuthenticationEvent.Back)
}
state.snackbarMessage?.let { resourceId ->
val message = stringResource(resourceId)
Log.e("Authentication", "error: $message")
viewModel.handleEvent(AuthenticationEvent.SnackbarMessage(message, SnackbarType.ERROR))
}
if (state.isAuthenticated) {
// TODO: launch main screen
}
LaunchedEffect(key1 = state.isRegistered) {
// TODO: launch login event
}
AuthenticationContent(
modifier = modifier,
type = type,
viewModel = viewModel,
state = state
)
}
State Data Class:
data class AuthenticationState(
val isLoading: Boolean = false,
val isFailure: Boolean = false,
val isRegistered: Boolean = false,
val isAuthenticated: Boolean = false,
#StringRes val snackbarMessage: Int? = null
) {
fun build(block: Builder.() -> Unit) = Builder(this).apply(block).build()
class Builder(uiModel: AuthenticationState) {
var isLoading = uiModel.isLoading
var isFailure = uiModel.isFailure
var isRegistered = uiModel.isRegistered
var isAuthenticated = uiModel.isAuthenticated
var snackbarMessage = uiModel.snackbarMessage
fun build(): AuthenticationState {
return AuthenticationState(
isLoading,
isFailure,
isRegistered,
isAuthenticated,
snackbarMessage
)
}
}
}
You are doing the updates like this:
_uiState.value.build {
isLoading = true
}
This code takes current value of uiState and calls build function on it. Build function creates new instance of AuthenticationState, but you don't do anything with that new instance and it's discarded. You have to set this new instance to be the new value of your StateFlow, like this:
_uiState.value = _uiState.value.build {}
// or this:
_uiState.update { current -> current.build {} }
I am working on app settings. I am doing this in Jetpack Compose. However, when the switch button is clicked and changed to true or false, this is printed out during debug, however, the settings don't appear to change nor be saved. No way to confirm this.
Not sure of LaunchEffect{} shall be used.
AppSettings:
import kotlinx.serialization.Serializable
#Serializable
data class AppSettings(
val enableLocation: Boolean = false
)
AppSettingsSerializer:
object AppSettingsSerializer : Serializer<AppSettings> {
override val defaultValue: AppSettings
get() = AppSettings()
override suspend fun readFrom(input: InputStream): AppSettings {
return try {
Json.decodeFromString(
deserializer = AppSettings.serializer(),
string = input.readBytes().decodeToString())
} catch (e: SerializationException){
e.printStackTrace()
defaultValue
}
}
override suspend fun writeTo(t: AppSettings, output: OutputStream) {
output.write(
Json.encodeToString(
serializer = AppSettings.serializer(),
value = t)
.encodeToByteArray()
)
}
}
SettingsViewModel:
#HiltViewModel
class SettingsViewModel #Inject constructor(
val preferencesRepository: PreferencesRepository
) : ViewModel() {
var preferences by mutableStateOf(AppSettings())
private set
init {
preferencesRepository.data
.onEach { preferences = it }
.launchIn(viewModelScope)
}
inline fun updatePreferences(crossinline body: (AppSettings) -> AppSettings) {
viewModelScope.launch {
val data = body(preferences)
preferencesRepository.updateSettings(data)
}
}
}
PreferenceRepository:
class PreferencesRepository(context: Context){
private val Context.dataStore by dataStore(
fileName = "app-settings.json",
serializer = AppSettingsSerializer
)
private val appDataStore = context.dataStore
val data = appDataStore.data
suspend fun updateSettings(settings: AppSettings) {
appDataStore.updateData { settings }
}
}
Inside settings screen:
item {
SwitchPreference(
title = stringResource(R.string.location),
subtitle = AnnotatedString(stringResource(R.string.location_desc)),
checked = settings.enableLocation,
onCheckedChange = {location ->
viewModel.updatePreferences { it.copy(enableLocation = location) }
}
)
}
I am struggling to understand what is the best way to get this to work.
I have some input fields and I created a TextFieldState to keep all the state in one place.
But it is not triggering a re-composition of the composable so the state never updates.
I saw this stack overflow answer on a similar question, but I just find it confusing and it doesn't make sense to me
Here is the code:
The Composable:
#Composable
fun AddTrip (
addTripVm: AddTripVm = hiltViewModel()
) {
var name = addTripVm.getNameState()
var stateTest = addTripVm.getStateTest()
Column(
//verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Text(text = "Add Trip")
Column(
){
println("From Composable: ${name.value.value}") //No Recomposition
meTextField(
value = name.value.value,
onChange = {
addTripVm.updateName(it)
},
placeholder = "Name",
)
}
View Model code:
#HiltViewModel
class AddTripVm #Inject constructor(
private val tripRepository: TripRepositoryContract,
private val tripValidator: TripValidatorContract
): TripValidatorContract by tripValidator, ViewModel() {
/**
* Name of the trip, this is required
*/
private val nameState: MutableState<TextFieldState> = mutableStateOf(TextFieldState())
private var stateTest = mutableStateOf("");
fun updateStateTest(newValue: String) {
stateTest.value = newValue
}
fun getStateTest(): MutableState<String> {
return stateTest
}
fun getNameState(): MutableState<TextFieldState> {
return nameState;
}
fun updateName(name: String) {
println("From ViewModel? $name")
nameState.value.value = name
println("From ViewModel after update: ${nameState.value.value}") //Updates perfectly
}
}
Text field state:
data class TextFieldState(
var value: String = "",
var isValid: Boolean? = null,
var errorMessage: String? = null
)
Is this possible? Or do I need to separate the value as a string and keep the state separate for if its valid or not?
You don't change instance of nameState's value with
nameState.value.value = name
It's the same object which State checks by default with
fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> =
StructuralEqualityPolicy as SnapshotMutationPolicy<T>
private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> {
override fun equivalent(a: Any?, b: Any?) = a == b
override fun toString() = "StructuralEqualityPolicy"
}
MutableState use this as
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
Easiest way is to set
nameState.value = nameState.value.copy(value= name)
other option is to write your own SnapshotMutationPolicy
The following code is from the official Advanced State in Jetpack Compose Codelab.
In the function fun DetailsScreen(), uiState.isLoading -> {...} will be fired when isLoading is true.
I searched all the code in the project, I can only find the code val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) to pass value to isLoading.
Will uiState.isLoading -> {...} always be fired in the project?
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
No
The default value of isLoading is false:
val isLoading: Boolean = false,
So the constructor calls that don't explicitly set it to true (DetailsUiState(throwError = true) and DetailsUiState(throwError = true)) will result in it being false.