Android-Realm list with strict Model/View separation - android

I've got a RecyclerView backed by a Realm findAll(). I use a RealmChangeListener to notify the list about updates, and everything works remarkably well given the heavy use of the blunt instrument notifyDataSetChanged().
private RealmResults<Sale> allSales;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
....
// Update sales list whenever the AllSales result changes
allSales = getRealm().where(Sale.class).findAll();
allSalesListener = new RealmChangeListener<RealmResults<Sale>>() {
#Override
public void onChange(RealmResults<Sale> results) {
saleAdapter.notifyDataSetChanged();
}};
allSales.addChangeListener(allSalesListener);
....
However, I'd really like to have good MVVC structure, keeping all the Realm code in the ViewModel and out of my Fragments. The Realm examples don't do this. And probably for good reason -- I don't see an elegant way to notify the adapter appropriately of changes in the RealmResults. Databinding isn't there yet; it doesn't seem to support backing a RecyclerView with an ObservableCollection... and even if it did, a RealmResult isn't an ObservableCollection.
At this point, I'm thinking that I need to create a "ListChangedListener" interface in my Fragment, and manually maintain a collection of listeners for every List property in my ViewModel. But that seems like an awful lot of extra code just to maintain View/Model separation.
TLDR: I'm looking for an example of a Realm-backed ListView or RecyclerView with no Realm code whatsoever in the View code. Or even just reassurance that my custom "listener" interface is a good path forward.
UPDATE: I had somehow overlooked the RealmRecyclerViewAdapter. See my answer below.

The Realm library includes a RealmRecyclerViewAdapter base class, which I had somehow overlooked. No matter how good your MVVC intentions, the Adapter can't really be divorced from the model implementation, so it may as well be one that's intended for it.
Anyhow, it is very clean and compact. Do yourself a favour and review the example.
Here's a minimalist working implementation, with Android Databinding used for the row fields to make the Adapter and ViewHolder even cleaner and simpler:
private void setUpRecyclerView() {
// Called from your onCreateView(...)
recyclerView.setLayoutManager(new LinearLayoutManager(mainActivity));
recyclerView.setAdapter(new MyRecyclerViewAdapter(mainActivity, mainActivity.getDb().serialsRR));
recyclerView.setHasFixedSize(true);
}
public class MyRecyclerViewAdapter extends RealmRecyclerViewAdapter<Serial, MyRecyclerViewAdapter.SerialViewHolder> {
private final ActivityMain activity;
public MyRecyclerViewAdapter(ActivityMain activity, OrderedRealmCollection<Serial> data) {
super(activity, data, true);
this.activity = activity;
}
#Override
public SerialViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.serial_row, parent, false);
return new SerialViewHolder(itemView);
}
#Override
public void onBindViewHolder(SerialViewHolder holder, int position) {
SerialRowBinding rowBinding = holder.getBinding();
rowBinding.setSerial(getData().get(position));
}
class SerialViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
#Getter SerialRowBinding binding;
public SerialViewHolder(View view) {
super(view);
binding = DataBindingUtil.bind(view);
}
}
}

Related

Creating a generic PagedListAdapter using android paging library

My app has more than 4 lists of different data models.
I want to create a more generic CommonAdapter that extends PagedListAdapter
Here is my current code
public abstract class CommonPagedListAdapter<T, VH extends RecyclerView.ViewHolder>
extends PagedListAdapter<T, VH> {
private Context mContext;
private ArrayList<T> mArrayList;
public abstract void onItemClick(T model, int position);
public abstract int getLayoutResId();
protected CommonPagedListAdapter(Context context, ArrayList<T> arrayList,
#NonNull DiffUtil.ItemCallback<T> diffCallback) {
super(diffCallback);
this.mContext = context;
this.mArrayList = arrayList;
}
#NonNull
#Override
public VH onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
//what should i return here?
View view = LayoutInflater.from(mContext).inflate(getLayoutResId(),parent,false);
return (VH) new ItemViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull VH holder, int position) {
//what should i do here?
// holder
}
class ItemViewHolder extends RecyclerView.ViewHolder {
public ItemViewHolder(#NonNull View itemView) {
super(itemView);
}
}
}
I'm using PagelListAdapter from the Android Paging Library
I would like to know a few things:
- What should be set in the onCreateViewHolder as I'll be having different ViewHolders?
- What should be set in onBindViewHolder?
- Is this really the right way that makes the CommonPagedListAdapter extensible and maintainable?
I had a similar issue where I tried to create a single adapter to use for multiple different list types. I ultimately came to the conclusion that it is best to use a separate adapter for each list type solely because it avoids having to make a very big "common" adapter class which goes against the "single-responsibility" principle. That way, each adapter is a lot smaller, more maintainable, and flexible.
However, if you really want to use a single adapter for similar items, the way I usually do it is by creating unique ViewHolders for each item type and then binding them accordingly in onBindViewHolder using a switch statement or something similar. In order to do this, you would need to override an additional method called getItemViewType in your adapter.
There's a really good guide that goes over how to create an adapter that can handle different view types on Codepath: https://guides.codepath.com/android/Heterogenous-Layouts-inside-RecyclerView

