React Native 中的 RecyclerView:notifyItemInserted() 和 notifyDataSetChanged() 没有效果

IT技术 android reactjs react-native firebase-realtime-database android-recyclerview
2021-04-29 18:38:10

我正在尝试在 React Native 应用程序中集成 Firebase 支持的 RecyclerView。对于硬编码数据,它运行良好,但在插入动态加载的行并在 RecyclerView.Adapter 上调用 notifyItemInserted() 或 notifyDataSetChanged() 时,RecyclerView 本身不会反映更改。这表现为初始空白视图,直到点击应用程序的刷新按钮,这唯一的效果是从 React Native 端重新渲染 RecyclerView,或者直到滚动或屏幕方向改变。

在本网站上阅读了十几个或更多关于不涉及 React Native 的类似问题并尝试了大多数常见解决方案后,我怀疑这个问题与 React Native 相关。

考虑到 React Native 的列表视图实现的持续性能问题,找到解决方案可能对许多人有帮助。

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>
2个回答

问题是requestLayout当它RecyclerView是本机 UI 组件时效果不佳

以下黑客使所有这些问题都消失了:

我现在覆盖了requestLayout我的RecyclerView. 然后在任何notify*方法,甚至scrollToPosition调用或任何调用重新布局的方法之前,我允许我的自定义requestLayout方法强制重新布局。

最终结果如下所示:

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

ReactNative 中的根视图是 ReactRootView,它onlayout是一个空方法。

当调用notifyDatasetChangedRecyclerView 时,它实际上是请求layout重新布局它的孩子。并且布局方法将首先调用super.layout遍历整个视图树。所以当根视图是 ReactRootView 时这不是问题。

您可以手动调用RecyclerView.onlayout(boolean changed, int l, int t, int r, int b)以触​​发其子项重新布局以使 notifyDatasetChanged 工作。