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:
notifyDataSetChanged()
AsyncTask
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.
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.
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();
});
});
}
}
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();
});
}
}
}