Android MVP - how to work with logic dependent on Android classes? - android

I'm working on an existing app, migrating it towards the MVP architecture. In one of the RecyclerView.Adapters, there's quite a lot of logic in regards to how a header View should look like. According to the MVP architecture, such logic should be moved to the Presenter and the presenter should use the View to display things. Also, I've read quite a lot about how if you have Android classes in your presenter - that basically indicates that something went wrong.
So my thoughts so far are to create a Presenter for the ViewHolder when the ViewHolder (also the View from the MVP standpoint) is created and to move my logic there. Howeverm the problem I'm having is that the logic is based on Android classes such as Paint, Rect, Spannable and so on.
So how do I solve this problem in the cleanest way possible? Do I move all the logic to the View? Or should I keep the logic in my Presenter, but move only the calculations related to the specific Android objects to the View? As an example that would look something like that:
in presenter:
double textLines = mView.getTextLines(text, 0 , text.length());
in view:
Paint paint = mTextView.getPaint();
Rect bounds = new Rect();
paint.getTextBounds(text, start, end, bounds);
double parentWidth = parentView.getWidth() - parentView.getPaddingLeft() - parentView
.getPaddingRight();
return Math.ceil((double) bounds.width() / parentWidth);
Or is there another better way?

Every piece of code that depends on Android classes should be kept on the view implementation (Activiy, Fragment, ViewHolder etc) - the view should be as dumb as possible. I think you don't need a presenter for the ViewHolder, as it usually should not have logic.
If you have unit tests (and you should), the process becomes easier to grasp because you will be forced to move Android dependent classes to somewhere else in order to be able to test it.
For example, a real case scenario I have with RecyclerView is something like this: one view interface for the Activity, one view interface for the ViewHolder, one presenter for the Adapter and one presenter for the Activity. The following is a partial, non compilable example just for demonstration, where you can see the separation of concerns between the layers.
So, in your case, the approach is correct: the view interface just have a method that returns a string "from somewhere", while in the real android view you use the actual classes and methods from the Android API to retrieve the requested data.
My example is below.
// Definitions for the event adapter stuff
public interface EventAdapterContract {
interface View {
void onFetchEventsFailed(String reason);
void notifyDataSetChanged();
}
interface Presenter {
void getItemViewType(int position);
void getItemCount();
void bindEventRow(int position, ViewHolder holder, int backgroundColor);
}
interface ViewHolder {
void setTitle(String value);
void setHour(String value);
void setBackgroundColor(int color);
}
}
// Adapter presenter implementation. Again, partial code for simplicity
public class EventAdapterPresenter implements EventAdapterContract.Presenter {
private final EventAdapterContract.View view;
private final List<Event> events;
static int VIEW_FINISHED = 0;
static int VIEW_pending = 0;
public EventAdapterPresenter(EventAdapterContract.View view, EventAPI api) {
this.view = view;
this.events = new ArrayList<>();
// EventAPI is an interface (Retrofit in this case), which can
// also be tested with plain junit
}
#Override
public int getItemCount() {
return events.size();
}
#Override
public int getItemViewType(int position) {
Event item = events.get(position);
return item.isFinished() ? VIEW_FINISHED : VIEW_PENDING;
}
#Override
public void bindEventRow(int position, EventAdapterContract.ViewHolder holder, int backgroundColor) {
Event event = events.get(position);
holder.setTitle(event.getTitle());
holder.setHour(String.format("Event time: %s", event.getStartTime()));
holder.setBackgroundColor(backgroundColor);
}
}
// The adapter itself. Please mind that this is a partial
// piece of code just for the sake of demonstration,
// I ommited several parts to keep it simple
public class EventAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements EventAdapterContract.View {
private EventAdapterContract.Presenter presenter;
public class EventAdapter(Contex context) {
// Regular adapter contructor
// ...
presenter = new EventAdapterPresenter(this, someImplOfEventApi);
}
#Override
public void onFetchEventsFailed(String reason) {
// Show an AlertDialog
}
#Override
public int getItemViewType(int position) {
return presenter.getItemViewType(position);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int backgroundColor = ContextCompat.getColor(context, R.color.colorEventPending);
presenter.bindEventRow(position, holder, backgroundColor);
}
}
public class EventRowViewHolder
extends RecyclerView.ViewHolder
implements View.OnClickListener, EventAdapterContract.ViewHolder.Row {
private TextView title;
private TextView hour;
public EventRowViewHolder(View view) {
super(view);
// init widgets etc...
view.setOnClickListener(this);
}
#Override
public void setTitle(String value) {
title.setText(value);
}
#Override
public void setHour(String value) {
hour.setText(value);
}
#Override
public void setBackgroundColor(int color) {
this.itemView.setBackgroundColor(color);
}
#Override
public void onClick(View view) {
EventBus.getDefault().post(new OpenEventDetailsMessage(orderId));
}
}

