In Google's official codelab about advanced-coroutines-codelab sample, they've used ConflatedBroadcastChannel to watch a variable/object change.
I've used the same technique in one of my side projects, and when resuming the listening activity, sometimes ConflatedBroadcastChannel fires it's recent value, causing the execution of flatMapLatest body without any change.
I think this is happening while the system collects the garbage since I can reproduce this issue by calling System.gc() from another activity.
Here's the code
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
Here's the full source code :
CoroutinesFlowTest.zip
Why is this happening?
What am I missing?
Quoting from the official response, (The simple and straightforward solution)
The problem here is that you are trying to use
ConflatedBroadcastChannel for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel for events.
To fix it, you can replace ConflatedBroadcastChannel with
BroadcastChannel<IncrementRequest>(1) (non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
In addition to the answer of Kiskae:
This might not be your case, but you can try to use BroadcastChannel(1).asFlow().conflate on a receiver side, but in my case it led to a bug where the code on a receiver side didn't get triggered sometimes (I think because conflate works in a separate coroutine or something).
Or you can use a custom version of stateless ConflatedBroadcastChannel (found here).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
On Coroutine 1.4.2 and Kotlin 1.4.31
Without using live data
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
Use Flow and Coroutine
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this#MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
Without using BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
The reason is very simple, ViewModels can persist outside of the lifecycle of Activities. By moving to another activity and garbagecollecting you're disposing of the original MainActivity but keeping the original MainViewModel.
Then when you return from DetailActivity it recreates MainActivity but reuses the viewmodel, which still has the broadcastchannel with a last known value, triggering the callback when count.observe is called.
If you add logging to observe the onCreate and onDestroy methods of the activity you should see the lifecycle getting advanced, while the viewmodel should only be created once.
Related
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 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!!
}
}
I am using MutableLiveData within my application for event based communication. I have single activity two fragments architecture.
With the help of ViewModel, I'm consuming the LiveData events in Fragment-1. But, when I replace this Fragment-1 with Fragment-2 using Menu bar and finally come back to Fragment-1, old values of LiveData are captured again.
How to avoid this problem? Any help/suggestions are highly appreciated!
Thank you.
You can use Event to wrap LiveData values to handle consuming its values as in the following article:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Event class would be like:
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Let us say that your LiveData value is a String then the LiveData of single event would be like:
val navigateToDetails = MutableLiveData<Event<String>>()
Problem with accepted answer is that you can only have one observer.
This article describes solution with multiple observers.
Wherever you're observing the liveData, in onChanged method remove the observers by calling myLiveDataObject.removeObservers(this);
This will remove the observer after first-time data is observed.
I faced the same problem and came up with this library to solve it
https://github.com/ueen/LiveEvent
Hope this helps, enjoy!
Simple, clean, reusable:
class Event<T>(val payload: T, var broadcasted: Boolean = false)
class MutableEventLiveData<T>: MutableLiveData<Event<T>>() {
fun postEvent(value: T) {
super.postValue(Event(value))
}
}
typealias EventLiveData<T> = LiveData<Event<T>>
class EventObserver<T>(private val broadcastCallback: (t: T)->Unit): Observer<Event<T>> {
override fun onChanged(e: Event<T>) {
if (!e.broadcasted) {
broadcastCallback(e.payload)
e.broadcasted = true
}
}
}
Sample usage:
class YourViewModel : ViewModel() {
private val _errorEvent = MutableEventLiveData<String>()
val errorEvent: EventLiveData<String>
get() = _errorEvent
fun fireErrorEvent(errorMessage: String) {
_errorEvent.postEvent(errorMessage)
}
...
}
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
//Note!!! EventObserver handles events not Observer
viewModel.errorEvent.observe(this, EventObserver {
errorMessage -> showErrorMessage(errorMessage)
})
}
...
}
I have a coroutine I'd like to fire up at android startup during the splash page. I'd like to wait for the data to come back before I start the next activity. What is the best way to do this? Currently our android is using experimental coroutines 0.26.0...can't change this just yet.
UPDATED: We are now using the latest coroutines and no longer experimental
onResume() {
loadData()
}
fun loadData() = GlobalScope.launch {
val job = GlobalScope.async {
startLibraryCall()
}
// TODO await on success
job.await()
startActivity(startnewIntent)
}
fun startLibraryCall() {
val thirdPartyLib() = ThirdPartyLibrary()
thirdPartyLib.setOnDataListener() {
///psuedocode for success/ fail listeners
onSuccess -> ///TODO return data
onFail -> /// TODO return other data
}
}
The first point is that I would change your loadData function into a suspending function instead of using launch. It's better to have the option to define at call site how you want to proceed with the execution. For example when implementing a test you may want to call your coroutine inside a runBlocking. You should also implement structured concurrency properly instead of relying on GlobalScope.
On the other side of the problem I would implement an extension function on the ThirdPartyLibrary that turns its async calls into a suspending function. This way you will ensure that the calling coroutine actually waits for the Library call to have some value in it.
Since we made loadData a suspending function we can now ensure that it will only start the new activity when the ThirdPartyLibrary call finishes.
import kotlinx.coroutines.*
import kotlin.coroutines.*
class InitialActivity : AppCompatActivity(), CoroutineScope {
private lateinit var masterJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + masterJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
masterJob = Job()
}
override fun onDestroy() {
super.onDestroy()
masterJob.cancel()
}
override fun onResume() {
this.launch {
val data = ThirdPartyLibrary().suspendLoadData()
// TODO: act on data!
startActivity(startNewIntent)
}
}
}
suspend fun ThirdPartyLibrary.suspendLoadData(): Data = suspendCoroutine { cont ->
setOnDataListener(
onSuccess = { cont.resume(it) },
onFail = { cont.resumeWithException(it) }
)
startLoadingData()
}
You can use LiveData
liveData.value = job.await()
And then add in onCreate() for example
liveData.observe(currentActivity, observer)
In observer just wait until value not null and then start your new activity
Observer { result ->
result?.let {
startActivity(newActivityIntent)
}
}
Activity receiving intent
class AddNoteActivity : AppCompatActivity() {
private lateinit var addViewModel: NoteViewModel
private lateinit var titleEditText: TextInputEditText
private lateinit var contentEditText: TextInputEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_note_activty)
setSupportActionBar(toolbar)
addViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
titleEditText = findViewById(R.id.itemTitle)
contentEditText = findViewById(R.id.itemNote)
val extra = intent.extras
if (extra != null) {
val uuid = extra.getLong("UUID")
val note: Notes? = addViewModel.getNote(uuid)
titleEditText.setText(note!!.title)
contentEditText.setText(note.note)
}
}
}
NoteViewModel class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private var note: Notes = Notes(0, "", "test title", "test ontent")
}
fun getNote(uuid: Long?): Notes {
val job = async(CommonPool) {
getNoteAsyncTask(notesDatabase).execute(uuid)
}
runBlocking { job.await() }
return note
}
class getNoteAsyncTask(database: NotesDatabase) : AsyncTask<Long, Unit, Unit>() {
private val db: NotesDatabase = database
override fun doInBackground(vararg params: Long?) {
note = db.notesDataDao().getNote(params[0])
}
}
}
If I pass an intent to get a Note object from the database with a uuid and set that received data in titleEditText and contentEditText, the data set in the Note was from previous intent invoked when we clicked on the Note item in RecyclerView. On clicking the Note item for the first time, I get the default value which I have set "test title" and "test content".
Aforementioned is the behavior most of the time. Sometimes the data set in titleEditText and contentEditText is of the correct Note object.
Can someone please tell me what I have done wrong? How can I correct my apps behavior?
Unfortunately, there is a big mistake in how you use a view model to provide a data to your view(AddNoteActivity).
Basically, your view never has a chance to wait for the data to be fetched as it always receives a default value. This happens because the AsyncTask runs on its own thread pool so the coroutine completes immediately and returns a default value.
You should consider using LiveData to post a new object to your view and refactor your view model.
So, you need to make a query to the database synchronous and observe changes to a note rather than have a getter for it. Of course, in a real life scenario it might be a good idea to have different kind of states to be able to show a spinner while a user is waiting. But this is another big question. So to keep things simple consider changing your view model to something like that:
class NoteViewModel(private val database: NotesDatabase) : ViewModel { // you do not need an application class here
private val _notes = MutableLiveData<Notes>()
val notes: LiveData<Notes> = _notes
fun loadNotes(uuid: Long) {
launch(CommonPool) {
val notes = database.notesDataDao().getNote(uuid)
_notes.setValue(notes)
}
}
}
Then, you can observe changes to the note field in your activity.
class AddNoteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val noteViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
noteViewModel.notes.observe(this, Observer {
title.text = it.title
content.text = it.note
})
}
}
Also you need to use a ViewModelProvider.Factory to create your view model and properly inject dependencies into it. Try to avoid having a context there as it makes it much harder to test.