How to implement realtime search using firestore [duplicate] - java

I have two queries:
Query firstQuery = ref.orderBy("name", Query.Direction.ASCENDING).limit(10);
getData(firstQuery);
Query secondQuery = ref.orderBy("price", Query.Direction.ASCENDING).limit(10);
getMoreData(secondQuery);
The first method looks like this:
private void getData(Query query) {
firestoreRecyclerOptions = new FirestoreRecyclerOptions.Builder<ModelClass>().setQuery(query, ModelClass.class).build();
myFirestoreRecyclerAdapter = new MyFirestoreRecyclerAdapter(firestoreRecyclerOptions);
recyclerView.setAdapter(myFirestoreRecyclerAdapter);
}
And here is the second method.
private void getMoreData(Query query) {
firestoreRecyclerOptions = new FirestoreRecyclerOptions.Builder<ModelClass>().setQuery(query, ModelClass.class).build();
myFirestoreRecyclerAdapter = new MyFirestoreRecyclerAdapter(firestoreRecyclerOptions);
recyclerView.setAdapter(myFirestoreRecyclerAdapter);
}
Both variables are declared as global:
private FirestoreRecyclerOptions<ModelClass> firestoreRecyclerOptions;
private MyFirestoreRecyclerAdapter myFirestoreRecyclerAdapter;
When the app starts, the elements are displayed in the RecyclerView using the first method. What I want to achieve is that on a button click, when the getMoreData() method is triggered to add the result from the second query in the same adapter, ending up having 20 elements. Now, when I click the button, the elements from the second query will override the first ones.

There is nothing built-in to combine two queries in a FirestoreRecyclerAdapter.
The best I can think of is creating a List/array of the combined results in your app code and then using an array adapter. It's not ideal, since you won't be using FirebaseUI.
Alternatively, have a look at FirebaseUIs FirestorePagingAdapter, which combines multiples pages of (non-realtime) DocumentSnapshots in a single recycler view.

