0

I have a denormalized database which can be simplified as follows:

|__ menu
|     |___ menuKey1
|             |___ subMenus (key list of subMenus belongs to menu)
|             |___ other fields...
|
|__ subMenu
|     |___ subMenuKey1
|             |__ menuItems (key list of menuItems belongs to subMenu)
|             |__ other fields...
|
|__ menuItem
      |_____ ...

What I want to show in the UI is the list of submenus (outer RecyclerView), each having their own list of menu items (one inner RecyclerView for each row of outer RecyclerView). I achieved this with following code in onCreate() of my fragment:

rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference subMenuKeyRef = rootRef.child("menu").child(menuKey).child("subMenus");
DatabaseReference subMenuRef = rootRef.child("subMenu");
subMenuAdapter = new FirebaseIndexRecyclerAdapter<MySubMenu, SubMenuHolder>(
        MySubMenu.class,
        R.layout.row_submenu,
        SubMenuHolder.class,
        subMenuKeyRef,
        subMenuRef) {
    @Override
    protected void populateViewHolder(SubMenuHolder viewHolder, MySubMenu model, int position) {
        String subMenuKey = getRef(position).getKey();
        DatabaseReference menuItemKeyRef = rootRef.child("subMenu").child(subMenuKey).child("menuItems");
        DatabaseReference menuItemRef = rootRef.child("menuItem");
        FirebaseIndexRecyclerAdapter menuItemAdapter = new FirebaseIndexRecyclerAdapter<MyMenuItem, MenuItemHolder>(
                MyMenuItem.class,
                R.layout.row_menu_item,
                MenuItemHolder.class,
                menuItemKeyRef,
                menuItemRef) {
            @Override
            protected void populateViewHolder(MenuItemHolder viewHolderx, MyMenuItem modelx, int positionx) {
                viewHolderx.setName(modelx.getName());
                viewHolderx.setPrice(modelx.getPrice());
            }
        };
        viewHolder.setName(model.getName());
        viewHolder.setMenuItemList(menuItemAdapter);
    }
};
subMenuList.setAdapter(subMenuAdapter);

Here is row_submenu.xml, which is row of outer RecyclerView:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:visibility="gone">

    <TextView
        android:id="@+id/submenu_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textAllCaps="true"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/submenu_menu_items"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:paddingBottom="8dp"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/submenu_name" />

</android.support.constraint.ConstraintLayout>

row_menu_item.xml just shows menuItem's name and price in TextViews so I don't include it here.

To sum up, this design causes weird animations, jumps, etc. during initialization of RecyclerViews. I have two questions:

  • Is there a better alternative in terms of design? (Something equivalent to ExpandableListView)

  • If not, how can I detect that initial data load is complete (so that I can hide everything until last menuItem is retrieved)? I think calling addListenerForSingleValueEvent on menuItemRef won't work here.

Mehmed
  • 2,880
  • 4
  • 41
  • 62

2 Answers2

0

The FirebaseUI adapters have a method called onDataChanged() to detect when a data change has completely loaded.

See the source code on github. From there:

This method will be triggered each time updates from the database have been completely processed. So the first time this method is called, the initial data has been loaded - including the case when no data at all is available. Each next time the method is called, a complete update (potentially consisting of updates to multiple child items) has been completed.

You would typically override this method to hide a loading indicator (after the initial load) or to complete a batch update to a UI element.

Also see: How to dismiss a progress bar even if there is no view to populate in the FirebaseListAdapter?, which I just updated with this information (and a sample).

Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • I know when data load is complete for outer adapter. But each entry creates another inner adapter here. I want to know when the last inner data load completion happens but I cannot since all of them are asynchronous. – Mehmed Oct 04 '17 at 14:41
0

I used two counters to be able to detect the end of asynchronous retrievals. Key counts are cumulatively added into totalItemCount so it holds total number of items to be loaded inside the inner adapters. When inner adapters populate a ViewHolder, populatedItemCount is incremented by one and its current value is compared with totalItemCount. If values are equal, it means all data is now retrieved and the parent RecyclerView and RecyclerViews inside its rows can be made visible.

Here is the last version of the nested adapters I provided in the question:

rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference subMenuKeyRef = rootRef.child("menu").child(menuKey).child("subMenus");
DatabaseReference subMenuRef = rootRef.child("subMenu");
subMenuAdapter = new FirebaseIndexRecyclerAdapter<MySubMenu, SubMenuHolder>(
        MySubMenu.class,
        R.layout.row_submenu,
        SubMenuHolder.class,
        subMenuKeyRef,
        subMenuRef) {
    int totalItemCount = 0;
    int populatedItemCount = 0;

    @Override
    protected void populateViewHolder(SubMenuHolder viewHolder, MySubMenu model, int position) {
        totalItemCount += model.getMenuItems().size();
        String subMenuKey = getRef(position).getKey();
        DatabaseReference menuItemKeyRef = rootRef.child("subMenu").child(subMenuKey).child("menuItems");
        DatabaseReference menuItemRef = rootRef.child("menuItem");
        FirebaseIndexRecyclerAdapter menuItemAdapter = new FirebaseIndexRecyclerAdapter<MyMenuItem, MenuItemHolder>(
                MyMenuItem.class,
                R.layout.row_menu_item,
                MenuItemHolder.class,
                menuItemKeyRef,
                menuItemRef) {
            @Override
            protected void populateViewHolder(MenuItemHolder viewHolderx, MyMenuItem modelx, int positionx) {
                populatedMenuItemCount++;
                viewHolderx.setName(modelx.getName());
                viewHolderx.setPrice(modelx.getPrice());

                // TODO - Find a resolution if this if never gets true
                if (populatedMenuItemCount == totalMenuItemCount) {
                    (new Handler()).postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            showSubMenuList();
                        }
                    }, 500);
                }
            }
        };
        viewHolder.setName(model.getName());
        viewHolder.setMenuItemList(menuItemAdapter);
    }
};
subMenuList.setAdapter(subMenuAdapter);

I am still not sure about the if statement that compares two counters. Would there be a case where data retrieval somehow stops and thus the condition of the if statement never become true and app hangs?

Mehmed
  • 2,880
  • 4
  • 41
  • 62