Can/should RecyclerView.Adapter have access to the ViewHolders?

I have anEditTextin eachView, and inonViewRecycled(.)I update the item in theAdapterwith any user input. For the purpose of maintaining the information when scrollingViewsout of and back into view.
However, I realised that with this solution it's a bit of a hassle to retrieve the information that has been modified but not recycled. I solved this by adding eachViewHolderto a separate list in theAdapter(added inonCreateViewHolder(.)), since I couldn't find a simpler way to update theViewson the screen.
Is there any reason not to do this; let theAdapterhave direct access to theViewHolders? I.e. are there any better ways to call a function on allViewscurrently in existance?
Edit:
public class AdapterRv extends Adapter<AdapterRv.MyViewHolder> {
private List<Item> items;
private List<MyViewHoders> viewHolders;
public AdapterRv(List<Item> inItems) {
...
viewHolders = new ArrayList<>();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
private EditText text;
private Item item;
private MyViewHolder(View inView) {
...
}
public void bindItem(Item inItem) {
...
}
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup inParent, int inViewType) {
...
if (!viewHolders.contains(tmpHolder)) {
viewHolders.add(tmpHolder);
}
...
}
....
#Override
public void onViewRecycled(MyViewHolder inViewHolder) {
saveView(inViewHolder.item, inViewHolder.text.getText().toString());
}
private void saveViews() {
for (MyViewHolder tmpViewHolder : viewHolders) {
saveView(tmpViewHolder.item, tmpViewHolder.text.getText().toString());
}
}
private void saveView(Item inItem, String inNewText) {
if (inItem.getText().equals(inNewText)) {
return;
}
inItem.setText(inNewText);
}
public List<Item> fetchTexts() {
saveViews();
return items;
}
}
The purpose of the RecyclerView.Adapter is only to create ViewHolder instances and provide them with data, but it is the RecyclerView itself that requests holders and manages them.
The purpose of the ViewHolder is, as it name suggests, to hold the item's view hierarchy so the Views can be cached and reused by the RecyclerView. Hence, the adapter should only create and bind the correct data to holders, and it is recommended to not store any references to holders inside the adapter, since you only need the reference to holder in the onBindViewHolder(ViewHolder holder, int position method, where it is provided by the RecyclerView. It's also vice-versa, the view holders don't need a reference to the adapter, so your MyViewHolder should be marked static.
If you need to operate on recycler's views, the RecyclerView has plenty of methods in it's API, such as findViewHolderForLayoutPosition(int position), getChildViewHolder(View child) etc. You can also set listeners for observing scroll, item touch, view attach state etc. changes.
See the documentation for the RecyclerView
So, if you need to access and manipulate the views (ie. call the function on all of recycler's views), do it through the RecyclerView and not the adapter, because it's the recycler that manages them - adapter only provides data.

Better practice: reusing RecyclerViewAdapter or separate instance for each?

I have fragment "Incoming" on one slide of a viewpager. This fragment contains a RecyclerView populated with custom-relative-layouts. The LinearLayoutManager orientation is Vertical.
I have a second fragment "Find" on the next slide of the said viewpager. "Find" will consist of two recyclerviews. It will have a Horizontal recyclerview filled with cardviews (fast loading of profile pictures). Underneath that, I am loading more slowly another recyclerview with a custom-relative-layout, the same as in the "incoming" fragment.
Does that make sense? I'll elaborate some more:
The question is for these three recyclerviews, should I declare a new RecyclerAdapter for each one? The reason I ask is that they'll all have unknown variable item_counts.
Here is the RecyclerAdapter I have for "Incoming":
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder>{
private Context mContext;
public RecyclerAdapter(Context context, List<Incoming> items) {
mContext = context;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View v) {
super(v);
// Define all of the components in the view
}
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater mInf = LayoutInflater.from(mContext);
View customView = mInf.inflate(R.layout.item_layout_incoming, parent, false);
final ViewHolder vh = new ViewHolder(customView);
return vh;
}
#Override
public int getItemCount(){ return 6; } // THIS IS TEMPORARY; WILL BE VARIABLE
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Replace contents
}
For my criteria, should I create another Adapter for my horizontal-cardview-recyclerview? It seems repetitive, but otherwise, how would I handle either inflating cardview or item_layout_incoming?
Seems like there should be a DRY way to do this, without hits to performance. Thanks
You are using fragments so you will create 2 objects of that class. So it's the same thing. you just reduce compiler load by reducing the task of loading the new class into memory and then create its object.
It's better to use two different Adapter because of 2 reasons.
Your code will become ugly I mean so much congested and so many if
else condition.
In future, if you need to change something in layouts then again it
will affect all objects if same adapter class.
So my advice do developer friendly code and create two classes.

Databinding apply for one layout used by multiple activity/fragment

