Accessing parent fragments methods from recyclerview's adapter - android

Hi guys I'm trying to access few methods and variables of fragment(containing a recycler view) from the recycler views adapter class.. Simplest way is to pass in the fragment reference along with the adapter which creating it. But I dont think passing the full adapter reference which creating the adapter is a good approach.
I'm using RxJava in my project and tried a lot of things with PublishSubject like creating a Subject in adapter, calling its onNext which an event is performed and subscribe to that subject in the fragment but it didnt work out..
So any good approach will be highly appreciated.
TIA...

I'd suggest to introduce EventBus in your app - pretty elegant way of communication between different components of the app.
Then it'd look like:
Fragment:
public class MyFragment extends Fragment {
private EventBus eventBus = EventBus.getDefault();
RecyclerViewAdapter viewAdapter;
#Override
public void onAttach(Context context) {
super.onAttach(context);
eventBus.register(this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_a, container, false);
if (viewAdapter == null) {
viewAdapter = new RecyclerViewAdapter(eventBus);
}
RecyclerView recyclerView = (RecyclerView)rootView.findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(viewAdapter);
return rootView;
}
#SuppressWarnings("unused") // invoked by EventBus
public void onEventMainThread(final DataRefreshedEvent event) {
// Do something!
}
#Override
public void onDetach() {
eventBus.unregister(this);
super.onDetach();
}
}
Adapter:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
EventBus eventBus;
public RecyclerViewAdapter(EventBus eventBus) {
this.eventBus = eventBus;
}
void sentSomethingToFragment() {
eventBus.post(new DataRefreshedEvent());
}
.....
}
Event: public final class DataRefreshedEvent {}
And just a note - with Dagger, it'd look even better.
I hope, it helps

My suggestion is go with interface approach.
1. Create one interface.
2. Fragment should implement that interface.
3. Pass that interface reference to the adapter.
4. Call the interface method from adapter
So that way you can communicate between fragment and adapter.

Related

LiveData not updating when objects in database change

I have following fragment:
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_favourite_books, container, false);
...
getActivity().getFavouriteBooks().observe(this, books -> booksAdapter.setBooks(books));
return view;
}
My Activity:
public LiveData<List<Book>> getFavouriteBooks() {
return viewModel.getFavouriteBooks();
}
My ViewModel (books are fetched from database):
LiveData<List<Book>> favouriteBooks;
public MainViewModel(#NonNull Application application) {
super(application);
favouriteBooks = booksRepository.getFavouriteBooks();
}
public LiveData<List<Book>> getFavouriteBooks() {
return favouriteBooks;
}
Problem:
When I change favourite flag outside this functionality (e.g. in another fragment), favouriteBooks in adapter are not refreshed. This fragment I have in TabLayout, and favouriteBooks are refreshed only when I click on another tab one more time. Any idea how to solve this? It is very simple code and I believe LiveData should support this updating outside itself, I believe this is for what LiveData has been created in the first place. Thanks for your help.
EDIT: Moving observer from onCreateView to onViewCreated does not make any difference.
I found the solution. In setBooks method of Adapter, I need to call notifyDataSetChanged();. With that, list is refreshed properly everytime the collection changes.
public void setBooks(List<Book> books) {
this.books = books;
notifyDataSetChanged();
}

Anonymous Listener in RecyclerView Adapter

Edited:
I want know about creating Anonymous Listener in bindViewHolder method cause any performance problem or not for large data set.
Suppose i have a RecyclerView Adapter. And in bindViewHolder method if i set all my listeners Anonymously does this cause any performance problem? Because when user scrolls the RecyclerView it will create lots of Anonymous listeners and set them to the views.
Example:
view.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
}
});
Or i can implements OnClickListener in my ViewHolder class and just add to views.Like
view.setOnClickListener(this);
Here lots of Anonymous Listeners are not created. Does this perform better from previous in performance calculation for large dataset?
Thanks in advance.
RecyclerView will only display few items, ViewHolder will only be created for items that are visible, so even if you have 1000s of items in your adapter, only small fraction of ViewHolders are created.
But you will have to be careful with addListener methods, for most setListener methods, you will be setting same listener again and again when item is recycled, which does not take less then few milliseconds as it only keeps reference of the listener implementation.
But with addListener, you will have to remove old listener before adding new one.
Example of setListener is setClickListener and example of addListener is addTextWatcher
//.. part of adapter
private TextWatcher textWatcher;
public void bindViewHolder(DataViewHolder holder, int index){
// no performance issue
holder.button.setClickListener( .... );
// wrong, this is added everytime
holder.editText.addTextWatcher( .... );
// this is safe...
if(textWatcher != null)
holder.editText.removeTextWatcher(textWatcher);
textWatcher = new TextWatcher(){
// ... implementation
};
holder.editText.addTextWatcher(textWatcher);
}
Basically, you set a OnClickListener in every item of your RecyclerView and "connect" it to your Activity or Fragment. This "connection" is important, so you can have your onItemClick method inside your Activity or Fragment and access the members there.
A minimal implementation would look like this (in a Fragment, but you can also use an Activity):
public class YourFragment extends Fragment implements RecyclerViewAdapter.ItemClickListener {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_your, container, false);
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recyclerview);
RecyclerViewAdapter recyclerViewAdapter = new RecyclerViewAdapter(activity);
recyclerViewAdapter.setClickListener(this);
recyclerView.setAdapter(recyclerViewAdapter);
return view;
}
#Override
public void onItemClick(View view, int position) {
// do something here
}
}
And the Adapter class
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private ItemClickListener itemClickListener;
void setClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
interface ItemClickListener {
void onItemClick(View view, int position);
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
// TextView is an example
final TextView textView;
ViewHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.text);
textView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
if (itemClickListener != null) {
itemClickListener.onItemClick(view, getAdapterPosition());
}
}
}
}
I'm pretty sure the compiler just creates a no-name concrete version of your anonymous class under the hood. That is nearly identical to implementing the interface and providing this as a concrete listener. Realistically, you shouldn't have a performance problem with either.
Just keep in mind that of the fact that an anonymous class holds a reference to the outer class. That might create memory leaks (example: if the outer class is an activity) or just make it so that garbage collection happens all at once instead of small pieces over time. See Implicit and Synthetic Parameters in the oracle documentation for more details on that.

