更改配置后从后台线程更新 recyclerview 项目

Update recyclerview item from background thread after changing configuration

我在 Fragment 中有一个 RecyclerView。我还有一个 AsyncTask 从后台线程更新 RecyclerView 的项目。问题是当我旋转设备(或导致任何其他配置更改)时,AsyncTask 无法更新视图。

这里可以看到简化的代码:

HomeFragment:

public class HomeFragment extends Fragment {

    public HomeFragment() {
    }

    public static HomeFragment newInstance() {
        return new HomeFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_home, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        RecyclerView mRecyclerView = requireView().findViewById(R.id.recyclerview);
        Activity activity = requireActivity();
        ArrayList<String> items = new ArrayList<>(Arrays.asList("Item1", "Item2", "Item3"));
        ListAdapter adapter = new ListAdapter(activity, items);
        mRecyclerView.setAdapter(adapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
    }
}

RecyclerView的适配器及其内部AsyncTaskclass:

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

    private final ArrayList<String> items;
    private final LayoutInflater mInflater;
    private RecyclerView mRecyclerView;

    public ListAdapter(Context context, ArrayList<String> items) {
        mInflater = LayoutInflater.from(context);
        this.items = items;
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        mRecyclerView = recyclerView;
    }

    @NonNull
    @Override
    public ListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View mItemView = mInflater.inflate(R.layout.item, parent, false);
        return new ViewHolder(mItemView, this);
    }

    @Override
    public void onBindViewHolder(@NonNull ListAdapter.ViewHolder holder, int position) {
        holder.name.setText(items.get(position));
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder {
        final ListAdapter mAdapter;
        final TextView name;

        public ViewHolder(@NonNull View itemView, ListAdapter mAdapter) {
            super(itemView);
            this.mAdapter = mAdapter;
            this.name = itemView.findViewById(R.id.file_name);
            itemView.setOnClickListener(v -> {
                Task task = new Task();
                task.execute();
            });
        }
    }

    class Task extends AsyncTask<Void, Integer, Void> {

        @Override
        protected void onProgressUpdate(Integer... values) {
            mRecyclerView.post(() ->
                    ((ViewHolder) Objects.requireNonNull(mRecyclerView.findViewHolderForLayoutPosition(0))).
                            name.setText(String.valueOf(values[0])));
            mRecyclerView.invalidate();
        }

        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i <= 30; i++) {
                publishProgress(i);
                SystemClock.sleep(500);
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void unused) {
            ((ViewHolder) Objects.requireNonNull(mRecyclerView.findViewHolderForLayoutPosition(0))).name.setText("task is finished");
        }
    }
}

如您所见,我使用了 mRecyclerView.post() 方法来避免接触来自非 UI 线程的视图。如何在旋转设备后更新项目?

我之前尝试过的:

您可以使用以下代码在屏幕旋转中更新回收器视图。

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {

    super.onConfigurationChanged(newConfig);

    if(adapter!=null){
        adapter.notifyDataSetChanged();
    }
}

onCreate()onCreateView()onActivityCreated() 方法中调用 setRetainInstance(true)

异步任务默认在后台线程上工作,我们无法从后台线程更新我们的 ui 它用于网络调用等密集型工作。请尝试 运行ning a

runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // WORK on UI thread here
        }
    });

异步任务的 onPostExecute 方法中的语句,因为它表示您的后台处理已完成。将更新视图的代码放入此 运行() 方法

AsyncTask 自 API 级别 30 起已弃用。它存在问题,特别是对于配置更改 per documentation:

AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes.

而且由于这些问题,它不应该用于长时间的 运行ning 任务。

AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.

问题:

当您 运行 在 doInBackground 中执行任务时,当配置更改时,会重新创建 activity(即旧的 activity 被销毁,新的 activity 已创建);但是附加到旧 activity 的 AsyncTask 继续 运行 (导致内存泄漏);等稍后 onPostExecute() 准备好将结果发布到 UI 线程时;它会将它们释放到已销毁的 activity 而不是屏幕上显示的当前 activity;因此用户不会看到它们。因此,您的案件结果被驳回。

因此,建议查看其他后台线程替代方案,例如 Executors 或使用回调来更新 UI 或使用 ViewModel(在配置更改后仍然存在)& LiveData 将结果发布到 UI.

带执行器的解决方案

  • 此处提供Executor

听众:

interface ProgressListener {
    void onPostExecute();

    void onProgressUpdate(int value);
}

片段:

    ListAdapter adapter = new ListAdapter(activity, items, new ProgressListener() {
            @Override
            public void onPostExecute() {
                ((ListAdapter.ViewHolder) Objects.requireNonNull(recyclerView.
                        findViewHolderForLayoutPosition(0))).name.setText("task is finished");
            }

            @Override
            public void onProgressUpdate(int value) {
                recyclerView.post(() ->
                        ((ListAdapter.ViewHolder) Objects.requireNonNull(recyclerView.findViewHolderForLayoutPosition(0))).
                                name.setText(String.valueOf(value)));
                recyclerView.invalidate();
            }
        });

        mRecyclerView.setAdapter(adapter);

适配器:只使用单线程执行器:

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

    ProgressListener progressListener;

    public ListAdapter(Context context, ArrayList<String> items, ProgressListener listener) {
        mInflater = LayoutInflater.from(context);
        this.items = items;
        progressListener = listener;
    }
    
    
    // ........... CODE IS OMITTED
    
    
    // ViewHolder constructor
    
        public ViewHolder(@NonNull View itemView, ListAdapter mAdapter) {
            super(itemView);
            this.mAdapter = mAdapter;
            this.name = itemView.findViewById(R.id.file_name);
            itemView.setOnClickListener(v -> {
                Executors.newSingleThreadExecutor().execute(() -> {
                    for (int i = 0; i <= 30; i++) {
                        progressListener.onProgressUpdate(i);
                        SystemClock.sleep(500);
                    }
                    progressListener.onPostExecute();
                });
            });
        }
    

}   

使用 AsyncTask

  • 如果你还需要使用AsyncTask来解决这个问题,不推荐如前所述。您需要以某种方式保留适配器和 AsnyncTask 实例;这里使用了一个 ViewModel:

创建 ViewModel 以保存适配器和 AsyncTask 实例

public class MainViewModel extends AndroidViewModel {

    AsyncTask<Void, Integer, Void> myTask;
    ListAdapter adapter;

    public MainViewModel(@NonNull Application application) {
        super(application);
    }

}

在片段中:仅当适配器为空时才实例化适配器,并将 AsynkTask 的实例传递给适配器:

    if (viewModel.adapter == null)
        viewModel.adapter = new ListAdapter(activity, items, viewModel);
    recyclerView.setAdapter(viewModel.adapter);

最后在适配器中使用ViewModel AsynkTask实例:

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

    // ......... code is omitted

    private AsyncTask<Void, Integer, Void> task;

    public ListAdapter(Context context, ArrayList<String> items, AsyncTask<Void, Integer, Void> myTask) {
        mInflater = LayoutInflater.from(context);
        this.items = items;
        this.task = myTask;
    }
    
    // ......... code is omitted
    
    
    public class ViewHolder extends RecyclerView.ViewHolder {
        final ListAdapter mAdapter;
        final TextView name;

        public ViewHolder(@NonNull View itemView, ListAdapter mAdapter) {
            super(itemView);
            this.mAdapter = mAdapter;
            this.name = itemView.findViewById(R.id.file_name);
            itemView.setOnClickListener(v -> {
                task = new Task();
                task.execute();
            });
        }
    }
    
    
}