Related

Fragment in a Adapter of RecyclerView JAVA

I have a fragment Users which has 3 other fragments in it (tabs). For one tab ( called Friends2Fragment ) I made a recycler View and made an adapter for it. In each item of RecyclerView I have a button "Add friend" and I want to call it from Friends2Fragment, not to call it from the adapter because I can't use Firestore Database properly.
RecyclerViewInterface:
public interface RecyclerViewInterface {
void onItemClick(int position, String button_pressed);
}
Friends2Fragment.java :
public void onStart(){
super.onStart();
recyclerView = (RecyclerView) v.findViewById(R.id.recycler);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
friendslist = new ArrayList<>();
myAdapter = new MyAdapter(friendslist,v.getContext());
recyclerView.setAdapter(myAdapter);
------ Firestore operations ------
}
#Override
public void onItemClick(int position, String button_pressed) {
switch ( button_pressed ){
case "ADD_FRIEND":
Log.d(TAG, "item clicked: " + friendslist.get(position).username);
}
}
MyAdapter.java :
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.myViewHolder> {
Context context;
public ArrayList<User> userArrayList;
public MyAdapter(ArrayList<User> userArrayList, Context context) {
this.userArrayList = userArrayList;
this.context = context;
}
public Context getContext() {
return context;
}
public ArrayList<User> getUserArrayList() {
return userArrayList;
}
#NonNull
#Override
public MyAdapter.myViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
MyAdapter.myViewHolder myViewHolder = new MyAdapter.myViewHolder(v);
myViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((Friends2Fragment)context).onItemClick(myViewHolder.getAdapterPosition(),"ADD_FRIEND");
}
});
return myViewHolder;
}
#Override
public void onBindViewHolder(#NonNull MyAdapter.myViewHolder holder, int position) {
User user = userArrayList.get(position);
holder.usernamerecycle.setText(user.username);
}
#Override
public int getItemCount() {
return userArrayList.size();
}
public void filterList(List<User> filteredList){
userArrayList = (ArrayList<User>) filteredList;
notifyDataSetChanged();
}
public class myViewHolder extends RecyclerView.ViewHolder{
TextView usernamerecycle;
Button addbutton;
View rootview;
public myViewHolder(#NonNull View itemView) {
super(itemView);
rootview = itemView;
usernamerecycle = itemView.findViewById(R.id.usernamerecycler);
addbutton = itemView.findViewById(R.id.addfriendbutton);
}
}
}
The problem is at this line : ((Friends2Fragment)context).onItemClick(myViewHolder.getAdapterPosition(),"ADD_FRIEND"); in onCreateViewHolder method in MyAdapter.
I have this error : Inconvertible types; cannot cast 'android.content.Context' to 'com.example.birthday.Fragments.Friends2Fragment'
Please help me ..
A Fragment isn't a Context (that's not one of its supertypes) so that cast is impossible, that's why you're getting the error.
I think you should organise it like this: your Adapter holds a bunch of User objects, right? It displays those, and you have a click listener on each ViewHolder that knows which index in the User list it's currently displaying, and it wants to inform some listener when it's clicked. That index is an internal detail really, it would make more sense to look up the actual User, and provide that to the listener.
The simplest way is to just provide your fragment as a listener. First store it in your adapter:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.myViewHolder> {
// store a reference to your fragment
private Friends2Fragment listener;
// add a function to provide that fragment
public void setListener(Friends2Fragment: listener) {
this.listener = listener
}
...
public MyAdapter.myViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
...
myViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (listener != null) {
// look up the actual user
User user = userArrayList.get(myViewHolder.getAdapterPosition());
// call a function on your fragment
listener.onItemClick(user, "ADD_FRIEND");
}
}
});
}
Then add the callback function your adapter uses, and also set your fragment on the adapter as a listener:
// Friends2Fragment
// You should REALLY be doing this in onViewCreated or something, so this setup happens once.
// You're losing all your state by creating a new adapter whenever the user returns to the app
public void onStart(){
...
myAdapter = new MyAdapter(friendslist,v.getContext());
// set the fragment as the listener
myAdapter.setListener(this);
recyclerView.setAdapter(myAdapter);
}
// now add the function the adapter calls
private void onItemClick(User user, String someString) {
// handle the clicked user
}
A better way is to create an interface with all the events that need to be handled, and make your Fragment implement those. It breaks the hard association with the Fragment since you could pass any object that implements those functions, and it's also clearer because the interface kinda documents all the data the adapter produces, and that a listener needs to be able to handle. Something like this:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.myViewHolder> {
// the listener is now something that implements the Callbacks interface
private Callbacks listener;
...
// nesting it inside MyAdapter makes the path MyAdapter.Callbacks, which makes it clear
// exactly what it is and what it relates to, and kinda gives the Adapter "ownership"
interface Callbacks {
void addFriend(User user)
}
And then you just make the Fragment implement that interface
public class Friends2Fragment() extends Fragment implements MyAdapter.Callbacks {
...
// implement all the callbacks you need to handle
override public void addFriend(User user) {
// do the thing
}
// set it in the same way, since this Fragment implements MyAdapter.Callbacks
myAdapter.setListener(this);
Which is a bit neater and cleaner, I think - but slightly more work. Also if you notice, I renamed the callback function from the generic handleItemClick to the more specific addFriend - so instead of having to pass a String saying what kind of click it is, you just have a function for each event you want to handle, and you can name them appropriately

Update TextView in selected RecyclerView rows

I have a RecyclerView inside a fragment where each line has an adapter which inflates a layout which looks as follows:
I want to access to the value of the EditText (in the following code numberET) of each row and pick the value if EditText is not empty.
How can I cycle on each element of the RecyclerView (I think inside the adapter) to have this behaviour? How can I access the EditText for each element to retrieve the value and use them inside the fragment?
Adapter:
`
public class UserFBEditTextAdapter <T extends UserFBEditTextAdapter.ViewHolder> extends UserFBAdapter<UserFBEditTextAdapter.ViewHolder>{
public UserFBEditTextAdapter(List<UserFB> users,int layoutId, Context context) {
super(users, layoutId, context);
}
#Override
public UserFBEditTextAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
return new UserFBEditTextAdapter.ViewHolder(view);
}
#Override
public void onBindViewHolder(UserFBAdapter.ViewHolder holder, int position) {
holder.userFB = users.get(position);
holder.usernameTV.setText(holder.userFB.getName());
}
public class ViewHolder extends UserFBAdapter.ViewHolder {
protected EditText numberET;
public ViewHolder(View itemView) {
super(itemView);
numberET = (EditText) itemView.findViewById(R.id.number_et);
}
}
}`
Fragment:
public class ExpenseCustomFragment extends Fragment {
private OnFragmentInteractionListener mListener;
private UserFBAdapter adapter;
private RecyclerView userCustomList;
public ExpenseCustomFragment() {
// Required empty public constructor
}
public static ExpenseCustomFragment newInstance() {
ExpenseCustomFragment fragment = new ExpenseCustomFragment();
return fragment;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_expense_custom, container, false);
userCustomList = (RecyclerView) view.findViewById(R.id.amountlist_rv);
userCustomList.setLayoutManager(new LinearLayoutManager(getContext()));
NewExpenseDescriptionActivity activity = (NewExpenseDescriptionActivity) getActivity();
adapter = new UserFBEditTextAdapter(activity.getUsersGroup(), R.layout.listitem_expensecustom, getContext());
userCustomList.setAdapter(adapter);
return view;
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}
You have to retain that data in some map-based data structure, and then, whenever those values are needed, iterate over that data structure.
You cannot rely on saving that data in a ViewHolder, because ViewHolders are being reused as soon as you perform scrolling. If you currently do not save the data that is filled in EditText, then you'll lose that data if you have many items and perform scrolling (i.e. screen fits 10 items, but your adapter is 20 items, as soon as you scroll to 15th item, the EditText value for the first item will be lost).
private Map<Integer, String> map = new ArrayMap<>(adapterSize);
...
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
String text = map.get(holder.getAdapterPosition());
// maybe we haven't yet saved text for this position
holder.editText.setText(text != null ? text : "");
// updated value in map as soon as the `EditText` in this position changes
holder.editText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
map.put(holder.getAdapterPosition(), s.toString());
}
#Override
public void afterTextChanged(Editable s) {
}
});
}
Now you'll have access to all EditText values in your RecyclerView. The only change that you can consider is updating map after user stops typing. Currently if user types "123456789" the map will be updated 9 times, whereas we need only once. An easy solution to this can be using RxJava's debounce operator combined with RxBinding library. This maybe sounds complicated, but you can see how plain it is in this answer.
This will work. But after you perform scrolling up and forth, soon you'll find out that some mess is going on there. That's because each time onBindViewHolder() gets called a new TextWatcher is being added to the EditText that already has a TextWatcher attached to it. Thus, you also have to take care of removing the TextWatcher after your ViewHolder is being recycled.
But there is no an API to remove all TextWatcher of the EditText. You can use a custom EditText implementation shown in this answer which will clear all TextWatcher attached to this EditText:
#Override
public void onViewRecycled(MyViewHolder holder) {
holder.editText.clearTextChangeListeners();
super.onViewRecycled(holder);
}

