How to programically trigger notify on MutableLiveData change - android

I have a LiveData property for login form state like this
private val _authFormState = MutableLiveData<AuthFormState>(AuthFormState())
val authFormState: LiveData<AuthFormState>
get() =_authFormState
The AuthFormState data class has child data objects for each field
data class AuthFormState (
var email: FieldState = FieldState(),
var password: FieldState = FieldState()
)
and the FieldState class looks like so
data class FieldState(
var error: Int? = null,
var isValid: Boolean = false
)
When user types in some value into a field the respective FieldState object gets updated and assigned to the parent AuthFormState object
fun validateEmail(text: String) {
_authFormState.value!!.email = //validation result
}
The problem is that the authFormState observer is not notified in this case.
Is it possible to trigger the notification programically?

Maybe you can do:
fun validateEmail(text: String) {
val newO = _authFormState.value!!
newO.email = //validation result
_authFormState.setValue(newO)
}

You have to set the value to itself, like this: _authFormState.value = _authFormState.value to trigger the refresh. You could write an extension method to make this cleaner:
fun <T> MutableLiveData<T>.notifyValueModified() {
value = value
}
For such a simple data class, I would recommend immutability to avoid issues like this altogether (replaces all those vars with vals). Replace validateEmail() with something like this:
fun validateEmail(email: String) = //some modified version of email
When validating fields, you can construct a new data object and set it to the live data.
fun validateFields() = _authFormState.value?.let {
_authFormState.value = AuthFormState(
validateEmail(it.email),
validatePassword(it.password)
)
}

Related

How to avoid default value in MutableStateFlow kotlin

I am using MutableStateFlow in my project. When we initialise the MutableStateFlow object we need to give default value.
val topics = MutableStateFlow<List<String>>(emptyList())
when I collect this value
[null, "Hello", "world"]
I want to pass this list in adapter . So is there a way we can remove the null object before passing in adapter or Is there any better way ?
viewModel.topics.collect { topicsList ->
println(topicsList) // [null, "Hello", "world"]
adapter.submitList(topicsList)
}
If you don't want it to have an enforced initial value, use MutableSharedFlow instead. If you give it replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, and distinctUntilChanged(), it's basically the same thing as a MutableStateFlow without the enforced value. And if onBufferOverflow is not BufferOverflow.SUSPEND, tryEmit will always succeed so you can use tryEmit() instead of value = .
private val _topics = MutableSharedFlow<List<String>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val topics: Flow<List<String>> = _topics.distinctUntilChanged()
// emitting to the shared flow:
_topics.tryEmit(newValue)
If you want to ignore initial value of StateFlow, set initial value null or anything you want. Then you can use filter function on flow.
For example initial value is null
launch {
val topicsState = MutableStateFlow<List<String?>?>(null)
topicsState.filterNotNull().map { topics -> topics.filterNotNull() }.onEach { topics ->
println(topics)
}.launchIn(this)
launch {
delay(1000)
topicsState.update { listOf(null, "Hello", "world") }
}
}
Output
[Hello, world]
Since it emits a list of strings you could try to initialise the StateFlow with a null like so
val topics = MutableStateFlow<List<String>?>(null)
And when you collect you can check if the emitted value is null or not
viewModel.topics.collect { topicsList ->
topicsList?.let { safeTopics ->
adapter.submitList(safeTopics)
}
}
If we have given a common generic type sealed class.
Common Sealed Class
sealed class Resource<T>(val data: T? = null, val error: String? = null) {
class Loading<T> : Resource<T>()
class Success<T>(data: T) : Resource<T>(data = data)
class Error<T>(error: String) : Resource<T>(error = error)
}
In that case, we can set the initial value like this.
private val _mutableStateFlow: MutableStateFlow<Resource<List<PackageModel>>?> = MutableStateFlow(null)
PackageModel is Model/Pojo class
I think what you need is this:
val sampleList = listOf(null, "Hello", "world")
val topics = MutableStateFlow<List<String>>(sampleList.filer { it != null })

Boolean flows sync

