I'm working with MVVM, and I have made different implementations of it, but one thing that is still making me doubt is how do I get data from a Repository (Firebase) from my ViewModel without attaching any lifecycle to the ViewModel.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea because I think I should communicate from my repository to my ViewModel either with a callback or a Transformation.
I leave here an example where I fetch a device from Firebase and update my UI, if we can see here, I'm observing the data coming from the repo from the UI, but from the ViewModel I'm also observing data from the repo, and here is where I really doubt if I'm using the right approach, since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
observeData(deviceId)
}
}
fun observeData(deviceId:String){
viewModel.fetchDeviceData(deviceId).observe(this, Observer {
textView.text = "Tipo: ${it.devType}"
})
ViewModel
class MainViewmodel: ViewModel() {
private val repo = Repo()
fun fetchDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
repo.getDeviceData(deviceId).observeForever {
mutableData.value = it
}
return mutableData
}
}
Repository
class Repo {
private val db = FirebaseDatabase.getInstance().reference
fun getDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
db.child(deviceId).child("config/device").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
})
return mutableData
}
}
This example just shows how to fetch the device from Firebase, it works, but from my ViewModel, it keeps making me think that observeForever() is not what I'm looking for to communicate data between the repository to the ViewModel.
I have seen Transformations, but I, in this case, I just need to deliver the entire Device object to my UI, so I don't need to transform the Object I'm retrieving to another Object
What should be here the right approach to communicate the repository and the ViewModel properly?
is observeForever lifecycle aware?
No, that's why it's called observeForever.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea
No, it's not, you should be using Transformations.switchMap {.
since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
Well if you're not clearing it in onCleared() using removeObserver(observer), then it won't clear itself, because it observes forever.
here is where I really doubt if I'm using the right approach,
No, you can do much better than this following a reactive approach.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
viewModel.onSelectedDeviceChanged(deviceId)
}
viewModel.selectedDevice.observe(this, Observer { device ->
textView.text = "Tipo: ${device.devType}"
})
}
And
class MainViewModel(
private val savedStateHandle: SavedStateHandle,
): ViewModel() {
private val repo = Repo() // TODO: move to Constructor Argument with ViewModelProvider.Factory
private val selectedDeviceId: MutableLiveData<String> = savedStateHandle.getLiveData<String>("selectedDeviceId")
fun onSelectedDeviceChanged(deviceId: String) {
selectedDeviceId.value = deviceId
}
val selectedDevice = Transformations.switchMap(selectedDeviceId) { deviceId ->
repo.getDeviceData(deviceId)
}
}
And
class Repo {
private val db = FirebaseDatabase.getInstance().reference // TODO: move to constructor arg? Probably
fun getDeviceData(deviceId:String) : LiveData<Device> {
return object: MutableLiveData<Device>() {
private val mutableLiveData = this
private var query: Query? = null
private val listener: ValueEventListener = object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableLiveData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
}
override fun onActive() {
query?.removeEventListener(listener)
val query = db.child(deviceId).child("config/device")
this.query = query
query.addValueEventListener(listener)
}
override fun onInactive() {
query?.removeEventListener(listener)
query = null
}
}
}
}
This way, you can observe for changes made in Firebase (and therefore be notified of future changes made to your values) using LiveData, rather than only execute a single fetch and then not be aware of changes made elsewhere to the same data.
To use ObserveForever, you need to remove the observer inside onClear in the ViewModel.
In this case, I would suggest to use Transformation even though you just need a direct mapping without any processing of the data, which is actually the same as what you are doing with the observer for observerForever.
observeForever() is not Lifecycle aware and will continue to run until removeObserver() is called.
In your ViewModel do this instead,
class MainViewmodel: ViewModel() {
private val repo = Repo()
private var deviceData : LiveData<Device>? = null
fun fetchDeviceData(deviceId:String):LiveData<Device>{
deviceData = repo.getDeviceData(deviceId)
return deviceData!!
}
}
Related
The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns
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'm building an application with latest android architecture components. I'm using firebase firestore as a database with jetpack navigation(Bottom nav). I'm successfully able to display data from DB. But Whenever I rotate mt screen the store fragment recreates & makes request to DB.
Repo
override fun getAllStores() = callbackFlow<State<List<Store>>> {
// Emit loading state
send(State.loading())
val listener = remoteDB.collection(Constants.COLLECTION_STORES)
.addSnapshotListener { querySnapshot, exception ->
querySnapshot?.toObjects(Store::class.java)?.let { store ->
// Emit success state with data
offer(State.success(store))
}
exception?.let {
// emit exception with message
offer(State.failed(it.message!!))
cancel()
}
}
awaitClose {
listener.remove()
cancel()
}
}.catch {
// Thrown exception on State Failed
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
ViewModel
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores()
}
Store Fragment
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreFragment : Fragment(R.layout.fragment_store) {
private lateinit var storeAdapter: StoreAdapter
private val viewModel: StoreViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).supportActionBar?.title = getString(R.string.store_title)
setUpRV()
// get all stores
lifecycleScope.launch {
getAllStores()
}
}
private suspend fun getAllStores() {
viewModel.getAllStores().collect { state ->
when (state) {
is State.Loading -> {
store_progress.show()
}
is State.Success -> {
storeAdapter.differ.submitList(state.data)
store_progress.animate().alpha(0f)
.withEndAction {
store_rv.animate().alpha(1f)
store_progress.hide()
}
}
is State.Failed -> {
store_progress.hide()
activity?.toast("Failed! ${state.message}")
}
}
}
}
private fun setUpRV() {
storeAdapter = StoreAdapter()
store_rv.apply {
adapter = storeAdapter
addItemDecoration(SpacesItemDecorator(16))
}
}
}
Main activity(Nav graph)
#InternalCoroutinesApi
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// init bottom navigation
bottom_navigation.setupWithNavController(nav_host_fragment.findNavController())
}
}
Every time it recreates my fragment. I don't want to save or retain any views using methods. Because ViewModel used to protect view on screen rotation. Kindly let me know any tips & tricks. Thanks in advance ;)
Flow in itself is not stateful - that is a key difference between it and LiveData. That means that after your collect completes, the next collect starts the callbackFlow from scratch.
This is precisely why the lifecycle-livedata-ktx artifact contains the asLiveData() extension that allows you to continue to use a Flow at the repository layer while maintaining the stateful (and Lifecycle) properties of LiveData for your UI:
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores().asLiveData()
}
You'd change your UI code to continue to use LiveData and observe().
Kotlin is working on a shareIn operation that would allow your ViewModel to save the state of a Flow. That would allow you to use Flow at all layers of your app without requerying information from scratch when the Fragment/Activity that is calling collect gets destroyed and recreated.
you can add android:configChanges="orientation|screenSize|screenLayout" to your manifest for the activity. this should prevents restarts when orientation changes.
check this site and also here are som infos.
I'm fairly new to Kotlin/Android development, and am trying to figure out the best way to update data in a Room database. After following some tutorials, I currently have an architecture that looks like this:
Room Database with tables and DAOs -> Repository -> ViewModel -> Activity
So the activity has a ViewModel that calls the Repository, which in turn updates the database.
The ViewModel for the activity has a LiveData list of the object (there's also a factory to create the ViewModel, but that's just to allow the bookId to be passed in):
class ViewBookViewModel(application: Application, bookId: Int) : AndroidViewModel(application) {
private val repository: AppRepository
internal val flashCards: LiveData<List<FlashCard>>
init {
val flashCardDao = AppDatabase.getDatabase(application, viewModelScope).flashCardDao()
val bookDao = AppDatabase.getDatabase(application, viewModelScope).bookDao()
repository = AppRepository(flashCardDao, bookDao)
flashCards = flashCardDao.getByBookId(bookId)
}
fun insert(flashCard: FlashCard) = viewModelScope.launch(Dispatchers.IO){
repository.insert(flashCard)
}
fun setIsFavorited(cardUid: Long, favorited: Boolean) = viewModelScope.launch(Dispatchers.IO) {
repository.setIsFavorited(cardUid, favorited)
}
}
//The actual query that gets called eventually
#Query("UPDATE flashcard SET is_favorited = :favorited WHERE uid LIKE :cardUid")
fun setFavorited(cardUid: Long, favorited: Boolean)
And the Activity sets up the viewModel and also creates an observer on the
class ViewBookActivity : AppCompatActivity() {
private lateinit var flashCards: LiveData<List<FlashCard>>
private var layoutManager: RecyclerView.LayoutManager? = null
private lateinit var viewModel: ViewBookViewModel
private var bookId: Int = 0
private lateinit var bookTitle: String
override fun onCreate(savedInstanceState: Bundle?) {
...
bookId = intent.extras["bookId"] as Int
bookTitle = intent.extras["bookTitle"].toString()
layoutManager = LinearLayoutManager(this)
flashCardRecyclerView.layoutManager = layoutManager
viewModel = ViewModelProviders.of(this, ViewBookViewModelFactory(application, bookId as Int)).get(ViewBookViewModel::class.java)
flashCards = viewModel.flashCards
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
})
}
}
Finally, I have a custom RecyclerAdapter, which is where I'm running into trouble. I have it set up so that when the user taps the "favorite" button on the Flash Card, it updates the database. However, this also causes the Activity to "refresh", scrolling to the top. I assume this is because it is observing LiveData, and that data is being changed.
custom RecylcerAdapter with ViewHolder code (stripped not-relevant code):
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val viewModel: ViewBookViewModel) : RecyclerView.Adapter<FlashCardRecyclerAdapter.FlashCardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlashCardViewHolder {
val v: View = LayoutInflater
.from(parent.context)
.inflate(R.layout.flash_card, parent, false)
return FlashCardViewHolder(v)
}
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
override fun getItemCount(): Int {
return flashCards.size
}
inner class FlashCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var mFavorited: Button
var frontShowing: Boolean
var isFavorited: Boolean = false
var uid: Long = 0
init {
mFavorited = itemView.findViewById(R.id.favoriteButton)
mFavorited.setOnClickListener { _ ->
isFavorited = !isFavorited
viewModel.setIsFavorited(uid, isFavorited) // Here is the database call
modifyFavoriteButtonImage(isFavorited)
}
}
fun modifyFavoriteButtonImage(isFavorited: Boolean){
// Code removed, just updates the image to be a filled/empty star based on favorited status
}
}
I feel like I am probably doing something wrong, as passing the ViewModel into the recylcer adapter in order to update the DB does not seem correct. Is there a pattern I should be using for this sort of situation, or should I change the code to not be using LiveData? Any help would be greatly appreciated.
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
}
you should not be making a new adapter instance here, instead, assign the values you get from the live data to the existing adapter (adapter.flashCards = flashCards, LiveData value) and call adapter.notifyDataSetChanged, this will tell your adapter that new data came in and it needs to update.
you should not be passing your ViewModel to your adapter (or anything).
you can do something like this instead:
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val callback:(FlashCard) -> Unit)
then, where you declare your adapter, you do this :
val adapter = FlashCardRecyclerAdapter(...) {
viewModel.update(it)
}
and then :
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.itemView.setOnClickListener {
callback.invoke(card)
}
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
In your repository method, I am not sure what you are doing there but rather than passing in a livedata instance, you should pass in the underlying data of the livedata instance. That way, the observer in the main activity doesn't get triggered everytime you call setIsFavorited(). If you do want to trigger the observer, then you can just call postValue() on the livedata instance. As for the adapter question, I do not know the best practices but I usually create a listener interface so I don't have to pass around my viewmodels everywhere. All of my viewmodels are contained within my fragments and never goes anywhere else. Let me know if this answers your questions.
Also, if you are using viewmodels with recyclerview, consider using list adapters. They are made to work seamlessly with viewmodels. https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
It makes it much simpler to use viewmodels with recyclerview.
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()
}