How to implement Infinite Scrolling with RecyclerView?

I have a recycler and inside of it there are cardviews where I fetch information from a REST service, I'm trying to implement an endless scroll, It's supposed that user will see 10 cardviews every time he scrolls down until there are no more cardviews to show, How can I achieve that?
I've seen a few examples but none of them really helped me about how to do it. I don't even know what I need to put in adapter.class or in my Fragment.class because I don't understand how to implement that, it would be great if someone could tell me the correct way to implement the infinite scroll in my code...
Thanks in advance.
MainAdapter.class
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> implements View.OnClickListener
{
private ArrayList<Business> businessList;
private Activity activity;
private int layoutMolde,idb;
public MainAdapter(Activity activity, ArrayList<Business> list, int layout)
{
this.activity = activity;
this.businessList = list;
layoutMolde = layout;
}
#Override
public MainAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_row, parent, false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.mTitle.setText(businessList.get(position).getBusiness_name());
holder.number_rating.setText(businessList.get(position).getRating().toString());
Glide.with(activity).load(businessList.get(position).getLogo_url_string()).into(holder.mImg);
}
#Override
public int getItemCount() {
return businessList.size();
}
#Override
public void onClick(View v)
{
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView mTitle;
public ImageView mImg;
public ImageView logo;
public RatingBar main_rating;
public TextView number_rating;
public ViewHolder( View itemView)
{
super(itemView);
mTitle = (TextView) itemView.findViewById(R.id.nom_business_main);
number_rating = (TextView) itemView.findViewById(R.id.number_rating);
mImg = (ImageView) itemView.findViewById(R.id.img_main);
main_rating=(RatingBar) itemView.findViewById(R.id.rating_main);
main_rating.setRating((float)1);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v)
{
Intent in = new Intent(v.getContext(), BusinessPremium.class);
int position = getAdapterPosition();
idb = businessList.get(position).getId();
in.putExtra("no", idb);
v.getContext().startActivity(in);
}
});
}
}
}
FeedsFragment.class
public class FeedsFragment extends Fragment
{
private ArrayList<Business> arrayBusiness,arrayBasics;
private Gson gson;
private static final Type BUSINESS_TYPE = new TypeToken<ArrayList<Business>>() {}.getType();
private RecyclerView.LayoutManager mLayoutManager;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View android = inflater.inflate(R.layout.fragment_feeds, container, false);
if (!internetConnectionCheck(FeedsFragment.this.getActivity()))
{
Toast.makeText(getApplicationContext(), "Error de Conexión", Toast.LENGTH_LONG).show();
}
new RequestBase(getActivity()) {
#Override
public JsonObject onHttpOk(JsonObject response) throws JSONException {
JsonObject objeto, pagination_details = null, details, premium_img;
JsonArray data;
if (getActivity() == null)
return response;
if (response.get("pagination") == null)
{
objeto = response;
} else {
objeto = response;
pagination_details = response.get("pagination").getAsJsonObject();
data = objeto.get("data").getAsJsonArray();
gson = new Gson();
arrayBusiness = gson.fromJson(data, BUSINESS_TYPE);
Log.d("size", String.valueOf(arrayBusiness.size()));
FeedsFragment.this.getActivity().runOnUiThread(new Runnable()
{
#Override
public void run()
{
RecyclerView recycler = (RecyclerView) FeedsFragment.this.getActivity().findViewById(R.id.recycler_main);
MainAdapter adapter = new MainAdapter(getActivity(), arrayBusiness, R.layout.main_row);
recycler.setNestedScrollingEnabled(false);
mLayoutManager = new GridLayoutManager(FeedsFragment.this.getActivity(), 2);
recycler.setLayoutManager(mLayoutManager);
recycler.setAdapter(adapter);
GifTextView loading = (GifTextView)FeedsFragment.this.getActivity().findViewById(R.id.loading);
TextView loadingText = (TextView)FeedsFragment.this.getActivity().findViewById(R.id.loadingText);
loading.setVisibility(View.GONE);
loadingText.setVisibility(View.GONE);
}
});
}
if (pagination_details.isJsonNull()) {
Log.d("Paginacion", pagination_details.toString());
}
return objeto;
}
#Override
public void onHttpCreate(JsonObject response) throws JSONException
{
}
#Override
public void onHttpUnprocessableEntity(JsonObject response) throws JSONException
{
this.cancel(true);
final String error = response.get("errors").toString();
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
Toast.makeText(getActivity().getApplicationContext(), error, Toast.LENGTH_LONG).show();
}
});
}
}.execute("businesses/premiums", "GET");
return android;
}
}
you can refresh using SwipeRefreshLayout in android to refresh and in the on refresh override method call your api
note:put your API call request in a method and call that method inyour onRefresh method of SwipeRefreshLayout
When writing RecyclerView.Adapter, you anyway need to provide the getItemCount method that returns the correct number of items (may be large). RecyclerView will call on its own initiative the onBindViewHolder(holder, position) method of this adapter. All you need is to provide functionality of retrieving data, relevant to this position. There is no difference at all, if your list is smaller than screen, slightly larger than screen or Integer.MAX_VALUE size. RecyclerView will take care not to fetch/allocate too much extra items.
You do not need to implement scroll listeners or otherwise explicitly handle the scrolling.
The only tricky part is that you may need to take a long action like server call to get some items. Then just return uninitialized holder (empty view) on the first invocation and start fetching the needed row in the background thread. When you have it, call notifyDataSetChanged or notifyItemRangeChanged, and RecyclerView will take care to update itself.
For performance reasons I would strongly recommend to update content in chunks of the fixed size rather than sending individual server request per every row displayed. For some public servers like Google Books this is clearly a requirement, as they have quota limits per request.
If you need to view the full source code on how this possibly could be implemented, there is an open source project here in GitHub.
Make a static boolean variable named "ready" and initialize it to false.
Add the if ready condition in the onLoadMore method as below.
public boolean onLoadMore(int page, int totalItemsCount) {
if (ready) {
//load more from API
}
return false;
}
set ready to true in onBindViewHolder when the position of item is last.
Here is a way that a colleague of mine introduced. we worked in it together and i implemented it successfully with no issues. I wanted to give back to anyone having this issue.
in your adapter you need to set the count to be infinite size and then when you want the position of an item you should use val loopPos = position % dataSource.size anytime you need the position. lets take a look how this can be done in a recyclerView adapter but could also be applied to FragmentStatePagerAdapter.
class InfiniteLoopingHorizontalRecyclerViewAdapter(var dataSource: ArrayList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflatedView: View = LayoutInflater.from(parent.context)
.inflate(R.layout.your_finite_layout, parent, false)
return ItemHolder(inflatedView)
}
override fun getItemCount(): Int {
return Integer.MAX_VALUE //***** this should be high enough - wink wink ******
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
//****** this is critical here when you need the position use the loopPos ****/
val loopPos = position % dataSource.size
(holder as? ItemHolder)?.bind(dataSource[loopPos], loopPos)
}
inner class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(myString: String, position: Int) = with(itemView) {
myTextView.setText(myString)
}
}
}
how it works:
lets say your dataSource size is 50 but your position is at 51 that means the following: 51%50 . which gives you position 1. and lets say again your position is 57 and again your dataSource size is still 50. that means your position is 7. so to be clear, anytime you need a infinite affect you can use the modules of the position and the dataSource size.
ps:
lets go crazy and say we scrolled to position 11323232323214 then that means 11323232323214%50 = 14 so its position 14 in your datasource that will be used. you can then polish off the affect with wrapping your recyclerview in a SnapHelper class
You can add a scrollListener to your recyclerview.
Check a similar answer here
And the main SO post here
Where, the scrollListener will check where exactly are you in the recyclerview and based on some logic (which you can flexibly write) make a second call!

