How to mutate an incoming string in an observableField - android

I have the following field binded to an editText.
val lastName = ObservableField(MutableLiveData<String>())
I want to mutate the entered string so that the first letter will be automatically set in uppercase.
So if you type
williams -> Williams
I thought I could solve this by doing this as follows
lastName.getObservable()
.subscribe { input ->
val lastname = input.decapitalize()
lastName.getField().postValue(lastname.capitalize())
}
I noticed that doing it this way, will throw me in an eternal loop because of the postvalue triggering the subscribe each time. How can I mutate the incoming string through RxJava without having to do it in the way I have it now?

You can do this at the source by overriding set. I don't see the reason for multi-layered observability, so I flattened it here.
val lastName = object: ObservableField<String>() {
override fun set(value: String) {
super.set(value.capitalize())
}
}
If there is some reason you need the layering, you could instead override the setValue method of the MutableLiveData.
val lastName = ObservableField(object: MutableLiveData<String>() {
override fun setValue(value: String) {
super.setValue(value.capitalize())
}
})
But this multi-layering looks convoluted to me. I don't see how you can reliably subscribe to the underlying data if the LiveData instance can be overwritten.

Related

update a MutableStateFlow when another MutableStateFlow emits

val persons = MutableStateFlow<List<Person>>(emptyList())
val names = MutableStateFlow<List<String>>(emptyList())
I want to update names whenever persons emits a new value.
This could be done by observing persons like:
viewModelScope.launch{
persons.collectLatest{personList->
names.emit(personList.map{it.name})
}
}
but I was wondering if there is another way to achieve that, e.g. using flow operators ?
Looks a little nicer
persons.map{ persons ->
names.emit(persons.map{ it.name })
}.launchIn(viewModelScope)
If there is no need for reactive actions, then a function can be used.
val names = {
persons.map{
it.name
}
}
// call
println(names())
If it's a class property, even better
val names: String
get() = persons.map{it.name}

Emit Article with title Kotlin Flow

My task is to get whole Article with provided title from RecyclerView.
When I click on specific Article i get title from it.
Room database:
#Query("SELECT * FROM article_table WHERE title = :title")
fun getArticleDetails(title: String): Flow<ArticleLocal>
Repository:
fun getArticleDetails(title: String): Flow<ArticleLocal> {
return articleDao.getArticleDetails(title)
}
ViewModel:
val articleDetail = MutableStateFlow<ArticleLocal>(ArticleLocal("","","","",""))
fun getArticle(title: String) {
viewModelScope.launch {
articleRepository.getArticleDetails(title).collect {
articleDetail.emit(it)
}
}
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title)
viewModel.articleDetail.collect {
Log.d(TAG, "onCreate: $it")
}
}
Problem with this code is that articleDetail on first touch gives me empty ArticleLocal e.g. title = "" I defined in ViewModel, later I get good result.
EDIT: With MyActivity .collet I get whole object but cannot access propert like it.title
Use a SharedFlow so it doesn't have to publish a default result. The flow won't emit anything until it receives its first value. Use replay = 1 to get similar behavior as StateFlow as far as new subscribers getting the most recent value immediately.
You also need to consider that if the title changes, it should not keep publishing values with the old title. Currently, you have it collecting from more and more flows each time the title changes.
If you use another MutableSharedFlow just for the title, you can get it to automatically cancel unnecessary collection of those old title flows. It also allows you to get the benefit of SharingStarted.WhileSubscribed to avoid unnecessary collection from the repository when there are no subscribers.
In ViewModel:
private val articleTitle = MutableSharedFlow<String>(bufferOverflow = BufferOverflow.DROP_OLDEST)
val articleDetail = articleTitle.flatMapLatest { articleRepository.getArticleDetails(it) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun getArticle(title: String) {
articleTitle.tryEmit(title)
}
You can get rid of additional flow to emit data and use the flow returned from the repository directly.
ViewModel:
fun getArticle(title: String): Flow<ArticleLocal> {
return articleRepository.getArticleDetails(title)
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title).collect {
Log.d(TAG, "onCreate: $it")
}
}

