update RecyclerView with Android LiveData - android

There are many examples how to push new list to adapter on LiveData change.
I'm trying to update one row (e.g number of comments for post) in the huge list. It would be stupid to reset whole list to change only one field.
I am able to add observer onBindViewHolder, but I can't understand when should I remove observer
#Override
public void onBindViewHolder(ViewHolder vh, int position) {
Post post = getPost(position);
vh.itemView.setTag(post);
post.getLiveName().observeForever(vh.nameObserver);
...
}

Like #Lyla said, you should observe the whole list as LiveData in Fragment or Activity, when receive changes, you should set the whole list to the adapter by DiffUtil.
Fake code:
PostViewModel {
LiveData<List<Post>> posts; // posts comes from DAO or Webservice
}
MyFragment extends LifecycleFragment {
PostAdapter postAdapter;
...
void onActivityCreated() {
...
postViewModel.posts.observer(this, (postList) -> {
postAdapter.setPosts(postList);
}
}
}
PostAdapter {
void setPosts(List<Post> postList) {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {...}
...
}
}

Using DiffUtil might help with updating one row in a huge list. You can then have LiveData wrap the list of comments instead of a single comment or attribute of a comment.
Here's an example of using DiffUtil within a RecyclerView adapter and the list LiveData observation code in the fragment.

Use Transformations.switchMap() to swap the underlying Post object. Then there is no need to remove and re-add observers when the cell is recycled.
#Override
public void onBindViewHolder(PostViewHolder vh, int position) {
Post post = getPost(position);
vh.bind(post);
}
Then in your ViewHolder class
public class PostViewHolder extends RecyclerView.ViewHolder {
private final MutableLiveData<Post> post = new MutableLiveData<>();
public PostViewHolder(View itemView) {
super(itemView);
LiveData<String> name = Transformations.switchMap(post, new Function<Post, LiveData<String>>() {
#Override
public LiveData<String> apply(Post input) {
return input.getLiveName();
}
});
name.observeForever(new Observer<String>() {
#Override
public void onChanged(#Nullable String name) {
// use name
}
});
}
public void bind(Post post) {
post.setValue(post);
}
}

I think you should use LiveAdapter for RecyclerView Adapter instead of creating an extra class for the adapter.
It has DiffUtil implementation as well, so only single item will be updated.
and without calling notifyDatasetChange.
// Kotlin sample
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
onBind{
}
onClick{
}
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
onBind{
}
onClick{
}
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.into(recyclerview)

add context to your adapterClass construstor : AdpaterClass(context: Context)
then smart cast the context to AppCompactActivity
livedata.observe(context as AppCompatActivity, Observer {it ->
//perform action on it(livedata.value)
})
when calling the adapter from anywhere activity, fragment pass the context into the adpater

Related

Why list changes fires on whole list

I`m using mvp with repository to update items on recyclerview when item at firestore database are changing.
As asked, here is some more code from SharedModelClass:
public LiveData<List<Task>> tasksListening() {
return repository.tasksListening(false);
}
which lead to:
public LiveData<List<Task>> tasksListening(boolean b) {
return new FirestoreTasksData(tasksReference, b);
}
Here is FirestoreTasksData extends LiveData<List>:
public class FirestoreTasksData extends LiveData<List<Task>> {
List<Task> tasks;
for (QueryDocumentSnapshot doc : value) {
Task item = doc.toObject(Task.class);
tasks.add(item);
}
setValue(tasks);
};
}
All works perfect except that list is updated in whole even when updating one item.
sharedViewModel.tasksListening().observe(getViewLifecycleOwner(), tasks -> {
tasksAdapter.submitList(tasks);
});
and here is adapter code:
public class MyTasksAdapter extends RecyclerView.Adapter<MyTasksAdapter.TaskHolder> {
private final AsyncListDiffer<Task> mDiffer = new AsyncListDiffer<>(this, DIFF_CALLBACK);
private static final DiffUtil.ItemCallback<Task> DIFF_CALLBACK = new DiffUtil.ItemCallback<Task>() {
#Override
public boolean areItemsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.getId().equals(newItem.getId());
}
#Override
public boolean areContentsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.geteDate().equals(newItem.geteDate()) && (new HashSet<>(oldItem.getRoles().values()).equals(new HashSet<>(newItem.getRoles().values())));
}
};
}
public void submitList(List<Task> list) {
mDiffer.submitList(list);
}
#Override
public int getItemCount() {
return mDiffer.getCurrentList().size();
}
Is this a bug or a feature? I was using Firestore UI recycler adapter before, just decided to refactor code.
That is expected behavior. When there's any change to the results of a query/collection, your code gets called with a QuerySnapshot object of all changes that match the query/collection.
If you want to see what has changed, you can look at the getDocumentChanges() of the snapshot to see those. For more on this (and an example) see the documentation on viewing change between snapshots.
Rather than implement custom adapter and DiffUtil Callback.
Take a look at LiveAdapter.
You just need to add the latest dependency in Gradle.
dependencies {
implementation 'com.github.RaviKoradiya:LiveAdapter:1.3.2-1608532016'
// kapt 'com.android.databinding:compiler:GRADLE_PLUGIN_VERSION' // this line only for Kotlin projects
}
and bind adapter with your RecyclerView
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
}
.into(recyclerview)
That's it. Not need to write extra code for adapter implementation, observe LiveData and notify the adapter.
Wanted to delete post but the rules.
I solved the problem. The problem is that all working good. Feel free to use code if needed.
I just realized that inside observer called another method, which is the reason I see rendering effect as if there were some bug with adapter.
sharedViewModel.tasksListening().observe(getViewLifecycleOwner(), tasks -> {
tasksAdapter.submitList(tasks);
checkEmpty(tasks.size());
});
private void checkEmpty(int size) {
if (size == 0) {
crossFade(noDataLayout, mTasksRecycler);
} else {
crossFade(mTasksRecycler, noDataLayout);
}
}
I fixed that and now all is fine.

NotifyDataSetChanged method does not refresh my RecyclerView Kotlin

I`m trying to understand why adapter method notifyDataSetChanged() not refresh my recyclerview. I find a solution when create method in adapter like this:
fun setData(list: List<DownloadModel>){
resumeList = list
notifyDataSetChanged()
}
This solution works but, i want to know why i can't do something like this:
private lateinit var downloadAdapter: DownloadRecyclerAdapter
private fun setupAdapter() {
downloadAdapter = DownloadRecyclerAdapter(
this#DownloadActivity,
downloadList,
{ id -> onViewClick(id) },
{ id -> onEditClick(id) },
{ id, position -> onDeleteClick(id, position) }
)
savedResumeRv.apply {
layoutManager = LinearLayoutManager(context)
layoutAnimation = AnimationUtils.loadLayoutAnimation(
this#DownloadActivity,
R.anim.layout_animation_down_to_up
)
adapter = downloadAdapter
}
}
private fun observers() {
downloadViewModel.getDownloadList().observe(this, Observer { list ->
downloadList = list
list?.let {
downloadAdapter.notifyDataSetChanged()
}
})
}
downloadAdapter hold same instance of list downloadList, and i wonder why when i notify adapter in activity not work properly.
downloadList = list
because of this line it is not same reference to download list anymore
try
private fun observers() {
downloadViewModel.getDownloadList().observe(this, Observer { list ->
with(downloadList){
clear()
addAll(list)
}
downloadList?.let {
downloadAdapter.notifyDataSetChanged()
}
})
}
You have to do:
val resumeList: MutableList<DownloadModel>
fun setData(list: List<DownloadModel>){
resumeList.clear()
resumeList.addAll(list)
notifyDataSetChanged()
}
I can agree that the previous reply will work for you, but it breaks the encapsulation of the data that the Adapter is holding. That is why in the first place you've hit that bug.
No client of the Adapter should be able to modify directly it's data without any protection.

Observable's Correct explanation in rxjava and rxandroid

What are the correct concepts and working of observables and observers in RxJava. I get confused by the words literal meaning. Whenever I change the values of observables its corresponding observers is not getting invoked i.e. I will explain this situation a bit more deeply, initially when I assign an observable with a list of strings(List list) and subscribe it to an observer, observer works perfectly but after that ,when I change the values of list(for example adding more String values to list) ...the observer's on next should automatically be invoked right.. but it isn't. Trying to implement in Android natively . I will be happy for some helps.
Observables work with three methods from Observer: onNext, onError and onCompleted. When you make Observable from a list and you subscribe it Observable will emit those values using onNext method and when it's finished it will call onCompleted method.
You can't change values that Observable is emitting by changing list you gave to some Observable operator. What would be you desired behaviour. Should Observable emit all elements on list change or should it emit only new changes.
This observable will emit all changes to collection made trough setCollection method:
public class CollectionObservable<T> extends Observable<T> {
private Collection<T> collection;
private List<Observer<? super T>> observers;
public CollectionObservable(Collection<T> collection) {
if (collection != null) {
this.collection = collection;
}
this.observers = new ArrayList<>(2);
}
public Collection<T> getCollection() {
return collection;
}
public void setCollection(Collection<T> collection) {
this.collection = collection;
emitValuesToAllObserver();
}
public void complete() {
if (this.collection != null) {
for (Observer<? super T> observer : this.observers) {
observer.onComplete();
}
}
}
#Override
protected void subscribeActual(Observer<? super T> observer) {
this.observers.add(observer);
emitValues(observer);
}
private void emitValuesToAllObserver() {
for (Observer<? super T> observer : this.observers) {
emitValues(observer);
}
}
private void emitValues(Observer<? super T> observer) {
if (this.collection != null) {
for (T obj : this.collection) {
observer.onNext(obj);
}
}
}
}
Note that in order to finish you manually have to call complete method.

Could not filter a PagedList?

I've implemented a recyclerview with paging with the Android's Paging Library (https://developer.android.com/topic/libraries/architecture/paging.html). It works fine on fetching data and retrieve subsequent pages. However, how to filter the PagedList ? Say I have a Search widget, and I want to search the list currently on screen. PagedList.filter() returns a List and PagedListAdapter.setList() won't accept a List.
I think you might be able to solve this with a MediatorLiveData.
Specifically Transformations.switchMap and some additional magic.
Currently I was using
public void reloadTasks() {
if(liveResults != null) {
liveResults.removeObserver(this);
}
liveResults = getFilteredResults();
liveResults.observeForever(this);
}
But if you think about it, you should be able to solve this without use of observeForever, especially if we consider that switchMap is also doing something similar.
So what we need is a LiveData that is switch-mapped to the LiveData> that we need.
private MutableLiveData<String> filterText = new MutableLiveData<>();
private final LiveData<List<T>> data;
public MyViewModel() {
data = Transformations.switchMap(
filterText,
(input) -> {
if(input == null || input.equals("")) {
return repository.getData();
} else {
return repository.getFilteredData(input); }
}
});
}
public LiveData<List<T>> getData() {
return data;
}
This way the actual changes from one to another are handled by a MediatorLiveData. If we want to cache the LiveDatas, then we can do in the anonymous instance that we pass to the method.
data = Transformations.switchMap(
filterText, new Function<String, LiveData<List<T>>>() {
private Map<String, LiveData<List<T>>> cachedLiveData = new HashMap<>();
#Override
public LiveData<List<T>> apply(String input) {
// ...
}
}

MvxRecyclerView Fluent API binding

I am unable to bind ItemClick from MvxRecyclerView (or its Adapter) to a command on my ViewModel using Fluent API. It works if I put both ItemsSource and ItemClick in the XML so I am not interested in such solution.
I used this post as an excellent guideline (How to use the MvvmCross fluent API to bind a RecyclerView item's TextView to a property of its ViewModel on Android?) and all of that works except that I am unable to bind ItemClick on MvxRecyclerView (or the adapter) to a MainViewModel's command which will take me to the next fragment (ItemsSource works like a charm but its a property and not a command!).
For the sake of brevity, I will not be copying the code from the original post (How to use the MvvmCross fluent API to bind a RecyclerView item's TextView to a property of its ViewModel on Android?) so assume that the MainViewModel from that post has been enhanced with a command ShowItemCommand as such:
public class MainViewModel : MvxViewModel
{
private IEnumerable<ViewModelItem> _viewModelItems;
public IEnumerable<ViewModelItem> ViewModelItems
{
get { return _viewModelItems; }
set { SetProperty(ref _viewModelItems, value); }
}
public MvxCommand<ViewModelItem> ShowItemCommand
{
get
{
return new MvxCommand<ViewModelItem>(selectedItem =>
{
ShowViewModel<ViewModelItem>
(new { itemId = selectedItem.Id });
});
}
}
}
and everything else has been implemented as per the referenced post.
So now, in addition to ItemsSource, I want to wire up ItemClick on the MvxRecyclerView (or the Adapter) to the command. The reason these are interchangeable is that MvxRecyclerView just relays these commands to the Adapter.
Apparently, this should work...but it does not:
adapter.ItemClick = ViewModel.ShowItemCommand;
This does not work either:
set.Bind(recyclerView).For(v => v.ItemClick).To(vm => vm.ShowItemCommand);
When creating a custom MvxRecyclerViewHolder you need to make sure that you assign the Click command over to the ViewHolder. This is done in the OnCreateViewHolder override of your custom adapter.
Example for custom ViewHolder
public class MyAdapter : MvxRecyclerAdapter
{
public MyAdapter(IMvxAndroidBindingContext bindingContext)
: base(bindingContext)
{
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
var itemBindingContext = new MvxAndroidBindingContext(parent.Context, this.BindingContext.LayoutInflaterHolder);
var view = this.InflateViewForHolder(parent, viewType, itemBindingContext);
return new MyViewHolder(view, itemBindingContext)
{
Click = ItemClick,
LongClick = ItemLongClick
};
}
}
I can't reproduce your issue. I just created a new project, added a RecyclerView and added the following binding:
var set = this.CreateBindingSet<FirstView, FirstViewModel>();
set.Bind(recyclerView).For(v => v.ItemsSource).To(vm => vm.ViewModelItems);
set.Bind(recyclerView).For(v => v.ItemClick).To(vm => vm.ShowItemCommand);
set.Apply();
This works just as expected, where ItemClick triggers the ShowItemCommand. VM's look as follows:
public class ViewModelItem : MvxViewModel
{
public void Init(string itemId)
{
Mvx.Trace($"Showing {itemId}");
}
public string Id { get; set; }
}
public class FirstViewModel
: MvxViewModel
{
public FirstViewModel()
{
ViewModelItems = new ViewModelItem[] {
new ViewModelItem { Id = "Hello"},
new ViewModelItem { Id = "World"},
new ViewModelItem { Id = "Foo"},
new ViewModelItem { Id = "Bar"},
new ViewModelItem { Id = "Baz"}
};
}
private IEnumerable<ViewModelItem> _viewModelItems;
public IEnumerable<ViewModelItem> ViewModelItems
{
get { return _viewModelItems; }
set { SetProperty(ref _viewModelItems, value); }
}
public MvxCommand<ViewModelItem> ShowItemCommand =>
new MvxCommand<ViewModelItem>(DoShowItem);
private void DoShowItem(ViewModelItem item)
{
ShowViewModel<ViewModelItem>(new { itemId = item.Id });
}
}

Categories

Resources