class RequestViewModel(private val repository: RequestRepository) : ViewModel() {
// request currency type
private val currencySearchType = MutableLiveData<String>()
val requests: LiveData<PagedList<Request>> = repository.getRequests(currencySearchType.value!!)
fun updateSearchType(type: String) {
currencySearchType.postValue(type)
}}
above is the code in my viewModel.
private fun initAdapter() {
recyclerView.adapter = adapter
viewModel.requests.observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
})
}
and this is the code in fragment. So basically, what i'm doing is I observe "requests" in viewModel, and in fragment, I will also call updateSearchType to update the currencySearchType. I was hoping that once currencySearchType changed, then the viewmodel.requests will change too. But it turns out the requests never gotta called again. Does anyone know where it went wrong? Appreciate for the help!
Changing the value of currencySearchType does not triggers live data you just using it as function parameter. You have to use Transformation for this.
https://developer.android.com/reference/android/arch/lifecycle/Transformations
class RequestViewModel(private val repository: RequestRepository) : ViewModel() {
// request currency type
lateinit var currencySearchType:String
val requests: LiveData<PagedList<Request>> = Transformations.switchMap(currencySearchType) { repository.getRequests(it) }
fun updateSearchType(type: String) {
currencySearchType = type
}}
Related
I get page data from a database, I have a repository that returns a flow.
class RepositoryImpl (private val db: AppDatabase) : Repository {
override fun fetchData (page: Int) = flow {
emit(db.getData(page))
}
}
In the ViewModel, I call the stateIn(), the first page arrives, but then how to request the second page? By calling fetchData(page = 2) I get a new flow, and I need the data to arrive on the old flow.
class ViewModel(private val repository: Repository) : ViewModel() {
val dataFlow = repository.fetchData(page = 1).stateIn(viewModelScope, WhileSubscribed())
}
How to get the second page in dataFlow?
I don't see the reason to use a flow in the repository if you are emitting only one value. I would change it to a suspend function, and in the ViewModel I would update a variable of type MutableStateFlow with the new value. The sample code could look like the following:
class RepositoryImpl (private val db: AppDatabase) : Repository {
override suspend fun fetchData (page: Int): List<Data> {
return db.getData(page)
}
}
class ViewModel(private val repository: Repository) : ViewModel() {
val _dataFlow = MutableStateFlow<List<Data>>(emptyList())
val dataFlow = _dataFlow.asStateFlow()
fun fetchData (page: Int): List<Data> {
viewModelScope.launch {
_dataFlow.value = repository.fetchData(page)
}
}
}
Like in title, I have _token
private val _token = MutableLiveData<String>()
that should update
val userPackages: LiveData<List<Packages>> = Transformations.switchMap(_token) {
packagesLiveData(it)
}
after i use
fun setToken(token: String) {
Log.d(TAG, "setToken: $token")
_token.postValue(token)
}
from this Log.D I know that im getting valid string
i tried
_token.value = token
but nothing changed
I see from this function that im calling inside switchmap
private fun packagesLiveData(string: String): LiveData<List<Packages>> {
Log.d(TAG, "switchMap: $string")
return liveData {
tvRepository
.getUserPackage(string)
.asLiveData()
}
}
that im not getting any change (cos this function not being called at all)
or if i inicialize value of _token to any string then i see it being called twice but with this initialized value and not with the value from _token.value that i confirmed while testing is indeed changing but switchmap can never access
EDIT
It looks like i just didn't observed my liveData but now when i try to do this I'm getting error that i don't have get function for this val
I'm getting my model in my fragment from this
private val tvViewModel: TvViewModel by viewModels()
and this is how my observer looks like
tvViewModel.userPackages.observe(viewLifecycleOwner, {
Log.d(TAG, "setTvObservers: ${it?.get(0)}")
})
Requested ViewModel:
#HiltViewModel
class TvViewModel #Inject constructor(
private val tvRepository: TVRepository
) :
ViewModel() {
companion object {
const val TAG = "TvViewModel"
}
private val _tvPath = MutableLiveData<String>()
private val _token = MutableLiveData<String>()
val tvPath: LiveData<String> = _tvPath
val userPackages: LiveData<List<Packages>> = Transformations.switchMap(_token) {
packagesLiveData(it)
}
fun setTvPath(path: String) {
_tvPath.postValue(path)
}
fun setToken(token: String) {
Log.d(TAG, "setToken: $token")
_token.postValue(token)
}
private fun packagesLiveData(string: String): LiveData<List<Packages>> {
Log.d(TAG, "switchMap: $string")
return liveData(Dispatchers.IO) {
tvRepository
.getUserPackage(string)
.asLiveData()
}
}
}
Are you observing this LiveData, i.e. userPackages somewhere?
Because as mentioned here,
The transformations aren't calculated unless an observer is observing the returned LiveData object.
So make sure you are observing userPackages somewhere, if it still doesn't work do tell, we'll try to find a solution again :)
EDIT:
By looking at the way you were observing the LiveData using viewLifecycleOwner, it seems that you are observing it inside a fragment. So, inside a fragment, you do not get the ViewModel by calling by viewModels(). Instead, you need to use by activityViewModels<TvViewModel>
if it still doesn't work let me know, we'll try to look again :)
LATEST UPDATE
ok so even though this doesn't give any compile-time warnings, but inside your packagesLiveData, the tvRepository.getUserPackage(string).asLiveData() returns a LiveData, and you are using livedata wrapper again around it. What you need to do is something like this:
private fun packagesLiveData(string: String): LiveData<List<Packages>> {
Log.d(TAG, "switchMap: $string")
return tvRepository
.getUserPackage(string)
.asLiveData()
}
I have a property with late initialization.
Now I want to provide a live data which does not emit anything until the property is initialized completely.
How to do this in proper Kotlin way?
class SomeConnection {
val data: Flow<SomeData>
...
class MyViewModel {
private lateinit var _connection: SomeConnection
// private val _connection: CompletableDeferred<SomeConnection>()
val data = _coonection.ensureInitilized().data.toLiveData()
fun connect(){
viewModelScope.launch {
val conn = establishConnection()
// Here I have to do something for the call ensureInitilized to proceed
}
}
private suspend fun establishConnection(){
...
}
Declare a MutableLiveData emitting values of type SomeConnection and a corresponding LiveData.
private val _connectionLiveData = MutableLiveData<SomeConnection>()
val connectionLiveData: LiveData<SomeConnection> = _connectionLiveData
Then assign value to _connectionLiveData when _connection is initialized:
if (::_connection.isInitialized()) _connectionLiveData.value = _connection
(or _connectionLiveData.postValue(_connection) if your code works concurrently)
Now observe this LiveData in another place in code, I'll use fragment here for the sake of example:
override fun firstOnViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.connectionLiveData.observe(this, ::sendData)
}
Then send the desired data via the corresponding view model method
In my ViewModel I have two MutableLiveData for the response of my webservice :
val getFindByCategorySuccess: MutableLiveData<List<DigitalService>> by lazy {
MutableLiveData<List<DigitalService>>()
}
val getFindByCategoryError: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
and this method for the request :
fun requestFindByCategory(categoryId: String){
viewModelScope.launch {
when (val retrofitPost = digitalServicesRemoteRepository.getFindByCategoryRequest(categoryId)) {
is ApiResult.Success -> getFindByCategorySuccess.postValue(retrofitPost.data)
is ApiResult.Error -> getFindByCategoryError.postValue(retrofitPost.exception)
}
}
}
It's working fine using it in my Fragment class :
viewModel.getFindByCategorySuccess.observe(viewLifecycleOwner, { digitalServices ->
logD("I have a good response from the webservice; luanch an other fragment now!")
})
The problem is if I go to an other fragment in my observable (using findNavController().navigate(action)). If I go back to the previous fragment, I go automatically to the nextFragment because the observable is called again.
So I'm looking for solutions...
Maybe clearing all my viewmodel when I go back to my fragment ?
Maybe clearing only getFindByCategorySuccess and getFindByCategoryError ?
Maybe an other solution? I think my architecture is not good. What do you think about it ?
By default, a livedata will emit to its current state (the value that exist on it) for any new observer that subscribes to it.
Answering your question, you might try the operator distincUntilChanged transformation, which, according to the documentation:
Creates a new LiveData object that does not emit a value until the source LiveData value has been changed. The value is considered changed if equals() yields false.
But, this showcases a problem with your snippet, and a bad practice that is common when using livedata, you shouldn't expose mutable live data to your observers. Instead, you should expose a non-mutable version of them.
In your case, in my opinion, your view model should look like the following:
private val getFindByCategorySuccess by lazy {
MutableLiveData<List<DigitalService>>()
}
private val getFindByCategoryError by lazy {
MutableLiveData<String>()
}
val onFindByCategorySuccess: LiveData<List<DigitalService>
get() = getFindByCategorySuccess.distincUntilChanged()
val onFindCategoryError: LiveData<List<String>
get() = getFindByCategoryrRror.distincUntilChanged()
And your observers would subscribe as follows:
ExampleFragment
fun setupObservers() {
viewModel.onFindByCategorySuccess.observe(viewLifecycleOwner) { // Do stuff }
}
I hope it helps
I found a solution to my problem using this class :
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, { t ->
if (mPending.compareAndSet(true, false))
observer.onChanged(t)
})
}
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
}
Like this :
var getFindByCategorySuccess: SingleLiveEvent<List<DigitalService>> = SingleLiveEvent()
var getFindByCategoryError: SingleLiveEvent<String> = SingleLiveEvent()
I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}