Hi what is the best way of passing data from a callback function defined in a different class to your activity back to your activity. I am new to android development so sorry if some of this is obvious. I am using an SDK provided by Xsens and a bit of background basically they provide sensors which connect to your device via bluetooth and then stream data back to your device such as acceleration, orientation etc...
The way my code is written is I scan for the sensors, they are then listed on my app and I can press connect on each sensor. When the connected button is clicked the callback class is defined (mine is called ConnectScannedDevice())
Inside this ConnectScannedDevice Class I have overridden the following function and written the below code
override fun onXsensDotDataChanged(address: String, XsensDotData: XsensDotData) {
XsensDotData.acc.forEachIndexed() { index, value ->
Log.d("Sensor Data Acceleration $index", value.toString())
}
XsensDotData.dq.forEachIndexed { index, value ->
Log.d("Sensor Data Orientation $index", value.toString())
}
}
This callback function is hit when I start measuring on the device by using the following code connectedDevice.startMeasuring() this is when the callback function is hit.
I have a setOnClickListener in my activity which then runs the above code to make the device start measuring.
What I now need to do is pass the data the callback function is logging to logcat back to the activity. What is the best way of passing the data the callback function is logging to my activity where the button was pressed.
In the SDK documentation it mentions The XsensDotData object has implemented the Parcelable object from Java, so this object can be passed to another class by Broadcast event.
When the device starts measuring it is a constant stream of data until I stop it from measuring, I need to pass all this data back to the activity. I am trying to display this data onto a graph.
The following is not tested but it outlines the logic of creating a LiveData stream.
In your ConnectScannedDevice class add a private MutableLiveData<XsensDotData> property (that you will update as data changes) and a LiveData<XsensDotData> that you will expose to the ViewModel.
// This is changed internally (never expose a MutableLiveData)
private var xSensorDataMutableLiveData = MutableLiveData<XsensDotData>()
// Changed switchMap() to map()
var xSensorDataLiveData: LiveData<XsensDotData> =
Transformations.map(xSensorDataMutableLiveData) { data ->
data
}
When the data changes you want to update your xSensorMutableDataLiveData in the onXsensDotDataChanged function
override fun onXsensDotDataChanged(address: String, xSensDotData: XsensDotData) {
xSensorDataMutableLiveData.value = XsensDotData
}
Now implement you ViewModel class as follows
// Uncomment if you want to use savedState
//class DeviceDataViewModel(private val savedState: SavedStateHandle): ViewModel() {
class DeviceDataViewModel(): ViewModel() {
// define and initialize a ConnectScannedDevice property
val connectScannedDevice = ConnecScannedDevice() // or however you initialize it
// get a reference to its LiveData
var xSensorDataLiveData = connectScannedDevice.xSensorDataLiveData
}
In your activity get hold of the ViewModel like this
private val deviceDataViewModel by lazy {
ViewModelProvider(this).get(DeviceDataViewModel::class.java)
}
Register your activity to observe the LiveData coming from the ViewModel in the onCreate(Bundle?) function and define how to respond
deviceDataViewModel.xSensorDataLiveData.observe(
this,
Observer { xSensDotData ->
xSensDotData.acc.forEachIndexed() { index, value ->
Log.d("Sensor Data Acceleration $index", value.toString())
}
xSensDotData.dq.forEachIndexed { index, value ->
Log.d("Sensor Data Orientation $index", value.toString())
}
}
)
Note
Google recommends the use of a repository pattern, which means you should add a singleton repository class that does what your ViewModel is currently doing. Your ViewModel should then get hold of the repository instance and simply get its LiveData and pass it on to the activity.
The repository's job is to gather all data from all sources and pass them on to various ViewModels. The ViewModel encapsulates the data needed by activities/fragments and helps make the data survive configuration changes. Activities and fragments get their data from ViewModels and display them on screen.
Related
I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!
I needed some direction on being able to observe some flow as live data in my ViewModel class.
For example: The ViewModel class has the field userDataFlow below which combines a few streams of Data Flow. I want to be able to extract out the work of that field into a separate class and let all of the inner working take place there and just want to observe the LiveData to the field in the ViewModel. I would need to pass in few things in the Parameter of that class from the ViewModel which the Flow would need in order to work. Not sure if this is a good practice. Basically, let my ViewModel observe the result and pass it along to the View.
val userDataFlow: Flow<List<UserData>> =
combine(
familyChannel.asFlow(),
userRealTimeData.asFlow,
).asLiveData()
}
Sounds like you need a UseCase/Interactor which in short processes data coming from different repositories.
For example suppose you want a list of your friends that live in countries with a COVID-19 infection rate above a certain value:
class GetFriendsInDangerUseCase(
private val friendsRepository: FriendsRepository,
private val countryRepository: CountryRepository)
fun invoke(threshold: Float) = friendsRepository.friendsFlow
.combine(countryRepository.countriesFlow) { friends, countries ->
val dangerousCountries = countries.filter { it.infectionRate >= threshold }
friends.filter { it.country in dangerousCountries }
}
Then use it like this from your VM:
val friendsInDangerFlow = getFriendsInDanger(0.5)
I have a ViewModel which has a property of type LiveData<UserData>, being read from a Room database.
Its code is as follows:
class UserDataViewModel(application: Application) : AndroidViewModel(application) {
private val userDataDao: UserDataDao = AppDatabase.getInstance(application).dao()
val userData: LiveData<UserData?> = userDataDao.getUserData()
}
In the associated activity, I get a reference to the view model:
private val viewModel: UserDataViewModel by viewModels()
In that activity, I need to get the value of the UserData on a button click:
private fun handleClick(view: View) {
viewModel.userData.value?.let {
// do stuff if the userData is present
}
}
Now in theory, unless the user presses the button before the data has been loaded, this should never be null.
However, as the code stands, the call to viewModel.userData.value is always null and the let block never executes.
But, if I add this statement in onCreate, the let block in the click handler executes as desired:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.userData.observe(this, Observer {
// do nothing
})
}
My question is: why do I need to call the observe function, even if I'm doing nothing with a change event, to get valid responses from LiveData::getValue?
My question is: why do I need to call the observe function, even if I'm doing nothing with a change event, to get valid responses from LiveData::getValue?
Because the ComputableLiveData returned from the Room DAO only executes the query if the LiveData has at least one active observer (inside LiveData.onActive()). Then it runs asynchronously on a different thread, and at some point in the future it will be posted into the LiveData.
You do not need to call observe() in order to get a LiveData to give up a value other than null. LiveData always contains and yields null initially until something sets its value. If you don't want this initial null value, then you should immediately set it to something else instead, before making the LiveData available to any other components. If you want to know when it first contains a non-null value, you will need to use an observer.
emit accepts the data class whereas emitSource accepts LiveData<T> ( T -> data ). Considering the following example :- I have two type of calls :-
suspend fun getData(): Data // returns directly data
and the other one ;
suspend fun getData(): LiveData<Data> // returns live data instead
For the first case i can use:-
liveData {
emit(LOADING)
emit(getData())
}
My question : Using the above method would solve my problem , WHY do we need emitSource(liveData) anyway ?
Any good use-case for using the emitSource method would make it clear !
As you mentioned, I don't think it solves anything in your stated problem, but I usually use it like this:
If I want to show cached data to the user from the db while I get fresh data from remote, with only emit it would look something like this:
liveData{
emit(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
emit(db.getData())
}
But with emitSource it looks like this:
liveData{
emitSource(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
}
Don't need to call emit again since the liveData already have a source.
From what I understand emit(someValue) is similar to myData.value = someValue whereas emitSource(someLiveValue) is similar to myData = someLiveValue. This means that you can use emit whenever you want to set a value once, but if you want to connect your live data to another live data value you use emit source. An example would be emitting live data from a call to room (using emitSource(someLiveData)) then performing a network query and emitting an error (using emit(someError)).
I found a real use-case which depicts the use of emitSource over emit which I have used many times in production now. :D The use-case:
Suppose u have some user data (User which has some fields like userId, userName ) returned by some ApiService.
The User Model:
data class User(var userId: String, var userName: String)
The userName is required by the view/activity to paint the UI. And the userId is used to make another API call which returns the UserData like profileImage , emailId.
The UserData Model:
data class UserData(var profileImage: String, var emailId: String)
This can be achieved internally using emitSource by wiring the two liveData in the ViewModel like:
User liveData -
val userLiveData: LiveData<User> = liveData {
emit(service.getUser())
}
UserData liveData -
val userDataLiveData: LiveData<UserData> = liveData {
emitSource(userLiveData.switchMap {
liveData {
emit(service.getUserData(it.userId))
}
})
}
So, in the activity / view one can ONLY call getUser() and the getUserData(userId) will be automatically triggered internally via switchMap.
You need not manually call the getUserData(id) by passing the id.
This is a simple example, imagine there is a chain of dependent-tasks which needs to be executed one after the other, each of which is observed in the activity. emitSource comes in handy
With emitSource() you can not only emit a single value, but attach your LiveData to another LiveData and start emitting from it. Anyway, each emit() or emitSource() call will remove the previously added source.
var someData = liveData {
val cachedData = dataRepository.getCachedData()
emit(cachedData)
val actualData = dataRepository.getData()
emitSource(actualData)
}
The activity that’s observing the someData object, will quickly receive the cached data on the device and update the UI. Then, the LiveData itself will take care of making the network request and replace the cached data with a new live stream of data, that will eventually trigger the Activity observer and update the UI with the updated info.
Source: Exploring new Coroutines and Lifecycle Architectural Components integration on Android
I will like share a example where we use "emit" and "emitsource" both to communicate from UI -> View Model -> Repository
Repository layer we use emit to send the values downstream :
suspend fun fetchNews(): Flow<Result<List<Article>>> {
val queryPath = QueryPath("tata", apikey = AppConstant.API_KEY)
return flow {
emit(
Result.success(
openNewsAPI.getResponse(
"everything",
queryPath.searchTitle,
queryPath.page,
queryPath.apikey
).articles
)
)
}.catch { exception ->
emit(Result.failure(RuntimeException(exception.message)));
}
}
ViewModel layer we use emitsource to pass the live data object to UI for subscriptions
val loader = MutableLiveData<Boolean>()
val newsListLiveData = liveData<Result<List<Article>>> {
loader.postValue(true)
emitSource(newRepo.fetchNews()
.onEach {
loader.postValue(false)
}
.asLiveData())
}
UI Layer - we observe the live data emitted by emitsource
viewModel.newsListLiveData.observe(viewLifecycleOwner, { result ->
val listArticle = result.getOrNull()
if (result.isSuccess && listArticle != null) {
setupList(binding.list, listArticle)
} else {
Toast.makeText(
appContext,
result.exceptionOrNull()?.message + "Error",
Toast.LENGTH_LONG
).show()
}
})
We convert Flow observable to LiveData in viewModel
I created live data which emits a single event as in this example.
My question is next:
How to notify only last subscribed observer when the value in the LiveData changes?
What comes to my mind is to store observers in the linked list in SingleLiveData class and then to call super.observe only if a passed observer is the same as the last element of the list.
I'm not sure if this is the best approach.
I want to use this mechanism to propagate FAB click events from activity to the fragments which are shown inside of the ViewPager. Fragments are dynamically added to view pager adapter, so let's say that we know the order of the fragments.
In the end, I found a workaround for this problem. I had to move away from the live data that emits a single event since it couldn't behave the way I needed it to behave.
Instead of this, I used simple mutable live data which emits an event object which wraps a data as in the last paragraph of this article by Jose Alcérreca.
I'm showing fragments in a view pager so I have only one visible fragment at the time.
So my view model looks like this:
class ActionViewModel : ViewModel() {
private val onCreateLiveData: MutableLiveData<Event<String>> = MutableLiveData()
fun observeOnCreateEvent(): LiveData<Event<String>> = onCreateLiveData
fun onCreateCollectionClick(message: String) {
this.onCreateLiveData.value = Event(message)
}
}
Event wrapper class implementation looks like this:
/*Used as a wrapper for data that is exposed via a LiveData that represents an
event.*/
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
}
In fragments now we can observe events like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionViewModel::class.java)
actionViewModel.observeOnCreateEvent()
.observe(this, Observer {
it?.takeIf { userVisibleHint }?.getContentIfNotHandled()?.let {
//DO what ever is needed
}
})
}
Fragment userVisibleHint property will return true if the fragment is currently visible to the user. Since we are only showing one fragment at the time this works for us. This means that the fragment will only access the event data if it is visible.
Also, implementation of the Event wrapper allows only one read of the value, so that every next time Observer gets this event, its value will be null and we'll ignore it.
Conclusion: This way we are simulating a single event live data which notifies only last subscribed observer.
If you're using Kotlin, you can replace LiveData with Flow. StateFlow can be used to replace regular LiveData, while SharedFlow can be used for stateless events. It will also provide you null safety and all the operators and configurations that come with Flow.
The migration is described here among other places. Here's a basic example:
ViewModel:
interface MyViewModel {
val myData: StateFlow<MyData>
val myEvents: SharedFlow<MyEvent>
}
class MyViewModelImpl: MyViewModel {
override val myData = MutableStateFlow(MyData())
override val myEvents = MutableSharedFlow<MyEvent>(replay = 0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST)
/*
* Do stuff
*/
}
Activity:
lifecycleScope.launch {
myData.collect {
// handle stateful data
}
}
lifecycleScope.launch {
myEvents.collect {
// handle stateless events
}
}
Note that lifecycleScope requires the appropriate ktx dependency.
Herer's some more reading about Flow in Android.
I found solution for me in LD extension:
fun <T> LiveData<T>.observeAsEvent(owner: LifecycleOwner, observer: Observer<in T>) {
var previousKey: Any? = value?: NULL
observe(owner) { value ->
if (previousKey == NULL || previousKey != value) {
previousKey = value
observer.onChanged(value)
}
}
}
private const val NULL = "NULL"
Usage for this:
viewModel.resultLiveData.observeAsEvent(viewLifecycleOwner) {
...
}
I crafted a solution, feel free to take a look
https://github.com/ueen/LiveEvent
I've created a library to handle the most common cases that we might encounter while working with event-driven data scenarios
https://github.com/javaherisaber/LiveX
It contains the following types of classes:
LiveData
LiveEvent
OneShotLiveEvent
SingleLiveEvent
Multiple observers can register, all of them receive the event based on lifecycle
Multiple observers can register, each one receive the event only once
Only one observer can register and receive the event only once
Multiple observers can register, only the first one receive the event