I ended up using a modified version of adapter class from the Friendly-eats code lab sample.
The following class allows you add an initial query and then set another one using the FirestoreAdapter.setQuery(query) method.
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.firestore.*
import com.google.firebase.firestore.EventListener
import java.util.*
/**
* RecyclerView adapter for displaying the results of a Firestore [Query].
*
* Note that this class forgoes some efficiency to gain simplicity. For example, the result of
* [DocumentSnapshot.toObject] is not cached so the same object may be deserialized
* many times as the user scrolls.
*
*
* See the adapter classes in FirebaseUI (https://github.com/firebase/FirebaseUI-Android/tree/master/firestore) for a
* more efficient implementation of a Firestore RecyclerView Adapter.
*/
abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query,
private val lifecycleOwner: LifecycleOwner)
: RecyclerView.Adapter<VH>(), EventListener<QuerySnapshot>, LifecycleObserver {
private var listener: ListenerRegistration? = null
private val snapshots = ArrayList<DocumentSnapshot>()
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startListening() {
if (listener == null) {
listener = query.addSnapshotListener(this)
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopListening() {
listener?.apply {
remove()
listener = null
}
snapshots.clear()
notifyDataSetChanged()
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
internal fun cleanup(source: LifecycleOwner) {
source.lifecycle.removeObserver(this)
}
override fun onEvent(snapshot: QuerySnapshot?, error: FirebaseFirestoreException?) {
when {
error != null -> onError(error)
else -> {
// Dispatch the event
snapshot?.apply {
for (change in documentChanges) {
when (change.type) {
DocumentChange.Type.ADDED -> onDocumentAdded(change)
DocumentChange.Type.MODIFIED -> onDocumentModified(change)
DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
}
}
onDataChanged()
}
}
}
}
protected fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
protected fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
protected fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
fun setQuery(query: Query) {
stopListening()
// Clear existing data
snapshots.clear()
notifyDataSetChanged()
// Listen to new query
this.query = query
startListening()
}
override fun getItemCount(): Int = snapshots.size
protected fun getSnapshot(index: Int): DocumentSnapshot = snapshots[index]
protected open fun onError(exception: FirebaseFirestoreException) {}
protected open fun onDataChanged() {}
}

Related

How to reference view objects inside a list?

I'm trying to create a method to set a listener to each of my views inside a list like:
private fun setListeners() {
val clickableViews: List<View> =
listOf(box_one_text, box_two_text, box_three_text,
box_four_text, box_five_text)
for(item in clickableViews){
item.setOnClickListener{makeColored(it)}
}
}
box_one_text, box_two_text and so on are the id of the views inside my xml file and I'm trying to set a color of it when they are clicked like:
fun makeColored(view: View) {
when (view.id) {
// Boxes using Color class colors for background
R.id.box_one_text -> view.setBackgroundColor(Color.DKGRAY)
R.id.box_two_text -> view.setBackgroundColor(Color.GRAY)
// Boxes using Android color resources for background
R.id.box_three_text -> view.setBackgroundResource(android.R.color.holo_green_light)
R.id.box_four_text -> view.setBackgroundResource(android.R.color.holo_green_dark)
R.id.box_five_text -> view.setBackgroundResource(android.R.color.holo_green_light)
else -> view.setBackgroundColor(Color.LTGRAY)
}
}
the problems is that all of the elements inside the list are all red lines or can't reference by the list
First thing first, the list type is wrong. You said that box_one_text and box_two_text are the view id. View id type is Int, so you should change the list to list of Int
val clickableViews: List<Int> =
listOf(box_one_text, box_two_text, box_three_text,
box_four_text, box_five_text)
Then, to apply a click listener to each of the id, you need to find the view using findViewById
for(item in clickableViews){
findViewById<View>(item).setOnClickListener{makeColored(it)}
}
Or if you use view binding, you can follow below code:
val clickableViews: List<View> =
listOf(binding.boxOneText, binding.boxTwoText, binding.boxThreeText,
binding.boxFourText, binding.boxFiveText)
for(item in clickableViews){
item.setOnClickListener{makeColored(it)}
}
Try this:
private fun setListeners() {
val clickableViews: List<Int> =
listOf(R.id.box_one_text, R.id.box_two_text, R.id.box_three_text,
R.id.box_four_text, R.id.box_five_text)
for(item in clickableViews){
findViewById<View>(item).setOnClickListener{makeColored(it)}
}
}

How can I avoid "Inconsistency detected. Invalid item position" when updating Realm from separate thread?

UPDATE:
I see the same error ("Inconsistency detected. Invalid view holder adapter position") in another situation - this time when bulk adding.
The situation is I am implementing a nested recyclerview, each of which uses a RealmRecyclerViewAdapter and each has an OrderedRealmCollection as its basis. The result I'm going after is this:
I have implemented this at the first level by a query for distinct items in my realm keyed off of year and month:
OrderedRealmCollection<Media> monthMedias = InTouchDataMgr.get().getDistinctMedias(null,new String[]{Media.MEDIA_SELECT_YEAR,Media.MEDIA_SELECT_MONTH});
This gives me one entry for July, one for August, of 2019 in this example.
Then for each ViewHolder in that list, during the bind phase I make another query to determine how many Media items are in each month for that year:
void bindItem(Media media) {
this.media = media;
// Get all the images associated with the year in that date, set adapter in recyclerview
OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getAllMediasForYearAndMonth(null, media.getYear(), media.getMonth());
// This adapter loads the CardView's recyclerView with a StaggeredGridLayoutManager
int minSize = Math.min(MAX_THUMBNAILS_PER_MONTH_CARDVIEW, medias.size());
imageRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(minSize >= 3 ? 3 : Math.max(minSize, 1), LinearLayoutManager.VERTICAL));
imageRecyclerView.setAdapter(new RealmCardViewMediaAdapter(medias, MAX_THUMBNAILS_PER_MONTH_CARDVIEW));
}
At this point I have the single month which is bound to the first ViewHolder, and now I have the count of media for that month, and I want to cause this ViewHolder to display a sampling of those items (maximum of MAX_THUMBNAILS_PER_MONTH_CARDVIEW which is initialized as 5) with the full count shown in the header.
So I pass the full OrderedRealmCollection of that media to the "second level" adapter that handles the list for this CardView.
That adapter looks like this:
private class RealmCardViewMediaAdapter extends RealmRecyclerViewAdapter<Media, CardViewMediaHolder> {
int forcedCount = NO_FORCED_COUNT;
RealmCardViewMediaAdapter(OrderedRealmCollection<Media> data, int forcedCount) {
super(data, true);
this.forcedCount = forcedCount;
}
#NonNull
#Override
public CardViewMediaHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(InTouch.getInstance().getApplicationContext());
View view = layoutInflater.inflate(R.layout.timeline_recycler_row_content, parent, false);
return new CardViewMediaHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CardViewMediaHolder holder, int position) {
// Let Glide load the thumbnail
GlideApp.with(InTouch.getInstance().getApplicationContext())
.load(Objects.requireNonNull(getData()).get(position).getUriPathToMedia())
.thumbnail(0.05f)
.placeholder(InTouchUtils.getProgressDrawable())
.error(R.drawable.ic_image_error)
.into(holder.mMediaImageView);
}
#Override
public int getItemCount() {
//TODO - the below attempts to keep the item count at forced count when so specified, but this is causing
// "Inconsistency detected. Invalid view holder adapter position" exceptions when adding a bulk number of images
return (forcedCount == NO_FORCED_COUNT ? getData().size() : Math.min(forcedCount,getData().size()));
//return getData().size();
}
}
So what this is attempting to do is limit the number of items reported by the adapter to the smaller set of thumbnails to show in the first level CardView to a max of 5, spread around using that StaggeredGridLayout.
All this works perfectly until I do a bulk add from another thread. The use case is the user has selected the FAB to add images, and they have selected a bunch (my test was ~250). Then the Uri for all of this is passed to a thread, which does a callback into the method below:
public void handleMediaCreateRequest(ArrayList<Uri> mediaUris, String listId) {
if ( handlingAutoAddRequest) {
// This will only be done a single time when in autoAdd mode, so clear it here
// then add to it below
autoAddedIDs.clear();
}
// This method called from a thread, so different realm needed.
Realm threadedRealm = InTouchDataMgr.get().getRealm();
try {
// For each mediaPath, create a new Media and add it to the Realm
int x = 0;
for ( Uri uri: mediaUris) {
try {
Media media = new Media();
InTouchUtils.populateMediaFromUri(this, media, uri);
InTouchDataMgr.get().addMedia(media, STATUS_UNKNOWN, threadedRealm);
autoAddedIDs.add(media.getId());
if ( x > 2) {
// Let user see what is going on
runOnUiThread(this::updateUI);
x = 0;
}
x++;
} catch (Exception e) {
Timber.e("Error creating new media in a batch, uri was %s, error was %s", uri.getPath(),e.getMessage());
}
}
} finally {
InTouchDataMgr.get().closeRealmSafely(threadedRealm);
runOnUiThread(this::updateUI);
}
}
This method is operating against the realm, which is then making its normal callback into the OrderedCollection which is the base of the list in the recyclerview(s).
The addMedia() method is standard Realm activity, and works fine everywhere else.
updateUI() essentially causes an adapter.notifyDataSetChanged() call, in this case to the RealmCardViewMediaAdapter.
If I either don't use a separate thread, or I don't attempt to limit the number of items the adapter returns to a max of 5 items, then this all works perfectly.
If I leave the limit of 5 in as the return value from getItemCount() and don't refresh the UI until all has been added, then this also works, even from a different thread.
So it seems there is something about notifyDataSetChanged() being called as the Realm based list of managed objects is being updated in real time that is generating this error. But I don't know why or how to fix?
UPDATE END
I am using Realm Java DB 6.0.2 and realm:android-adapters:3.1.0
I created a class that extends RealmRecyclerViewAdapter class for my RecyclerView:
class ItemViewAdapter extends RealmRecyclerViewAdapter<RealmObject, BindableViewHolder> implements Filterable {
ItemViewAdapter(OrderedRealmCollection data) {
super(data, true);
}
I am initializing this adapter using the standard pattern of passing an OrderedRealmCollection to the adapter:
ItemViewAdapter createItemAdapter() {
return new ItemViewAdapter(realm.where(Contact.class).sort("displayName"));
}
"realm" has been previously initialized in the class creating the adapter.
I allow the user to identify one or more rows in this recyclerView that they want to delete, then I execute an AsyncTask which calls the method handling the delete:
public static class DoHandleMultiDeleteFromAlertTask extends AsyncTask {
private final WeakReference<ListActivity> listActivity;
private final ActionMode mode;
DoHandleMultiDeleteFromAlertTask(ListActivity listActivity, ActionMode mode) {
this.listActivity = new WeakReference<>(listActivity);
this.mode = mode;
}
#Override
protected void onPreExecute() {
listActivity.get().mProgressBar.setVisibility(View.VISIBLE);
}
#Override
protected void onPostExecute(Object o) {
// Cause multi-select to end and selected map to clear
mode.finish();
listActivity.get().mProgressBar.setVisibility(View.GONE);
listActivity.get().updateUI(); // Calls a notifyDataSetChanged() call on the adapter
}
#Override
protected Object doInBackground(Object[] objects) {
// Cause deletion to happen.
listActivity.get().handleMultiItemDeleteFromAlert();
return null;
}
}
Inside handleMultiItemDeleteFromAlert(), since we are being called from a different thread I create and close a Realm instance to do the delete work:
void handleMultiItemDeleteFromAlert() {
Realm handleDeleteRealm = InTouchDataMgr.get().getRealm();
try {
String contactId;
ArrayList<String> contactIds = new ArrayList<>();
for (String key : mSelectedPositions.keySet()) {
// The key finds the Contact ID to delete
contactId = mSelectedPositions.getString(key);
if (contactId != null) {
contactIds.add(contactId);
}
}
// Since we are running this from the non-UI thread, I pass a runnable that will
// Update the UI every 3rd delete to give the use some sense of activity happening.
InTouchDataMgr.get().deleteContact(contactIds, handleDeleteRealm, () -> runOnUiThread(ContactListActivity.this::updateUI));
} finally {
InTouchDataMgr.get().closeRealmSafely(handleDeleteRealm);
}
}
And the deleteContact() method looks like this:
public void deleteContact(ArrayList<String> contactIds, Realm realm, Runnable UIRefreshRunnable) {
boolean success = false;
try {
realm.beginTransaction();
int x = 0;
for ( String contactId : contactIds ) {
Contact c = getContact(contactId, realm);
if (c == null) {
continue;
}
// Delete from the realm
c.deleteFromRealm();
if ( UIRefreshRunnable != null && x > 2 ) {
try {
UIRefreshRunnable.run();
} catch (Exception e) {
//No-op
}
x = 0;
}
x++;
}
success = true;
} catch (Exception e) {
Timber.d("Exception deleting contact from realm: %s", e.getMessage());
} finally {
if (success) {
realm.commitTransaction();
} else {
realm.cancelTransaction();
}
}
Now my problem - when I was doing this work entirely from the UI thread I had no errors. But now when the transaction is committed I am getting:
Inconsistency detected. Invalid item position 1(offset:-1).state:5 androidx.recyclerview.widget.RecyclerView{f6a65cc VFED..... .F....ID 0,0-1440,2240 #7f090158 app:id/list_recycler_view}, adapter:com.reddragon.intouch.ui.ListActivity$ItemViewAdapter#5d77178,
<a bunch of other lines here>
I thought that RealmRecyclerViewAdapter already registered listeners, kept everything straight, etc. What more do I need to do?
The reason I am using a separate thread here is that if a user identifies a few dozen (or perhaps hundreds) items in the list to delete, it can take several seconds to perform the delete (depending on which list we are talking about - there are various checks and other updates that have to happen to preferences, etc.), and I didn't want the UI locked during this process.
How is the adapter getting "inconsistent"?
I solved this by adjusting the architecture slightly. It seems there is some issue with StaggeredGridLayoutManager when mixed with:
The dynamic ability of RealmRecyclerView to update itself automatically during bulk adds or deletes.
An adapter that attempts to limit what is shown by returning a count from getItemCount() that is not equal to the current list count.
I suspect this has to do with how ViewHolder instances get created and positioned by the layout manager, since this is where the error is pointing to.
So what I did instead is rather than have the adapter return a value that can be less than the actual count of the list being managed at any point in time, I changed the realm query to use the .limit() capability. Even when the query returns less than the limit initially, it has the nice side effect of capping itself at the limit requested as the list dynamically grows from the bulk add. And it has the benefit of allowing getItemCount() to return whatever the current size of that list is (which always works).
To recap - when in "Month View" (where I want the user to only see a max of 5 images like the screen shot above), the first step is to populate the adapter of the top level RealmRecyclerView with the result of a DISTINCT type query which results in an OrderedRealmCollection of Media objects which correspond to each month from each year in my media Library.
Then in the "bind" flow of that adapter, the MonthViewHolder performs the second realm query, this time with a limit() clause:
OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getMediasForYearAndMonthWithLimit(null,
media.getYear(),
media.getMonth(),
MAX_THUMBNAILS_PER_MONTH_CARDVIEW); // Limit the results to our max per month
Then the adapter for the RealmRecyclerView associated with this specific month uses the results of this query as its list to manage.
Now it can return getData().size() as the result of the getItemCount() call whether I am in the Month view (capped by limit() ) or in my week view which returns all media items for that week and year.

Firestore Paging Adapter- How to know if query returns 0 results

I'm using firestore paging adapter to populate my RecyclerView with data from Firestore, if collection in Firestore is empty I would like to show a TextView to inform user about that, if it is not then I would like to populate EecyclerView with data but I don't see a way to do this with Firestore paging adapter because I can't acces data from inside the fragment where I create adapter
Im my Fragment inside onViewCreated
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(2)
.setPageSize(5)
.build()
val options = FirestorePagingOptions.Builder<Question>()
.setLifecycleOwner(viewLifecycleOwner)
.setQuery(FirestoreUtil.myFeedQuery, config, Question::class.java)
.build()
mAdapter = WallFeedRVAdapter(this, options)
WallFeedRVAdapter is RecyclerView adapter where I populate MyViewHolder with loaded data. How can I from this current fragment that hosts RecyclerView know if myFeedQuery returned any results so I can update recyclerView visibility to GONE and emptyInfoTextView to VISIBLE.
To get the number of items that are returned by the query which is passed to the FirestorePagingOptions object, you need to use getItemCount() method that exist in your adapter class. Because the data from Cloud Firestore is loaded asynchronously, you cannot simply call getItemCount() directly in your adapter class, as it will always be zero. So in order to get the total number of items, you need to register an observer like in the following lines of code:
mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
public void onItemRangeInserted(int positionStart, int itemCount) {
int totalNumberOfItems = adapter.getItemCount();
Log.d(TAG, String.valueOf(totalNumberOfItems));
if(totalNumberOfItems == 0) {
recyclerView.setVisibility(View.GONE);
emptyInfoTextView.setVisibility(View.VISIBLE);
}
}
});
I solved this problem by simple code. Just override onLoadingStateChangedListner method inside your FirestorePagingAdapter and then use getItemCount.
I implemented it as follow -
#Override
protected void onLoadingStateChanged(#NonNull LoadingState state) {
switch (state){
case LOADED:
case FINISHED:
progressBar.setVisibility(View.GONE);
if(getItemCount() == 0) {
emptyIcon.setVisibility(View.VISIBLE);
recyclerview.setVisibility(View.GONE);
}
break;
}
super.onLoadingStateChanged(state);
}
solved issue with registerAdapterDataObserver

Android Kotlin: How to update recyclerView after async call? getting CalledFromWrongThreadException

I am trying to do an async call and then update a RecyclerView. Much like what's outlined in this question: RecyclerView element update + async network call
However, when I try to do this, I get this error:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
Here is my code (the main issue is in the setAlbums function):
class AlbumActivity : AppCompatActivity() {
protected lateinit var adapter: MyRecyclerViewAdapter
protected lateinit var recyclerView: RecyclerView
var animalNames = listOf("nothing")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_album)
recyclerView = findViewById<RecyclerView>(R.id.rvAnimals)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MyRecyclerViewAdapter(this, animalNames)
recyclerView.adapter = adapter
urlCall("https://rss.itunes.apple.com/api/v1/us/apple-music/coming-soon/all/10/explicit.json")
}
private fun urlCall(url: String) {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) = getJSON(response.body()?.string())
})
}
fun getJSON(data: String?) {
val gson = Gson()
val allAlbums = ArrayList<Album>()
val jsonResponse = JSONObject(data)
val feed = jsonResponse.getJSONObject("feed")
val albums = feed.getJSONArray("results")
for (i in 0 until albums.length()) {
val album = albums.getJSONObject(i)
allAlbums.add(gson.fromJson(album.toString(), Album::class.java))
}
setAlbums(allAlbums)
}
fun setAlbums(albums: ArrayList<*>) {
animalNames = listOf("sue", "betsie")
adapter.notifyDataSetChanged() // This is where I am telling the adapter the data has changed
}
internal inner class Album {
var artistName: String? = null
var name: String? = null
}
}
Does anyone know the issue I am having?
You need to execute your wanted code on your main thread like this :
runOnUiThread(new Runnable() {
#Override
public void run() {
//update your UI
}
});
Your Callback functions will be called on a background thread. In part, that is because OkHttp is not an Android-specific library, so it has no idea about Android's main application thread.
You will need to do something to update the UI in the main application thread. Modern options include:
Have your Callback update a MutableLiveData that your UI observes, as then the UI will get updates on the main application thread
Use existing recipes for using OkHttp with RxJava