How to communicate between View and Adapter in MVP style

I'm studying MVP style with Android Architecture Blueprints
In this sample, Adapter(RecyclerViewAdapter etc) belongs to View.
Fragment means View in MVP and Adapter placed in the View as inner class.
In this case, a communication between View and Adapter was made by using callback interface.
public TasksAdapter(List<Task> tasks, TaskItemListener itemListener) {
setList(tasks);
mItemListener = itemListener;
}
I often though callback communication is a little confused.
Many reactions in this QA show this matter.
Why doesn't RecyclerView have onItemClickListener()? And how RecyclerView is different from Listview?
public class ReactiveAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
String[] mDataset = { "Data", "In", "Adapter" };
private final PublishSubject<String> onClickSubject = PublishSubject.create();
#Override
public void onBindViewHolder(final ViewHolder holder, int position) {
final String element = mDataset[position];
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onClickSubject.onNext(element);
}
});
}
public Observable<String> getPositionClicks(){
return onClickSubject.asObservable();
}
}
There are some ways on this.
Callbacks (callback, listener, and observer)
Observable with RxJava
PublishSubject with RxJava
Event Bus
Pass View or Presenter to Adapter's constructor
Which way is correct ? How should I do this?

Large number of items in RecyclerView.Adapter - Memory Issue