Android-Realm list with strict Model/View separation

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);
}
}
}

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.

Refreshing data display on a fragment

I have two fragments on a view pager.
I once had to move data from fragment B to A and refresh the data displayed on A and I did it with getItemPosition.
For some reason, the same method doesn't work when I try to reset all data..
In my adapter i have :
public void refresh()
{
notifyDataSetChanged();
}
#Override
public int getItemPosition( Object obj )
{
return POSITION_NONE;
}
in fragment where I click 'reset' :
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
notTriedPasswordsList = PagerActivity.mainList;
.....
....
resetButton.setOnClickListener( new View.OnClickListener() {
#Override
public void onClick( View v )
{
PagerActivity.resetPasswords();
PagerActivity.viewPagerAdapter.refresh();
}});
viewPager activity hosting both fragments:
public static void resetPasswords()
{
mainList.addAll( 0, historyList );
historyList.clear();
PagerActivity.viewPagerAdapter.refresh();
}
Main fragment where the pass is displayed :
#Override
public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState )
{
View view = inflater.inflate(R.layout.fragment_main, container, false);
.....
nextCodeDisplay = ( TextView ) view.findViewById( R.id.passwordDisplayTextView );
nextCodeDisplay.setText( notTriedPasswordsList.get( 0 ).getPasswordString() );
....
nextButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v)
{
notTriedPasswordsList.remove( 0 );
if( notTriedPasswordsList.size() > 0 && !(notTriedPasswordsList.get( 0 ).getTried()) )
{
nextCodeDisplay.setText( notTriedPasswordsList.get( 0 ).getPasswordString() );
}
}
PagerActivity is treated like a static class, and you can only access static methods and member data and objects in this way. About code:
notTriedPasswordsList = PagerActivity.mainList;
Note: So now PagerActivity can access static mainList object, or notTriedPasswordsList (sharing the same memory). But this is the only object you can access since your code references static methods.
On code PagerActivity.viewPagerAdapter.refresh(), I am not clear on what data this refreshes since I don't see the enough code, again refresh() must be a static method. With code notifyDataSetChanged(), there must be a direct link between viewPagerAdapter and the data object, probably an ArrayList. Certainly I don't see any direct relation between the two.
Perhaps you want code like:
viewPagerAdapter pagerAdapter = new viewPagerAdapter();
This way you can have the relationship between the adapter and possibly an ArrayList object. The benefit of creating an instance with new is that it saves data and the state inside the class in the form of an object, in my sample that is pagerAdapter.
I could not suggest specific set of codes for now since I don't see sufficient amount of it for me to fix. Perhaps you can fix code first and then we all can contribute.
Your call to PagerActivity.viewPagerAdapter.refresh(); won't cause your fragment to be redrawn. Instead you should access your fragment directly and create a custom refreshUI() method in it.
public void refreshUI(){
nextCodeDisplay.setText( notTriedPasswordsList.get( 0 ).getPasswordString() );
}
I suggest to change your approach. I've uploaded a simple project to my dropbox public folder. Here you can find a reference implementation of how two fragments managed by a ViewPager can share information. The first Fragment - Fragment#1 - simply displays a String that is generated by Fragment#2. Fragment#2 has a button that, when clicked, sends a random String to Fragment#1 through the Activity. No need to refresh viewpager, no need of static methods, simple and working. I guess you can adapt this example to your needs.
As you said that you want to refresh your data, personally i would like to suggest to use swipe refresh layout. It will be very useful for this purpose and stylish as well. Following is the code.
Swipe_Refresh_layout.xml
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swipe_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView android:id="#+id/listview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
/>
</android.support.v4.widget.SwipeRefreshLayout>
And following is the activity i am using for this layout.
public class LatestNewsFragment extends Fragment implements OnRefreshListener ,OnScrollListener{
SwipeRefreshLayout swipeLayout;
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState)
{
=
View rootView = inflater.inflate(R.layout.Swipe_Refresh_layout, container, false);
swipeLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_container);
swipeLayout.setOnRefreshListener(this);
swipeLayout.setColorScheme(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
return rootView;
}
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
#Override public void run() {
swipeLayout.setRefreshing(false);
additemstatus();
}
}, 5000);
}
Now in overridden refresh method you can refresh or load your data. I hope this will be very helpful for your support.

Categories

Resources