I have few StateFlow fields in the ViewModel class. It's add/edit form screen where each StateFlow is validation property for each editable field on the screen.
I would like to write some class FormValidation with StateFlow property for validation state of whole form. Value of this field based on the values of validation state of all fields and emit true when all field is valid and false when any field is invalid.
Something like this:
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
private val _isValid = MutableStateFlow(initValue)
val isValid: StateFlow<Boolean> = _isValid
init {
// todo: how to combine, subscribe and sync values of all fieldIsValid flows?
}
}
I know how to do it with LiveData<Boolean> and MediatorLiveData but i can't understand how to make it with flows.
Solution based on the answer of #tenfour04
class BooleanFlowMediator(scope: CoroutineScope, initValue: Boolean, vararg flows: Flow<Boolean>) {
val sync: StateFlow<Boolean> = combine(*flows) { values ->
values.all { it }
}.stateIn(scope, SharingStarted.Eagerly, initValue)
}
Demo code with StateFlow and ViewModel
class SyncViewModel : ViewModel() {
companion object {
private const val DEFAULT_VALUE: Boolean = false
}
private val values: List<List<Boolean>> = listOf(
listOf(false, false, false),
listOf(true, false, false),
listOf(false, true, true),
listOf(true, true, true)
)
private var index: Int = 0
private val _flow1 = MutableStateFlow(DEFAULT_VALUE)
val flow1: StateFlow<Boolean> = _flow1
private val _flow2 = MutableStateFlow(DEFAULT_VALUE)
val flow2: StateFlow<Boolean> = _flow2
private val _flow3 = MutableStateFlow(DEFAULT_VALUE)
val flow3: StateFlow<Boolean> = _flow3
val mediator = BooleanFlowMediator(viewModelScope, DEFAULT_VALUE,
flow1, flow2, flow3)
fun generateValues() {
val idx = (index + 1).mod(values.size).also { index = it }
val row = values[idx]
_flow1.value = row[0]
_flow2.value = row[1]
_flow3.value = row[2]
}
}
I think you can do this using combine. It returns a new Flow that emits each time any of the source Flows emits, using the latest values of each in a lambda to determine its emitted value.
There are also overloads of combine for up to five input Flows of different types, and one for an arbitrary number of Flows of the same type, which is what we want here.
Since Flow operators return basic cold Flows, but if you want to have a StateFlow so you can determine the initial value, you need to use stateIn to convert it back to a StateFlow with an initial value. And for that you'll need a CoroutineScope for it to run the flow in. I'll leave it to you to determine the best scope to use. Maybe it should be passed in from an owning class (like passing viewModelScope to it if the class instance is "owned" by the ViewModel). If you're not using a passed in scope, you will have to manually cancel the scope when this class instance is done with, or else the flow will leak.
I didn't test this code, but I think this should do it.
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
private val scope = MainScope()
val isValid: StateFlow<Boolean> =
combine(*fieldIsValid) { values -> values.all { it } }
.stateIn(scope, SharingStarted.Eagerly, initValue)
}
However, if you don't need to synchronously inspect the most recent value of the Flow (StateFlow.value), then you don't need a StateFlow at all, and you can just expose a cold Flow. The instant the cold Flow is collected, it will start collecting its source StateFlows, so it will immediately emit its first value based on the current values of all the sources.
class FormValidation(initValue: Boolean, vararg fieldIsValid: StateFlow<Boolean>) {
val isValid: Flow<Boolean> = when {
fieldIsValid.isEmpty() -> flowOf(initValue) // ensure at least one value emitted
else -> combine(*fieldIsValid) { values -> values.all { it } }
.distinctUntilChanged()
}
}

How to get json data from url to Text Composable?

How can I fetch json from url and add it's data to a Text Composable in Jetpack Compose
Here is json file
https://jsonplaceholder.typicode.com/posts
#Composable
fun Api(){
val queue = Volley.newRequestQueue(LocalContext.current)
val url = "https://jsonplaceholder.typicode.com/posts"
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url,null,
{ response ->
val title = response.getString("title")
print(title)
},
{ error ->
print(error.localizedMessage)
})
queue.add(jsonObjectRequest)
}
Just get the data however you want to inside a viewmodel. Then, store it in a variable, like var data by mutableStateOf("")
Then access this variable through the viewmodel from your text Composable. Updating this variable like a normal string will trigger recompositions
EDIT BASED ON THE COMMENT BELOW:-
Although it is not necessary to store it in a viewmodel, it is the recommended best practice. You can also store the state inside your normal activity class or even the Composable using remember(not recommended for important state storage)
However, by viewmodel, I just meant,
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
fun onLoadData(){
data = //json extraction logic
}
fun onDataChange(newData: String){
data = newData
}
}
Then, in your activity,
class mActiviry: AppCompatActivity(){
val vm by viewmodels<mViewModel>() //See docs for better approaches of initialisation
//...
setContent {
Text(vm.data)
}
}
Done
Edit:-
Alternately, ditch the onDataLoad()
class mViewModel: ViewModel(){
var data by mutableStateOf("")
private set //only viewmodel can modify values
init{
data = // code from the "Api" method in your question
}
fun onDataChange(newData: String){
data = newData
}
}

