Alvin Dizon
Alvin Dizon

Reputation: 2019

Recyclerview items are sometimes not displayed on app launch

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:

  1. Open app from home screen
  2. If items are displayed successfully, close the app
  3. Remove app from recents lists
  4. 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);
    }
}

Upvotes: 0

Views: 453

Answers (1)

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

Upvotes: 1

Related Questions