Issue
I have a screen that displays a list of launchable apps. The list is fetched from SharedPreferences
via a combination of RxJava2 and LiveData. Specifically, I observe a LiveData<List<AppModel>>
on my fragment's onStart
method. Once this list is fetched successfully using RxJava2, I update the UI with the list using LiveData and I set it to my RecyclerView.
However, I have noticed that there are times when I launch the app for the first time, and the app list is successfully fetched, but the items do not get displayed on the UI. Here's my procedure to see this behavior:
- Open app from home screen
- If items are displayed successfully, close the app
- Remove app from recents lists
- Launch app and do procedure again until the items are not displayed anymore.
Out of curiosity, I moved the code to observe the LiveData<List<AppModel>>
to onCreateView
, and the items are now displayed successfully every time the app is launched. Also, the bug only happens API 22, I tested it on API 27 and the bug does not appear. Anyone have an idea why this happens?
Here is the code that has the bug with the items not showing:
1) FavoritesFragment.java (where the list of saved apps are displayed via RecyclerView):
public class FavoritesFragment extends Fragment {
public static final String TAG = FavoritesFragment.class.getSimpleName();
private FaveListAdapter faveListAdapter;
FragmentFavoritesBinding binding;
private List<AppModel> faveList = new ArrayList<>();
@Inject
public ViewModelFactory viewModelFactory;
private FavoritesViewModel viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Injector.getViewModelComponent().inject(this);
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this, viewModelFactory).get(FavoritesViewModel.class);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentFavoritesBinding.inflate(inflater, container, false);
binding.button.setOnClickListener((v ->
navController.navigate(R.id.action_favorites_dest_to_app_list_dest)));
faveListAdapter = new FaveListAdapter(this::launchApp);
faveListAdapter.setAppList(faveList);
faveListAdapter.setOnDeleteItemListener(list -> {
faveList = list;
viewModel.saveFaveApps(faveList).observe(getViewLifecycleOwner(), this::handleSaveStatus);
updateRecyclerView();
});
binding.rvNav.setLayoutManager(new LinearLayoutManager(requireContext()));
binding.rvNav.setAdapter(faveListAdapter);
Log.d(TAG, "onCreateView: done initial RV setup");
updateRecyclerView();
return binding.getRoot();
}
@Override
public void onStart() {
super.onStart();
viewModel.loadFaveAppList().observe(this, list -> {
faveList = list;
faveListAdapter.swapItems(list);
updateRecyclerView();
});
}
private void updateRecyclerView() {
Log.d(TAG, "updateRecyclerView: start");
if(faveList.isEmpty()) {
binding.button.setVisibility(View.VISIBLE);
binding.frameFav.setVisibility(View.GONE);
} else {
binding.button.setVisibility(View.GONE);
binding.frameFav.setVisibility(View.VISIBLE);
}
}
private void launchApp(String packageName) {
// launch selected app
}
private void handleSaveStatus(SaveStatus saveStatus) {
// change UI/navigate to other screens depending on status
}
}
}
2) FavoritesViewModel.java (where I get the list using RxJava2 from a repository object and update the UI via LiveData)
public class FavoritesViewModel extends ViewModel {
private final PreferenceRepository preferenceRepository;
private CompositeDisposable compositeDisposable;
private List<String> favePackageNameList = new ArrayList<>();
@Inject
public FavoritesViewModel(PreferenceRepository preferenceRepository, DataRepository dataRepository) {
this.preferenceRepository = preferenceRepository;
this.dataRepository = dataRepository;
compositeDisposable = new CompositeDisposable();
}
public LiveData<List<AppModel>> loadFaveAppList() {
MutableLiveData<List<AppModel>> listData = new MutableLiveData<>();
compositeDisposable.add(dataRepository.loadFavesFromPrefs()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(listData::setValue, Throwable::printStackTrace));
return listData;
}
public LiveData<SaveStatus> saveFaveApps(List<AppModel> faveList) {
MutableLiveData<SaveStatus> saveStatus = new MutableLiveData<>();
compositeDisposable.add(dataRepository.saveFaveAppListToPrefs(faveList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> saveStatus.setValue(SaveStatus.SAVING))
.subscribe(() -> saveStatus.setValue(SaveStatus.DONE),
error -> {
error.printStackTrace();
saveStatus.setValue(SaveStatus.ERROR);
})
);
return saveStatus;
}
}
3) FavoritesAdapter.java (RecyclerView adapter that implements contextual action bar logic, also uses DiffUtils)
public class FaveListAdapter extends RecyclerView.Adapter<FaveListAdapter.ViewHolder> {
public interface FaveItemClickListener {
void onItemClick(String packageName);
}
public interface DeleteItemListener {
void onDeleteClick(List<AppModel> newAppList);
}
private List<AppModel> appList = new ArrayList<>();
private FaveItemClickListener onFaveItemClickListener;
private DeleteItemListener onDeleteItemListener;
private boolean multiSelect = false;
private List<AppModel> selectedItems = new ArrayList<>();
private ActionMode.Callback actionModeCallbacks = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
multiSelect = true;
menu.add("Delete");
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
for(AppModel app : selectedItems) {
appList.remove(app);
}
if(onDeleteItemListener != null) {
onDeleteItemListener.onDeleteClick(appList);
}
mode.finish();
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
multiSelect = false;
selectedItems.clear();
notifyDataSetChanged();
}
};
public FaveListAdapter(FaveItemClickListener onFaveItemClickListener) {
this.onFaveItemClickListener = onFaveItemClickListener;
}
public void setAppList(List<AppModel> appList) {
this.appList = appList;
notifyDataSetChanged();
}
public void setOnDeleteItemListener(DeleteItemListener onDeleteItemListener) {
this.onDeleteItemListener = onDeleteItemListener;
}
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView appIcon;
private final TextView appLabel;
private final ConstraintLayout itemLayout;
public ViewHolder(@NonNull View itemView) {
super(itemView);
appIcon = itemView.findViewById(R.id.app_icon);
appLabel = itemView.findViewById(R.id.app_label);
itemLayout = itemView.findViewById(R.id.item_layout);
}
private void selectItem(AppModel app) {
if(multiSelect) {
if(selectedItems.contains(app)) {
selectedItems.remove(app);
itemLayout.setBackgroundColor(Color.WHITE);
} else {
selectedItems.add(app);
itemLayout.setBackgroundColor(Color.LTGRAY);
}
}
}
private void bind(AppModel app, int i) {
appIcon.setImageDrawable(app.getLauncherIcon());
appLabel.setText(app.getAppLabel());
if(selectedItems.contains(app)) {
itemLayout.setBackgroundColor(Color.LTGRAY);
} else {
itemLayout.setBackgroundColor(Color.WHITE);
}
this.itemView.setOnClickListener(v ->{
if(multiSelect) {
selectItem(app);
} else {
onFaveItemClickListener.onItemClick(appList.get(i).getPackageName());
}
});
this.itemView.setOnLongClickListener(v -> {
((AppCompatActivity) v.getContext()).startSupportActionMode(actionModeCallbacks);
selectItem(app);
return true;
});
}
}
@NonNull
@Override
public FaveListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fave, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull FaveListAdapter.ViewHolder holder, int position) {
holder.bind(appList.get(position), position);
}
@Override
public int getItemCount() {
return appList.size();
}
public void swapItems(List<AppModel> apps) {
final AppModelDiffCallback diffCallback = new AppModelDiffCallback(this.appList, apps);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
this.appList.clear();
this.appList.addAll(apps);
diffResult.dispatchUpdatesTo(this);
}
}
Solution
Fragment and fragment`s viewLifecycleOwner has different lifecycles. ViewLifecycleOwner subscribes in onCreateView and unsubscribes in onDestroyView. Fragment`s lifecycle subscribes in onCreate and unsubscribes in onDestroy
Move this code in onCreateView()
viewModel.loadFaveAppList().observe(getViewLifecycleOwner, list -> { <-- change this
faveList = list;
faveListAdapter.swapItems(list);
updateRecyclerView();
});
https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb
Answered By - Анатолий Спитченко
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.