I am replacing existing code by databinding. But I face a problem.
I have some layout files shared by more than one activity/fragment. E.g there is a layout file layout_sub used by SubFragmentA and its extending class SubFragmentB. And the data model used in these two fragment are not the same.
The code looks like following.
public class SubFragmentA extends Fragment {
private DataA dataA;
#Override
public View onCreateView(Bundle Bundle) {
View v = LayoutInflator.from(getActivity()).inflate(R.layout.shared_layout);
initView(v, dataA);
return v;
}
private void initView(view v, DataA dataA) {
// use dataA to init v
}
}
public class SubFragmentB extends Fragment {
private DataB dataB;
#Override
public View onCreateView(Bundle Bundle) {
View v = LayoutInflator.from(getActivity()).inflate(R.layout.shared_layout);
initView(v, dataB);
return v;
}
private void initView(view v, DataB dataB) {
// use dataB to init v
}
}
So far, I think using DataA and DataB in layout_sub file at the same time is not a good idea, because it would require a lot of redundant code to decide which object to be used.
Please share your ideas on this problem.
Finally, I got a solution. The databinding is used for MVVM pattern. That means one layout corresponds to one ViewModel. And the ViewModel contains every data for UI layout. So I should prepare one ViewModel for each layout file. And every fragment/activity should just handle the ViewModel.

Challenges in RecyclerView of Android L

I have been playing around with RecyclerView for a little bit. Is there any easy way to put OnClickListener for items in RecyclerView? I have tried implementing it in ViewHolder. The onClick event never got triggered.
And I have used notifyItemInserted(position) for adding new value into RecyclerView. The UI does not got refreshed automatically. Needed to pull up and down to refresh. But when I invoke notifyDatasetChanged(..), it is ok.
I have applied DefaultItemAnimator to RecyclerView. But, not seeing any animation when new item added.
Thanks advance for any idea.
This is the first Android L component I have tested out and I am stucking there.
Here is my Adapter class:
public class AdapterRecyclerView extends RecyclerView.Adapter<AdapterRecyclerView.MyViewHolder> {
private List<String> arrExperiences;
//Provide a reference to the type of views that you are using - Custom ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder {
public TextView tvExperienceTitle;
public TextView tvExperienceDesc;
public MyViewHolder(RelativeLayout itemView) {
super(itemView);
tvExperienceTitle = (TextView) itemView.findViewById(R.id.tv_experience_title);
tvExperienceDesc = (TextView) itemView.findViewById(R.id.tv_experience_desc);
}
}
//Provide a suitable constructor : depending on the kind of dataset.
public AdapterRecyclerView(List<String> arrExperiences){
this.arrExperiences = arrExperiences;
}
//Create new view : invoke by a Layout Manager
#Override
public AdapterRecyclerView.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RelativeLayout view = (RelativeLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.view_item_recycler, parent, false);
MyViewHolder myViewHolder = new MyViewHolder(view);
return myViewHolder;
}
#Override
public void onBindViewHolder(AdapterRecyclerView.MyViewHolder viewHolder, int position) {
//get element from your dataset at this position.
//replace the content of the view with this element.
viewHolder.tvExperienceTitle.setText(arrExperiences.get(position));
}
#Override
public int getItemCount() {
return arrExperiences.size();
}
public void addExperience(String experience, int position){
arrExperiences.add(position, experience);
notifyItemInserted(position);
//notifyDataSetChanged();
}
public void removeExperience(){
int index = (int) (Math.random() * arrExperiences.size());
arrExperiences.remove(index);
notifyItemRemoved(index);
//notifyDataSetChanged();
}
}
Simply add this in your Adapter:
#Override
public void onBindViewHolder(AdapterRecyclerView.MyViewHolder viewHolder, int position) {
yourItems.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
//do your stuff
}
});
}
Please see my answer here. You do need an extra class (which may be included as part of the full release) but it will allow you to create OnItemClickListeners the way you are used to for ListViews.
Since you still didn't mark correct any answer, and even if it's an old question, I will try to provide the way I do. I think it is very clean and professional. The functionalities are taken from different blogs (I still have to mention them in the page), merged and methods have been improved for speed and scalability, for all activities that use a RecycleView.
https://github.com/davideas/FlexibleAdapter
At lower class there is SelectableAdapter that provides selection functionalities and it's able to maintain the state after the rotation, you just need to call the onSave/onRestore methods from the activity.
Then the class FlexibleAdapter handles the content with the support of the animation (calling notify only for the position. Note: you still need to set your animation to the RecyclerView when you create it the activity).
Then you need to extend over again this class. Here you add and implements methods as you wish for your own ViewHolder and your Domain/Model class (data holder). Note: I have provided an example which does not compile because you need to change the classes with the ones you have in your project.
I think that, it's the ViewHolder that should keep the listeners of the clicks and that it should be done at the creation and not in the Binding method (that is called at each invalidate from notify...() ).
Also note that this adapter handles the basic clicks: single and long clicks, if you need a double tap you need to use the way Jabob Tabak does in its answer.
I still have to improve it, so keep an eye on it. I also want to add some new functionalities like the Undo.
Here you get a simple Adapter class which can perform onItemClick event on each list row for the recyclerview.

Categories

Resources