Overview: I'm having a chat application. Till now, I was using CursorAdapter with a Listview to load my chat items in the list. But now, I'm planning to refactor the code to use RecyclerView with RecyclerView.Adapter and a "Load More" functionality like whatsapp.
Issue: Memory consumption. With CursorAdapter, items not in viewable area were getting Garbage Collected, but now since I'm using an ArrayList of my CustomModal, once you load all the items in the list (by clicking on the "Load More" button) I'm seeing high memory consumption in the memory logs (No Garbage Collection).
My guess is now, I'm loading all the items in an ArrayList and that is causing the issue. Is that it?
Is there a way to avoid the issue or optimize the problem?
EDIT:
Can't post the complete code here, but here is a snippet of the kind of Adapter that I've implemented:
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MyViewHolder> {
private ArrayList<MyModal> mMyModals;
public MessageAdapter(ArrayList<MyModal> mMyModals) {
this.mMyModals = mMyModals;
//... Some fields initialization here
}
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals = myModals;
//... Some fields initialization here
notifyDataSetChanged();
}
public void toggleLoadMore(boolean isLoadMoreEnabled){
if(isLoadMoreEnabled){
//..Checks if load more is already enabled or not
//..If not then enables it by adding an item at 0th poition of MyModal list
//..Then notifyDataSetChanged()
}else{
//..Checks if load more is already disabled or not
//..If not then disables it by removing an item at 0th poition of MyModal list
//..Then notifyDataSetChanged()
}
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder messageViewHolder = null;
View itemLayoutView = null;
MyModal.MessageType messageType = MyModal.MessageType.getMessageTypeFromValue(viewType);
switch (messageType){
case MESSAGE_TYPE1:
itemLayoutView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout1, null);
messageViewHolder = new Type1ViewHolder(itemLayoutView);
break;
case MESSAGE_TYPE2:
itemLayoutView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout2, null);
messageViewHolder = new Type2ViewHolder(itemLayoutView);
break;
}
return messageViewHolder;
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final MyModal myModal = mMyModals.get(position);
MyModal.MessageType messageType = myModal.getMessageType();
holder.initialize(myModal);
}
#Override
public int getItemCount() {
return (mMyModals != null)?mMyModals.size():0;
}
#Override
public int getItemViewType(int position) {
return mMyModals.get(position).getMessageType().getValue();
}
public abstract class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemLayoutView) {
super(itemLayoutView);
}
public abstract void initialize(MyModal myModal);
}
class Type1ViewHolder extends MyViewHolder {
//...Variables
public Type1ViewHolder(View itemLayoutView) {
super(itemLayoutView);
//...variables initialization here
}
#Override
public void initialize(MyModal myModal) {
//...Setting values in view using myModal
}
}
class Type2ViewHolder extends MyViewHolder {
//...Variables
public TextViewHolder(View itemLayoutView) {
super(itemLayoutView);
//...variables initialization here
}
#Override
public void initialize(MyModal myModal) {
//...Setting values in view using myModal
}
}
}
First of all :
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals = myModals;
//... Some fields initialization here
notifyDataSetChanged();
}
Here you are creating a new arraylist and assigning it to your mMyModals. This means there are 2 arraylists at this point and they take up twice the amount of space than required. GC doesnt work the way you expect it to. Since the arraylist is initialized in your activity it will persist as long as the arraylist persists and so will the initial arraylist.
Instead of creating a new arraylist in your activity and passing it to changeList. Just clear your old arraylist and pass that.And also in adapter changeList method you can do the below
public void changeList(ArrayList<MyModal> myModals, boolean isLoadMoreEnabled){
this.mMyModals.clear();
this.mMyModels.addAll(myModels);
//... Some fields initialization here
notifyDataSetChanged();
}
Please let me know if i am not clear. Also show your activity code if this does not work.
Instead of replacing the whole ArrayList and calling notifyDataSetChanged, try adding the items to the ArrayList and then call notifyItemRangeInserted(int positionStart, int itemCount), maybe that could work. Also, you dont have to replace the Adapter's ArrayList. Your Activity/Fragment probably has the same ArrayList, just editing this list in your Activity/Fragment and then calling notifyItemRangeInserted(int positionStart, int itemCount) should do the trick. Also, instead of retrieving all the messages, you could also try to only get the next X amount of messages, so you wont retrieve the messages you already retrieved before (if you didn't do that already).

Categories

Resources