I'm updating my app to use Firebase's Firestore database. I'm struggling to get the app to show the data that's been retrieved from the database. The data is retrieved ok, but doesn't show up. By setting breakpoints, I've established that the ViewHolder isn't being bound to the adapter at any point.
The data is being shown in a Fragment. The Fragment layout is (I've taken out irrelevant stuff like padding, sizes etc):
<android.support.constraint.ConstraintLayout
android:id="#+id/layout_charts_list"
tools:context="apppath.fragments.ChartListFragment">
<TextView
android:id="#+id/loading_list"
android:text="#string/loading_my_charts" />
<TextView
android:id="#+id/empty_list"
android:text="#string/my_charts_empty"
android:visibility="gone"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/charts_list" />
</android.support.constraint.ConstraintLayout>
The Fragment code itself is:
public abstract class ChartListFragment extends Fragment {
private FirebaseFirestore mDatabaseRef;
private FirestoreRecyclerAdapter<Chart, ChartViewHolder> mAdapter;
private Query mChartsQuery;
private RecyclerView mRecycler;
public ChartListFragment() {}
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View rootView = inflater.inflate(
R.layout.fragment_charts_list, container, false);
mRecycler = rootView.findViewById(R.id.charts_list);
mRecycler.setHasFixedSize(true);
// Set up Layout Manager, and set Recycler View to use it
LinearLayoutManager mManager = new LinearLayoutManager(getActivity());
mManager.setReverseLayout(true);
mManager.setStackFromEnd(true);
mRecycler.setLayoutManager(mManager);
// Connect to the database, and get the appropriate query (as set in the actual fragment)
mDatabaseRef = FirebaseFirestore.getInstance();
mChartsQuery = getQuery(mDatabaseRef);
// Set up Recycler Adapter
FirestoreRecyclerOptions<Chart> recyclerOptions = new FirestoreRecyclerOptions.Builder<Chart>()
.setQuery(mChartsQuery, Chart.class)
.build();
mAdapter = new ChartListAdapter(recyclerOptions);
// Use Recycler Adapter in RecyclerView
mRecycler.setAdapter(mAdapter);
return rootView;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Add listener to charts collection, and deal with any changes by re-showing the list
mChartsQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot queryDocumentSnapshots,
#Nullable FirebaseFirestoreException e) {
if (queryDocumentSnapshots != null && queryDocumentSnapshots.isEmpty()) {
((MainActivity) getActivity()).setPage(1);
mRecycler.setVisibility(View.GONE);
}else {
mRecycler.setVisibility(View.VISIBLE);
}
}
});
}
// HELPER FUNCTIONS
public abstract Query getQuery(FirebaseFirestore databaseReference);
}
ChartListAdapter is as follows:
public class ChartListAdapter
extends FirestoreRecyclerAdapter<Chart, ChartViewHolder> {
public ChartListAdapter(FirestoreRecyclerOptions recyclerOptions) {
super(recyclerOptions);
}
#Override
protected void onBindViewHolder(ChartViewHolder holder, int position, Chart model) {
holder.setChartName(model.getName());
// Bind Chart to ViewHolder
holder.bindToChart(model);
}
#Override
public ChartViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_chart, parent, false);
return new ChartViewHolder(view);
}
}
ChartViewHolder:
public class ChartViewHolder extends RecyclerView.ViewHolder {
private TextView chartNameView;
private String chartKey;
public ChartViewHolder(View itemView) {
super(itemView);
chartNameView = itemView.findViewById(R.id.chart_name);
}
public void setChartName(String chartName) {
chartNameView.setText(chartName);
}
public void bindToChart(Chart chart) {
chartKey = chart.getKey();
chartNameView.setText(chart.getName());
}
public String getChartKey() {
return chartKey;
}
}
The ChartListAdapter constructor is called, but onBindViewHolder and onCreateViewHolder are never called, and ChartViewHolder is never accessed at all. Am I missing a line of code? Or doing this completely wrong? I'm not all that familiar with Adapters and RecyclerViews, so I've found it quite hard to get to grips with putting it all together.
For those who came here from Google, set the lifecycle owner on the options so that start/stop listening is called automatically.
FirestoreRecyclerOptions<Chart> recyclerOptions = FirestoreRecyclerOptions.Builder<Chart>()
.setQuery(mChartsQuery, Chart.class)
.setLifecycleOwner(this)
.build();
You need to add the following, to begin listening for data, call the startListening() method. You may want to call this in your onStart() method. Make sure you have finished any authentication necessary to read the data before calling startListening() or your query will fail:
#Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
more info here:
https://github.com/firebase/FirebaseUI-Android/tree/master/firestore
Related
I have a BottomNavigationBar with 3 fragments. In the first fragment, I try to put SQLite data into a recyclerview. It works fine except for the fact that I need to switch between the Navigation Bar items in order to see the refreshed recyclerview. When I use a handler with postDelayed however, it does show the refreshed recyclerview if I set around 1 sec of delay. 0.2 secs wont work already.
Even though this is still very generic: is there any best practice for this? It seems to me that I need to use AsyncTask which has been -however- deprecated.
Thanks!
Simon
HomeFragment
public class HomeFragment extends Fragment {
private HomeViewModel homeViewModel;
private Context context;
private CardView cardview;
private LinearLayout.LayoutParams layoutparams;
private TextView textview;
private RelativeLayout relativeLayout;
private myDbAdapter helper;
RecyclerView myView;
public View onCreateView(#NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
homeViewModel =
new ViewModelProvider(this).get(HomeViewModel.class);
View root = inflater.inflate(R.layout.fragment_home, container, false);
helper = new myDbAdapter(getContext());
myView = (RecyclerView) root.findViewById(R.id.recyclerview_home);
RecyclerViewAdapter3 adapter = new RecyclerViewAdapter3(new ArrayList<String>(Arrays.asList(helper.classes())));
myView.setHasFixedSize(true);
myView.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
llm.setOrientation(LinearLayoutManager.VERTICAL);
myView.setLayoutManager(llm);
homeViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String s) {
textView.setText(s);
}
});
return root;
}
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
public void refresh(View v){
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
myView = (RecyclerView) v.findViewById(R.id.recyclerview_home);
helper = new myDbAdapter(v.getContext());
ArrayList<String> classes = new ArrayList(Arrays.asList(helper.classes()));
ArrayList<String> subClasses = new ArrayList(Arrays.asList(helper.subClasses()));
RecyclerViewAdapter3 adapter = new RecyclerViewAdapter3(classes);
myView.setHasFixedSize(true);
myView.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(v.getContext());
llm.setOrientation(LinearLayoutManager.VERTICAL);
myView.setLayoutManager(llm);
}
}, 1000); //time in millis
}
}
RecyclerViewAdapter3
public class RecyclerViewAdapter3 extends RecyclerView.Adapter<RecyclerViewAdapter3.MyViewHolder> {
public ArrayList<String> classArrayList;
public ArrayList<String> subClassArrayList;
myDbAdapter helper;
public RecyclerViewAdapter3(ArrayList<String> classArrayList){
this.classArrayList = classArrayList;
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View listItem = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview, parent, false);
return new MyViewHolder(listItem);
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.class.setText(classArrayList.get(position));
holder.delete.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
helper = new myDbAdapter(v.getContext());
helper.delete(classArrayList.get(position));
HomeFragment homeFragment = new HomeFragment();
homeFragment.refresh(v.getRootView());
}
});
holder.selectButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
}});}
#Override
public int getItemCount() {
return classArrayList.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
private TextView class;
private Button selectButton;
private ImageView delete;
public MyViewHolder(View itemView) {
super(itemView);
class = (TextView)itemView.findViewById(R.id.name);
selectButton = (Button) itemView.findViewById(R.id.selectButton);
delete = (ImageView) itemView.findViewById(R.id.delete);
}
}
}
Thanks for posting your code :)
There are a fair few things that can go wrong in your code as it is right now, and I can't really pinpoint what causes it to work when you use postDelay. I'm going to list a few, which you can look into:
From your onClick() inside your ViewHolder
HomeFragment homeFragment = new HomeFragment();
homeFragment.refresh(v.getRootView());
You should really not instantiate your fragments like this. You can instead pass a callback from your fragment to your adapter (eg.: View.OnClickListener)
You keep re-instantiating your adapter and your helper needlessly. You should create your adapter only once, set it as your recycler view adapter, and save it in a member variable.
Proposed solution
I see that you're already using ViewModel, so you're on a great path for a less error-prone screen, so I suggest that you move your db query-ing logic to your view model. If you're using raw SQLite (instead of Room), you can extend AndroidViewModel, so you'll have access to a context right away. And as you do with your homeViewModel.getText(), you should expose the classes array as live data, observe it, and submit the new list to your adapter.
For submitting your list to your adapter I suggest using ListAdapter, this will provide you a submitList method for submitting the list in the fragment, and inside the adapter, you will have a getItem(int position) method, which you can query inside the onBindViewHolder method.
Inside your fragment, it'll look something like this:
ClassAdapter adapter = null;
View onCreateView(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
View root = inflater.inflate(R.layout.fragment_home, container, false);
adapter = new ClassAdapter(
new ClassDeleteCallback() {
#Override
void onClassDeleted(Class class) {
// inside view model we can modify db state, than refresh live data
viewModel.deleteClass(class);
}
},
new ClassSelectedCallback() {
// follows same pattern of above
}
);
RecyclerView rv = root.findViewById(R.id.my_rv);
rv.setAdapter(adapter);
rv.setLayoutManager(new LinearLayoutManager(getContext());
homeViewModel.getClasses().observe(getViewLifecycleOwner(), new Observer<List<Class>>() {
#Override
public void onChanged(#Nullable List<Class> classes) {
adapter.submitList(classes);
}
});
homeViewModel.refreshClasses();
return root;
}
I can highly recommend for you to study this project a bit, because it covers lot of the basics which can lead to a much stabler app: Sunflower sample app
I think you should read a bit more about the architecture components, and then go through some code-labs and stuff, and have another go with this screen starting from square one, because it will be easier than fixing the current state :)
I hope this was helpful, and not too discouraging!
Can someone tell me why the submitList() method can't be accessed in the following code? Similar code is working fine in another test app, although that's in an Activity.
public class MyFFBooksFragment extends Fragment {
private int currentBook;
private RecyclerView mRecyclerView;
private RecyclerView.LayoutManager mLayoutManager;
private GamebookViewModel gamebookViewModel;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable
ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.myffbooks_fragment, container, false);
mLayoutManager = new LinearLayoutManager(getActivity());
final ArrayList<BookItem> bookList = new ArrayList<>();
final BookItemAdapter maAdapter = new BookItemAdapter(bookList);
mRecyclerView = view.findViewById(R.id.recyclerView);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setAdapter(maAdapter);
gamebookViewModel = ViewModelProviders.of(this).get(GamebookViewModel.class);
gamebookViewModel.getAllGamebooks().observe(this, new Observer<List<Gamebook>>() {
#Override
public void onChanged(#Nullable List<Gamebook> gamebooks) {
maAdapter.submitList(bookList);
}
});
return view;
}
}
Perhaps you should not use it in onCreateView(). Try use call method that triggers observation of VM after an Activity had been created, near onStart(). Maybe it will help.
When a new page is available, we call submitList() method of the PagedListAdapter class
override fun onStart() {
super.onStart()
gamebookViewModel.callFunc()
}
Also as I understand, you call getAllGamebooks() which triggers some actions and return you LiveData<*> and you subscribe on it with observe(). I recommend you to separate this on 2 methods:
subscribe
action
Use PagedList instead of list like below:
gamebookViewModel.getAllGamebooks().observe(this, new Observer<PagedList<Gamebook>>() {
#Override
public void onChanged(#Nullable PagedList<Gamebook> gamebooks) {
maAdapter.submitList(bookList);
}
});
And in your viewmodel too.
Found the problem. My adapter wasn't extending ListAdapter, so no wonder it couldn't find the submitList() method. Rookie error.
Hey guys I really need your help. I've spent like 5 days trying to get my recyclerview to update only when the user presses OK on a dialogbox, that appears from the menu actionbar. I've tried every possible method I could think of, every method I've seen on stackoverflow, YouTube, etc. and none of them worked for me.
How do I get the recyclerview in a fragment to update after dialogbox closes? I know there are similar questions regarding updating the menu, and (recyclerviews with dialogfragments), but none of them have a combination.
Out of the countless attempts, the current code configuration posted below isn't causing any errors, however, the recyclerview remains blank. The closest attempt I had to finding a solution, was creating an adapter and setting up the recycler in onOptionsItemSelected. But obviously, it updates only when the user clicks the button, and the initial click would create a blank recyclerview.
Fragment:
(There's a lot of repeated commented code from different attempts)
public class ExerciseRoutine extends Fragment implements ExerciseRoutine_Dialog.RoutineDialogListener{
private String Routine_name, Routine_split;
private ArrayList<ExerciseRoutine_Information> Routine_information = new ArrayList<>();
private RecyclerView recyclerView;
private RecyclerView.Adapter adapter;
private RecyclerView.LayoutManager layoutManager;
#Override
public void sendInput(String name, String split, RecyclerView.Adapter DialogAdapter) {
Routine_name = name;
Routine_split = split;
adapter = DialogAdapter;
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.exercise_routine_fragment, container, false);
//Report that this fragment would like to participate in populating the options menu by
//receiving a call to onCreateOptionsMenu(Menu, MenuInflater) and related methods.
//If true, the fragment has menu items to contribute.
setHasOptionsMenu(true);
recyclerView = view.findViewById(R.id.ExerciseRoutine_Recycler);
//BuildRecyclerView();
//recyclerView.setHasFixedSize(true); //If the Recyclerview is static
/*Routine_information.add(new ExerciseRoutine_Information(Routine_name, Routine_split));
recyclerView = view.findViewById(R.id.ExerciseRoutine_Recycler);
//recyclerView.setHasFixedSize(true); //If the Recyclerview is static
layoutManager = new LinearLayoutManager(getActivity());
adapter = new ExerciseRoutineAdapter(Routine_information);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);*/
return view;
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.exercise_routine_menu, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()){
case R.id.action_addRoutine:
ExerciseRoutine_Dialog routineDialog = new ExerciseRoutine_Dialog();
routineDialog.setTargetFragment(ExerciseRoutine.this, 1);
routineDialog.show(getFragmentManager(), "Routine Dialog");
//Routine_information.add(new ExerciseRoutine_Information(Routine_name, Routine_split));
BuildRecyclerView();
//adapter.notifyItemInserted(Routine_information.size());
//if(!Routine_name.equals("") && !Routine_split.equals("")) {
//}
}
return super.onOptionsItemSelected(item);
}
public void BuildRecyclerView(){
layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
}
public void BuildAdapter(){
//adapter = new ExerciseRoutineAdapter(getContext(),Routine_information);
adapter.notifyItemInserted(Routine_information.size());
}
}
My Dialog Fragment:
public class ExerciseRoutine_Dialog extends DialogFragment{
private TextView ActionOK, ActionCANCEL;
private EditText Routine_name, Routine_split;
private RoutineDialogListener activityCommander;
private ArrayList<ExerciseRoutine_Information> Routine_information = new ArrayList<>();
private RecyclerView.Adapter adapter;
public interface RoutineDialogListener{
void sendInput(String name, String split, RecyclerView.Adapter DialogAdapter);
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
try{
activityCommander = (RoutineDialogListener) getTargetFragment();
}catch(ClassCastException e){
throw new ClassCastException(context.toString() + "Must Implement RoutineDialogListener");
}
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.exercise_routine_dialog, container, false);
Routine_name = view.findViewById(R.id.ExerciseRoutine_DialogNameInput);
Routine_split = view.findViewById(R.id.ExerciseRoutine_DialogSplitInput);
ActionOK = view.findViewById(R.id.ExerciseRoutine_DialogAction_OK);
ActionCANCEL = view.findViewById(R.id.ExerciseRoutine_DialogAction_CANCEL);
//recyclerView = view.findViewById(R.id.ExerciseRoutine_Recycler);
ActionCANCEL.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
getDialog().dismiss();
}
});
ActionOK.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
String name = Routine_name.getText().toString();
String split = Routine_split.getText().toString();
if(!name.equals("") && !split.equals("")) {
Routine_information.add(new ExerciseRoutine_Information(name, split));
adapter = new ExerciseRoutineAdapter(getContext(), Routine_information);
activityCommander.sendInput(name, split, adapter);
adapter.notifyItemInserted(Routine_information.size());
}
getDialog().dismiss();
}
});
return view;
}
}
Your current approach seems to be to pass the RecyclerView.Adapter to the DialogFragment and try to insert the new data on-the-spot. I think this is a problematic setup. The dialog's purpose is to offer some means for the users to enter the required data, period. It should not be tasked with the job of managing the RecyclerView or its Adapter because that way your components will be very tightly coupled:
Imagine that first you use a ListView in your implementation, and suddenly someone decides to ban every ListView from your app (maybe for performance reasons) and has you exchange them all for RecyclerViews. Then your approach would force you to change the code for the DialogFragment (it would have to cater to a different type of Adapter). A more loosely coupled implementation would enable you to make changes to one component without having to rewrite too many others.
That's why I won't try to make your code work as-is, instead I'd like to show you another way:
Because the RecyclerView is part of the Fragment's UI, the Fragment is the place where code related to managing the RecyclerView belongs. It is basically possible to have the Adapter as an inner class of the Fragment but I prefer having it as a standalone class if the code gets a little longer, and also because it enforces "decoupling".
Interfaces play a very important part in good architecture, so the DialogFragment will still make use of an interface to send its data. But it's up to the class which actually implements the interface (here: the Fragment) to pass the data to any interested third parties, e.g. the RecyclerView.Adapter (The Adapter in turn could have its own interface to publish important events like clicks on list items).
Having said that, here are some code snippets:
The DialogFragment
public class ExerciseRoutine_Dialog extends DialogFragment {
private EditText Routine_name, Routine_split;
public interface RoutineDialogListener{
/**
* There is some new ExerciseRoutine_Information
*/
void sendInput(String name, String split);
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.exercise_routine_dialog, container, false);
Routine_name = view.findViewById(R.id.ExerciseRoutine_DialogNameInput);
Routine_split = view.findViewById(R.id.ExerciseRoutine_DialogSplitInput);
TextView actionOK = view.findViewById(R.id.ExerciseRoutine_DialogAction_OK);
TextView actionCANCEL = view.findViewById(R.id.ExerciseRoutine_DialogAction_CANCEL);
actionCANCEL.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
getDialog().dismiss();
}
});
actionOK.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
String name = Routine_name.getText().toString();
String split = Routine_split.getText().toString();
if(!name.equals("") && !split.equals("")) {
// just send the input to the main Fragment
RoutineDialogListener listener = getListener();
if(listener != null) {
listener.sendInput(name, split);
}
}
getDialog().dismiss();
}
});
return view;
}
/**
* Tries to find a suitable listener, examining first the hosting Fragment (if any) and then the Activity.
* Will return null if this fails
* #return x
*/
private RoutineDialogListener getListener(){
RoutineDialogListener listener;
try{
Fragment onInputSelected_Fragment = getTargetFragment();
if (onInputSelected_Fragment != null){
listener = (RoutineDialogListener) onInputSelected_Fragment;
}
else {
Activity onInputSelected_Activity = getActivity();
listener = (RoutineDialogListener) onInputSelected_Activity;
}
return listener;
}catch(ClassCastException e){
Log.e("Custom Dialog", "onAttach: ClassCastException: " + e.getMessage());
}
return null;
}
}
The Fragment:
public class ExerciseRoutine extends Fragment implements ExerciseRoutine_Dialog.RoutineDialogListener{
public static final String ROUTINE_DIALOG = "Routine Dialog";
private ArrayList<ExerciseRoutine_Information> routineInformations = new ArrayList<>();
private RecyclerView.Adapter adapter;
public static ExerciseRoutine instance(){
return new ExerciseRoutine();
}
#Override
public void sendInput(String name, String split) {
routineInformations.add(new ExerciseRoutine_Information(name, split));
adapter.notifyItemInserted(routineInformations.size());
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.exercise_routine_fragment, container, false);
setHasOptionsMenu(true);
RecyclerView recyclerView = view.findViewById(R.id.ExerciseRoutine_Recycler);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
adapter = new ExerciseRoutineAdapter(getContext(), routineInformations);
// So far you have a RecyclerView with an empty List.
recyclerView.setAdapter(adapter);
return view;
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.exercise_routine_menu, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()){
case R.id.action_addRoutine:
showDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showDialog(){
ExerciseRoutine_Dialog routineDialog = new ExerciseRoutine_Dialog();
routineDialog.setTargetFragment(ExerciseRoutine.this, 1);
routineDialog.show(getFragmentManager(), ROUTINE_DIALOG);
}
}
Application
I am building an Android application wherein order for certain item will be taken from customers. These order can have 4 different status : Pending, Confirmed, Completed and Cancelled. I have written necessary firebase rule for the same.
My initial design was listing up all the orders in single activity, HomeActivity, and everything looked good. But then I decided to change the design to tabs and viewpager for each status of the order in the same HomeActivity. I am using one of the best library, smart tab layout to generate my views.
Current Design
I planned to keep one single Fragment and update the adapter of recyclerview accordingly.
HomeActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
Global.replaceFragmentWithAnimation(new HomeTabFragment(), getSupportFragmentManager(), R.id.frame_container);
}
Global.replaceFragmentWithAnimation is a static method which just replaces the fragment to necessary container.
HomeTabFragment.java
public class HomeTabFragment extends Fragment implements SmartTabLayout.TabProvider {
public HomeTabFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_home_tab, container, false);
final ViewPager viewPager = view.findViewById(R.id.viewpager);
final SmartTabLayout viewPagerTab = view.findViewById(R.id.viewpagertab);
viewPagerTab.setCustomTabView(this);
FragmentPagerItems pages = new FragmentPagerItems(getContext());
pages.add(FragmentPagerItem.of("Pending", OrderListsFragment.class));
pages.add(FragmentPagerItem.of("Confirmed", OrderListsFragment.class));
pages.add(FragmentPagerItem.of("Cancelled", OrderListsFragment.class));
pages.add(FragmentPagerItem.of("Completed", OrderListsFragment.class));
FragmentStatePagerItemAdapter adapter = new FragmentStatePagerItemAdapter(
getActivity().getSupportFragmentManager(), pages);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(0);
viewPagerTab.setViewPager(viewPager);
viewPagerTab.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
});
return view;
}
#Override
public View createTabView(ViewGroup container, int position, PagerAdapter adapter) {
LayoutInflater inflater = LayoutInflater.from(container.getContext());
View tab = inflater.inflate(R.layout.custom_tab_icon_and_notification_mark, container, false);
TextView txtTab = tab.findViewById(R.id.txtTitle);
switch (position) {
case 0:
txtTab.setText(getResources().getString(R.string.pending_tab_text));
break;
case 1:
txtTab.setText(getResources().getString(R.string.confirmed_tab_text));
break;
case 2:
txtTab.setText(getResources().getString(R.string.cancelled_tab_text));
break;
case 3:
txtTab.setText(getResources().getString(R.string.completed_tab_text));
break;
default:
throw new IllegalStateException("Invalid pos - " + position);
}
return tab;
}
}
and finally this is my OrderListFragment
OrderListFragment.java
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_order_lists, container, false);
recycler_view_order_list = view.findViewById(R.id.recycler_view_order_list); //declared outside
emptyView = view.findViewById(R.id.empty_view);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
recycler_view_order_list.setItemAnimator(new DefaultItemAnimator());
recycler_view_order_list.setLayoutManager(mLayoutManager);
loadOrders();
return view;
}
public void loadOrders() {
if (recycler_view_order_list != null) {
recycler_view_order_list.setHasFixedSize(true);
}
final DatabaseReference orderTableRef = Global.getDatabase().getReference(Constants.ORDERS_TABLE);
final DatabaseReference customerRef = Global.getDatabase().getReference(Constants.CUSTOMERS_TABLE);
orderTableRef.keepSynced(true);
customerRef.keepSynced(true);
final Query orderList = orderTableRef.orderByChild("status").equalTo("Pending");
//I hardcoded Pending value to make sure everything looks good.
Global.adapter = new FirebaseRecyclerAdapter<Order, OrderItemHolder>(
Order.class,
R.layout.order_item_view,
OrderItemHolder.class,
orderList
) {
#Override
protected void populateViewHolder(final OrderItemHolder viewHolder, final Order model, final int position) {
if(model.getCustId()!=null) {
customerRef.child(model.getCustId()).addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Customers customers = dataSnapshot.getValue(Customers.class);
//set Customer name
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
if ("Pending".equals(model.getStatus())) {
//Some UI updates for Pending
}
if ("Completed".equals(model.getStatus())) {
//Some UI updates for Pending
}
if ("Cancelled".equals(model.getStatus())) {
//Some UI updates for Pending
}
if ("Confirmed".equals(model.getStatus())) {
//Some UI updates for Pending
}
}
}
};
orderTableRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (!dataSnapshot.hasChildren())
emptyView.setVisibility(View.VISIBLE);
else
emptyView.setVisibility(View.GONE);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
emptyView.setVisibility(View.GONE);
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
if (Global.adapter.getItemCount() == 0)
emptyView.setVisibility(View.VISIBLE);
else
emptyView.setVisibility(View.GONE);
}
};
Global.adapter.registerAdapterDataObserver(mObserver);
recycler_view_order_list.setAdapter(Global.adapter);
}
Very long code but I've shortened it as much as possible. I've couple of problems here.
The data never gets displayed even though it is fetched in orderList. when debugged, it never hits populateViewHolder of FirebaseRecyclerAdapter. When I did some research on this issue, All I found was to add RecyclerView.AdapterDataObserver and register it to Global.adapter which I've already implemented. I am still confused as in why the data is not fetched. addListenerForSingleValueEvent for orderTableRef hits anyhow and if (!dataSnapshot.hasChildren()) condition within evaluates to false thus hiding emptyView message. Few suggestions from GitHub issue also stated to remove recycler_view_order_list.setHasFixedSize(true); but it did not help either.
How can I pass different order status for different tabs? I tried doing below within:
HomeTabFragment.java
Bundle bundle=new Bundle();
bundle.putString("orderType","Pending");
pages.add(FragmentPagerItem.of("Pending", OrderListsFragment.class,bundle));
bundle.putString("orderType","Confirmed");
pages.add(FragmentPagerItem.of("Confirmed", OrderListsFragment.class,bundle));
bundle.putString("orderType","Cancelled");
pages.add(FragmentPagerItem.of("Cancelled",OrderListsFragment.class,bundle));
bundle.putString("orderType","Completed");
pages.add(FragmentPagerItem.of("Completed", OrderListsFragment.class,bundle));
Overriding FragmentPagerItems's add method and passing budnle with different value for same key but then it ended up passing last value i.e. Completed during every initialization.
Note - FragmentPagerItems is an utility extension available with the tabs library used.
Could anyone point me in the right direction here?
I have faced with a problem that my RecyclerView didn't show up after it gets the data from server side.
When I am trying to debug my code - sometimes it went to DataAdapter class - sometimes not. Unfortunatly, I don't understand why it happends.
I am using the MVP pattern, and maybe I am setting data to adapter wrong.
I'll try to put code here and explain what am I doing.
First of all my card view xml file:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="5dp"
card_view:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ImageView android:id="#+id/info_image"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_weight="1.0"
android:scaleType="centerCrop"/>
<TextView
android:id="#+id/info_text"
android:layout_marginLeft="5dp"
android:layout_marginBottom="5dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
</LinearLayout>
</android.support.v7.widget.CardView>
Here is my Adapter for Recycler view:
public class DataAdapter extends RecyclerView.Adapter<DataAdapter.ViewHolder> {
private AutoService[] autoServices = new AutoService[]{};
private Context context;
private static final int IMAGE_WIDTH = 120;
private static final int IMAGE_HEIGHT = 60;
//TODO check how to properly set data on view from presenter
public void setAutoServices(AutoService[] autoServices) {
this.autoServices = autoServices;
}
public DataAdapter(Context context) {
this.context = context;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
CardView cardView = (CardView) LayoutInflater.from(parent.getContext()).
inflate(R.layout.auto_service_card_view, parent, false);
return new ViewHolder(cardView);
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
CardView cardView = holder.cardView;
ImageView imageView = (ImageView)cardView.findViewById(R.id.info_image);
Picasso.with(context).load(autoServices[position].getImageURL()).resize(IMAGE_WIDTH, IMAGE_HEIGHT).into(imageView);
TextView textView = (TextView)cardView.findViewById(R.id.info_text);
textView.setText(autoServices[position].getServiceName());
}
#Override
public int getItemCount() {
return autoServices.length;
}
public class ViewHolder extends RecyclerView.ViewHolder{
private CardView cardView;
public ViewHolder(CardView card) {
super(card);
cardView = card;
}
}
}
In my fragment(view) I have a setter which sets the data for adapter.
Date went from presenter which calls data from server side useng rest template.
Here is my presenter:
public class RecyclerViewPresenter implements RecyclerViewContract.Presenter {
private RestTemplate restTemplate;
private RecyclerViewContract.View view;
public RecyclerViewPresenter(RecyclerViewContract.View view){
this.view = view;
new RecyclerViewTask().execute();
}
private class RecyclerViewTask extends AsyncTask<Void, Void, AutoService[]>{
#Override
protected AutoService[] doInBackground(Void... voids) {
restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
ResponseEntity<AutoService[]> autoServiceEntity = restTemplate.getForEntity(URL.getAllAutoServices(), AutoService[].class);
return autoServiceEntity.getBody();
}
#Override
protected void onPostExecute(AutoService[] autoServices) {
view.setAutoServiceListOnAdapter(autoServices);
}
}
}
For some reason when my activity with fragment(RecyclerView) starts - the recyclerView someTimes shows up sometimes not. May be am I setting data for adapter wrong using MVP?
Also I am using the Picasso library - and pictures didn't load from url path, for example URL: https://pbs.twimg.com/profile_images/616076655547682816/6gMRtQyY.jpg .
May be the bproblem in Picasso library ?
Any suggestions please ?
public class RecyclerViewFragment extends Fragment implements RecyclerViewContract.View{
private RecyclerViewContract.Presenter presenter;
private DataAdapter dataAdapter;
public RecyclerViewFragment() {
// Required empty public constructor
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(presenter == null){
presenter = new RecyclerViewPresenter(this);
}
}
public static RecyclerViewFragment getInstance(){
return new RecyclerViewFragment();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
RecyclerView recyclerView = (RecyclerView) inflater.inflate(R.layout.fragment_recycler_view, container, false);
dataAdapter = new DataAdapter(getActivity());
recyclerView.setAdapter(dataAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
return recyclerView;
}
#Override
public void setPresenter(RecyclerViewContract.Presenter presenter) {
this.presenter = presenter;
}
#Override
public void setAutoServiceListOnAdapter(AutoService[] autoServices) {
dataAdapter.setAutoServices(autoServices);
}
Also when I am trying to open activity with recycler view and it didn't shows I have next log:
W/EGL_emulation: eglSurfaceAttrib not implemented
W/OpenGLRenderer: Failed to set EGL_SWAP_BEHAVIOR on surface 0xe1b01280, error=EGL_SUCCESS
D/OpenGLRenderer: endAllStagingAnimators on 0xeb8d2e80 (RippleDrawable) with handle 0xe1b2dab0
When I am reopen the activity with recycler view and it shows - I have next log:
W/Settings: Setting airplane_mode_on has moved from android.provider.Settings.System to android.provider.Settings.Global, returning read-only value.
D/OpenGLRenderer: endAllStagingAnimators on 0xeb8d2e80 (RippleDrawable) with handle 0xe25165b0
A/libc: Fatal signal 4 (SIGILL), code 2, fault addr 0xf6f1d46e in tid 2539 (16/6gMRtQyY.jpg)
and application crashes after this.
Check my answer.
Try to return proper fragment root viewGroup, not the widget view.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_recycler_view, container, false);
RecyclerView recyclerView = (RecyclerView)view.findViewById(R.id.recyclerView);
dataAdapter = new DataAdapter(getActivity());
recyclerView.setAdapter(dataAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
return view;
}
Edit: I have added dataAdapter.notifyDataSetChanged(); line in place where adapter gets data.
#Override
public void setAutoServiceListOnAdapter(AutoService[] autoServices) {
dataAdapter.setAutoServices(autoServices);
dataAdapter.notifyDataSetChanged();
}