How can I perform Transformations on MutableLiveData?

The docs show how you can perform Transformations on a LiveData object? How can I perform a transformation like map() and switchMap() on a MutableLiveData object instead?
MutableLiveData is just a subclass of LiveData. Any API that accepts a LiveData will also accept a MutableLiveData, and it will still behave the way you expect.
Exactly the same way:
fun viewModelFun() = Transformations.map(mutableLiveData) {
//do somethinf with it
}
Perhaps your problem is you dont know how does yor mutable live data fit on this.
In the recent update mutable live data can start with a default value
private val form = MutableLiveData(Form.emptyForm())
That should trigger the transformation as soon as an observer is attached, because it will have a value to dispatch.
Of maybe you need to trigger it once the observer is attached
fun viewModelFun(selection: String) = liveData {
mutableLiveData.value = selection.toUpperCase
val source = Transformations.map(mutableLiveData) {
//do somethinf with it
}
emitSource(source)
}
And if you want the switch map is usually like this:
private val name = MutableLiveData<String>()
fun observeNames() = Transformations.switchMap(name) {
dbLiveData.search(name) //a list with the names
}
fun queryName(likeName: String) {
name.value = likeName
}
And in the view you would set a listener to the edit text of the search
searchEt.doAfterTextChange {...
viewModel.queryName(text)
}

Why is Livedata setValue ignored when called twice?

I have the following ViewModel with MutableLiveData data and another LiveData ones that is derived from data in a way that it updates its value only if the data.number is equal to 1.
class DummyViewModel : ViewModel() {
private val data = MutableLiveData<Dummy>()
val ones = data.mapNotNull { it.takeIf { it.number == 1 } }
init {
data.value = Dummy(1, "Init")
doSomething()
}
fun doSomething() {
data.value = Dummy(2, "Do something")
}
}
data class Dummy(val number: Int, val text: String)
fun <T, Y> LiveData<T>.mapNotNull(mapper: (T) -> Y?): LiveData<Y> {
val mediator = MediatorLiveData<Y>()
mediator.addSource(this) { item ->
val mapped = mapper(item)
if (mapped != null) {
mediator.value = mapped
}
}
return mediator
}
I observe ones in my fragment. However, If I execute doSomething, I don't receive any updates in my fragment. If I don't execute doSomething, the dummy Init is correctly present in ones and I receive an update.
What is happening here? Why is ones empty and how can I overcome this issue?
Maybe I'm missing something, but the behavior seems like expected to me...
Lets' try to reproduce both cases sequentially.
Without doSomething() :
Create Livedata
Add Dummy(1, "Init")
Start listening in the fragment: Because number is 1, it passes your filter and the fragment receives it
With doSomething():
Create Livedata
Add Dummy(1, "Init")
Add Dummy(2, "Do something") (LiveData keeps only the last value, so if nobody observes, the first value is getting lost)
Start listening in the fragment: Because number is 2, the value gets filtered and the fragment receives nothing
A little offtopic: it's always good to write tests for ViewModel cases like this, because you'll be able to isolate the problem and find the real reason quickly.
EDIT: also be aware that your filter is only working on observing, it isn't applied when putting the value into LiveData.

Transformations.map LiveData

I have following code :
val liveData = MutableLiveData<String>()
liveData.value = "Ali"
val res = map(liveData) { post(it) }
textview.text = res.value.toString()
fun post(name: String): String {
return "$name $name"
}
I expect it to print Ali Ali but it prints a null value. What am I missing?
You are missing a null check.
res.value.toString()
Imagine the case when res.value is null you are doing this.
null.toString() which the result is the string "null"
And the other hand, when you use LiveData the right approach is to observe all changes like zsmb13 suggested.
res.observe(this, Observer { name ->
textview.text = name
})
LiveData works asynchronously. The value you've set for it isn't immediately available in the transformed LiveData. Instead of trying to read that value directly, you should observe its changes, then you'll get the latest values:
res.observe(this, Observer { name ->
textview.text = name
})
(This code sample assumes that this is a LifecyleOwner, such as an AppCompatActivity.)

Categories

Resources