Android: MVVM is it possible to display a message (toast/snackbar etc.) from the ViewModel

I want to know what is the best approach to display some sort of message in the view from the ViewModel. My ViewModel is making a POST call and "onResult" I want to pop up a message to the user containing a certain message.
This is my ViewModel:
public class RegisterViewModel extends ViewModel implements Observable {
.
.
.
public void registerUser(PostUserRegDao postUserRegDao) {
repository.executeRegistration(postUserRegDao).enqueue(new Callback<RegistratedUserDTO>() {
#Override
public void onResponse(Call<RegistratedUserDTO> call, Response<RegistratedUserDTO> response) {
RegistratedUserDTO registratedUserDTO = response.body();
/// here I want to set the message and send it to the Activity
if (registratedUserDTO.getRegisterUserResultDTO().getError() != null) {
}
}
});
}
And my Activity:
public class RegisterActivity extends BaseActivity {
#Override
protected int layoutRes() {
return R.layout.activity_register;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
ActivityRegisterBinding binding = DataBindingUtil.setContentView(this, layoutRes());
binding.setViewModel(mRegisterViewModel);
}
What would the best approach be in this case?
We can use a SingleLiveEvent class as a solution. But it is a LiveData that will only send an update once. In my personal experience, using an Event Wrapper class with MutableLiveData is the best solution.
Here is a simple code sample.
Step 1 :
Create an Event class (this is a boilerplate code you can reuse for any android project).
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
}
Step 2 :
At the top of your View Model class, define a MutableLiveData with wrapper (I used a String here, but you can use your required data type), and a corresponding live data for encapsulation.
private val statusMessage = MutableLiveData<Event<String>>()
val message : LiveData<Event<String>>
get() = statusMessage
Step 3 :
You can update the status message within the functions of the ViewModel like this:
statusMessage.value = Event("User Updated Successfully")
Step 4 :
Write code to observe the live data from the View (activity or fragment)
yourViewModel.message.observe(this, Observer {
it.getContentIfNotHandled()?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
Display Toast/snackbar message in view (Activity/Fragment) from viewmodel using LiveData.
Step:
Add LiveData into your viewmodel
View just observe LiveData and update view related task
For example:
In Viewmodel:
var status = MutableLiveData<Boolean?>()
//In your network successfull response
status.value = true
In your Activity or fragment:
yourViewModelObject.status.observe(this, Observer { status ->
status?.let {
//Reset status value at first to prevent multitriggering
//and to be available to trigger action again
yourViewModelObject.status.value = null
//Display Toast or snackbar
}
})

Categories