2

I am experimenting with integrating a Firebase-backed RecyclerView in a React Native app. With hardcoded data it works well, but upon inserting rows loaded dynamically and calling either notifyItemInserted() or notifyDataSetChanged() on the RecyclerView.Adapter, the RecyclerView itself does not reflect the change. This manifests as an initial blank view until the app's refresh button is tapped, which has the sole effect of re-rendering the RecyclerView from the React Native side, or until scrolling or a screen orientation change.

After reading through a dozen or more questions on this site about similar issues not involving React Native and having tried most of the common solutions, I suspect this issue is related specifically to React Native.

Finding a solution to this could be helpful to many, given the ongoing performance issues with React Native's list view implementations.

public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.ViewHolder> {

    ArrayList<Post> mDataset;

    public PostsAdapter(ArrayList<Post> list){
        mDataset = list;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public LinearLayout mPostView;
        public ViewHolder(LinearLayout v) {
            super(v);
            mPostView = v;
        }
    }

    @Override
    public PostsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        LinearLayout v = (LinearLayout) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.post, parent, false);
        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        TextView tv = (TextView) holder.mPostView.findViewById(R.id.title);
        tv.setText(mDataset.get(position).title);
    }

    @Override
    public int getItemCount() {
        return mDataset.size();
    }

}

 public class RNPostsViewManager extends SimpleViewManager{
    private ArrayList<Post> mDataset = new ArrayList<>();
    public static final String REACT_CLASS = "AndroidPostsView";
    private RecyclerView mRecyclerView;
    private PostsAdapter mAdapter;
    private RecyclerView.LayoutManager mLayoutManager;
    private DatabaseReference mDatabase;
    private Query initialDataQuery;
    private ChildEventListener initialDataListener;

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @UiThread
    public void addPost (Post p){
        mDataset.add(p);
        mAdapter.notifyItemInserted(mDataset.size()-1);
    }

    @Override
    public RecyclerView createViewInstance( ThemedReactContext context) {

        mAdapter = new PostsAdapter(mDataset);

        mRecyclerView = new RecyclerView(context){
            @Override
            protected void onAttachedToWindow() {
                super.onAttachedToWindow();
                initialDataQuery.addChildEventListener(initialDataListener);
            }
        };

        mLayoutManager = new LinearLayoutManager(context.getCurrentActivity(), LinearLayoutManager.VERTICAL, false);
        DividerItemDecoration mDecoration = new DividerItemDecoration(context, 1);
        mDecoration.setDrawable(ContextCompat.getDrawable(context.getCurrentActivity(), R.drawable.sep));
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.addItemDecoration(mDecoration);
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mRecyclerView.setAdapter(mAdapter);

        mDatabase = FirebaseDatabase.getInstance().getReference();
        initialDataQuery = mDatabase.child("wp-posts").orderByChild("unixPostDate").limitToFirst(100);
        initialDataListener = new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
                Post p = dataSnapshot.getValue(Post.class);
                addPost(p);
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
            }
            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
            }
            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
            }
            @Override
            public void onCancelled(DatabaseError databaseError) {
            }
        };

        return mRecyclerView;
    }

}

// layout file post.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"

    android:background="@android:color/background_light"
    android:clickable="true"
    android:focusable="true"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="10dp"
    android:weightSum="1">

    <Button
        android:id="@+id/button"
        style="@style/Widget.AppCompat.Button.Small"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="20dp"
        android:background="@android:drawable/ic_menu_more"
        android:gravity="center_vertical" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical"
        android:orientation="horizontal"
        android:weightSum="1">

        <TextView
            android:id="@+id/title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="fill_vertical"
            android:layout_weight="1"
            android:gravity="top"
            android:text="TextView"
            android:textColor="@color/title"
            android:textStyle="bold" />
    </LinearLayout>

</LinearLayout>
Isaac16
  • 59
  • 2
  • 7

2 Answers2

19

The problem is that requestLayout does not work well when the RecyclerView is a native UI component.

The following hack made all those issues go away:

I now overwrite the requestLayout method inside my RecyclerView. Then before any notify* method, or even scrollToPosition calls or any method that invokes a re-layout, I allow my custom requestLayout method to force a re-layout.

The end result looks like this:

private boolean mRequestedLayout = false;


public void aMethodThatUpdatesStuff(int indexToUpdate, ReadableMap updatedChild) {
    final SPAdapter adapter = (SPAdapter) getAdapter();
    mRequestedLayout = false;
    adapter.updateDataAtIndex(indexToUpdate, updatedChild); // <-- this runs notifyItemChanged inside
}

@Override
public void requestLayout() {
    super.requestLayout();
    // We need to intercept this method because if we don't our children will never update
    // Check https://stackoverflow.com/questions/49371866/recyclerview-wont-update-child-until-i-scroll
    if (!mRequestedLayout) {
        mRequestedLayout = true;
        this.post(new Runnable() {
            @SuppressLint("WrongCall")
            @Override
            public void run() {
                mRequestedLayout = false;
                layout(getLeft(), getTop(), getRight(), getBottom());
                onLayout(false, getLeft(), getTop(), getRight(), getBottom());
            }
        });
    }
}
SudoPlz
  • 20,996
  • 12
  • 82
  • 123
0

The root view in ReactNative is ReactRootView which onlayout is an empty method.

when call notifyDatasetChanged in RecyclerView, it's actually request layout to relayout its children. And the layout method will call super.layout to travel the whole view tree first. So that't a problem when the root view is ReactRootView.

You can manually call RecyclerView.onlayout(boolean changed, int l, int t, int r, int b) to trigger its children relayout to make notifyDatasetChanged work.

fuxi chu
  • 404
  • 4
  • 6
  • But `onLayout` does not even get invoked inside the `ReactRootView` when a notify* runs, I don't think that's the issue here. :/ – SudoPlz Mar 19 '18 at 21:11