Search code examples
javaandroidandroid-fragmentsandroid-recyclerviewandroid-adapter

Update recyclerview item from background thread after changing configuration


I have a RecyclerView in a Fragment. Also I have an AsyncTask which updates items of RecyclerView from background thread. The problem is when I rotate the device (or cause any other configuration change), the AsyncTask cannot update views.

Here you can see the simplified code:

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

The adapter of RecyclerView and its inner AsyncTask class:

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

As you can see I used mRecyclerView.post() method to avoid touching views from non UI thread. How I can update items after rotating the device?

What I tried before:

  • changing data set of adapter and calling notifyDataSetChanged()
  • creating a method in Fragment for updating items and calling it from AsyncTask
  • resetting adapter for RecyclerView
  • ...

Solution

  • AsyncTask is deprecated since API level 30. It has issues especially for configuration changes 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.

    And because of these issues it shouldn't be used in long running tasks.

    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.

    Issue:

    When you run tasks in doInBackground, when the configuration changes, the activity is recreated (i.e. the old activity is destroyed, and a new activity is created); but the AsyncTask that was attached to the old activity continues to run (causing memory leaks); and so later on when onPostExecute() is ready to publish the results to the UI thread; it will release them to the destroyed activity rather than the current activity displayed on the screen; and the user won't see them consequently. Therefore the result is dismissed in your case.

    So, it's recommended to see other background threading alternatives such as Executors either with callbacks to update the UI or with ViewModel (which survive configuration changes) & LiveData to publish the results to the the UI.

    Solution With Executor

    • Here Executor is provided:

    Listener:

    interface ProgressListener {
        void onPostExecute();
    
        void onProgressUpdate(int value);
    }
    

    Fragment:

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

    Adapter: just use single thread executor:

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

    With AsyncTask

    • If you still need to solve this using AsyncTask, which is not recommended as previously mentioned. You need someway to preserve the adapter & AsnyncTask instances; here a ViewModel is used:

    Create the ViewModel to save the adapter & AsnyncTask instances

    public class MainViewModel extends AndroidViewModel {
    
        AsyncTask<Void, Integer, Void> myTask;
        ListAdapter adapter;
    
        public MainViewModel(@NonNull Application application) {
            super(application);
        }
    
    }
    

    In fragment: Instantiate the adapter only if it's null, and pass an instance of AsynkTask to the adapter:

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

    Finally use the ViewModel AsynkTask instance in the adapter:

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