Jetpack Compose: Modify Room data class using TextField

Modifying simple values and data classes using EditText is fairly straight forward, and generally looks like this:
data class Person(var firstName: String, var lastName: Int)
// ...
val (person, setPerson) = remember { mutableStateOf(Person()) }
// common `onChange` function handles both class properties, ensuring maximum code re-use
fun <T> onChange(field: KMutableProperty1<Person, T>, value: T) {
val nextPerson = person.copy()
field.set(nextPerson, value)
setPerson(nextPerson)
}
// text field for first name
TextField(
value = person.firstName,
onChange = { it -> onChange(Person::firstName, it) })
// text field for last name name
TextField(
value = person.lastName,
onChange = { it -> onChange(Person::lastName, it) })
As you can see, the code in this example is highly reusable: thanks to Kotlin's reflection features, we can use a single onChange function to modify every property in this class.
However, a problem arises when the Person class is not instantiated from scratch, but rather pulled from disk via Room. For example, a PersonDao might contain a `findOne() function like so:
#Query("SELECT * FROM peopleTable WHERE id=:personId LIMIT 1")
fun findOne(personId: String): LiveData<Person>
However, you cannot really use this LiveData in a remember {} for many reasons:
While LiveData has a function called observeAsState(), it returns State<T> and not MutableState<T>, meaning that you cannot modify it with the TextFields. As such this does not work:
remember { personFromDb.observeAsState()}
You cannot .copy() the Person that you get from your database because your component will render before the Room query is returned, meaning that you cannot do this, because the Person class instance will be remembered as null:
remember { mutableStateOf(findPersonQueryResult.value) }
Given that, what is the proper way to handle this? Should the component that contains the TextFields be wrapped in another component that handles the Room query, and only displays the form when the query is returned? What would that look like with this case of LiveData<Person>?
I would do it with a copy and an immutable data class
typealias PersonID = Long?
#Entity
data class Person(val firstName: String, val lastName: String) {
#PrimaryKey(autoGenerate = true)
val personID: PersonID = null
}
//VM or sth
object VM {
val liveData: LiveData<Person> = MutableLiveData() // your db call
val personDao: PersonDao? = null // Pretending it exists
}
#Dao
abstract class PersonDao {
abstract fun upsert(person: Person)
}
#Composable
fun test() {
val personState = VM.liveData.observeAsState(Person("", ""))
TextField(
value = personState.value.firstName,
onValueChange = { fName -> VM.personDao?.upsert(personState.value.copy(firstName = fName))}
)
}

How to get value of the ObservableField in android

Hi I have this ObservableField in my java code. I want to get the value of it which can be done by calling get method on it.
val email = ObservableField<String>()
This can be done using below approach. I am confused and don't know should I make a getter here to get the value of it ? or there is different standard approach to get the value of ObservableField I am using RxJava too in my app.
fun login(view: View) {
val emailVal = email.get()
}
This is exactly what delegation is about. Delegation of a property in Kotlin means having a class that implements the operator function getValue and optionally setValue, which will be called when accessing or updating the property.
Your delegate could look like this:
class <T> ObservableDelegate
{
val field = ObservableField<T>()
operator fun getValue(self: Any?, prop: KProperty<*>) : T
= field.get()
operator fun setValue(self: Any?, prop: KProperty<*>, value: T)
= field.set(value)
}
You can then use the delegate like this:
val email : String by ObservableDelegate()
fun login(view: View) {
val emailVal = email
}
Read more about delegation of properties here: https://kotlinlang.org/docs/reference/delegated-properties.html
I think it is good enough to use email.get(). If you really want to eliminate the use of .get() in your code, you may use backing field:
val _email = ObservableField<String>()
var email: String
get() = _email.get()
set(value) = _email.set(value)
//use
fun login(view: View) {
val emailVal = email
}

Categories

Resources