3

In this part I try to add one feature in my app and fix one problem, I would like to save the recyclerview position so when click and navigate to details fragment and click back, this should back to the same position, the issue here is when I click back I see the items overlapping, there are about 8 items and it starts from next list position, while I searching for the solution of this issue, I found this answer then I decided to add the dependencies of recyclerview separately implementation "androidx.recyclerview:recyclerview:1.2.1" to use this feature

adapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);

but it doesn't work, the next GIF describe the problem

recyclerview position problem

and this example of the app with this feature

Target

HomeFragment

@AndroidEntryPoint
public class HomeFragment extends Fragment {

    private FragmentHomeBinding binding;
    private PostViewModel postViewModel;
    public static final String TAG = "HomeFragment";
    private PostAdapter adapter;
    private List<Item> itemArrayList;
    private GridLayoutManager titleLayoutManager, gridLayoutManager;
    WrapContentLinearLayoutManager layoutManager;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        binding.homeRecyclerView.addOnItemTouchListener(new RecyclerItemClickListener(requireContext(),
                binding.homeRecyclerView, new RecyclerItemClickListener.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {

                Item item = itemArrayList.get(position);

                if (Objects.requireNonNull
                        (Navigation.findNavController(requireView())
                                .getCurrentDestination()).getId() == R.id.nav_home) {
                    Navigation.findNavController(requireView())
                            .navigate(HomeFragmentDirections.actionNavHomeToDetailsFragment(item));
                }
            }

            @Override
            public void onLongItemClick(View view, int position) {

            }
        }));
    }


    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {


        binding = FragmentHomeBinding.inflate(inflater, container, false);
        setHasOptionsMenu(true);

        postViewModel = new ViewModelProvider(this).get(PostViewModel.class);
        postViewModel.finalURL.setValue(Constants.getBaseUrl() + "?key=" + Constants.getKEY());
        itemArrayList = new ArrayList<>();
        adapter = new PostAdapter(getContext(), itemArrayList, this, postViewModel);

        layoutManager = new WrapContentLinearLayoutManager(requireContext(),
                LinearLayoutManager.VERTICAL, false);
        titleLayoutManager = new GridLayoutManager(getContext(), 2);
        gridLayoutManager = new GridLayoutManager(getContext(), 3);


//        binding.homeRecyclerView.setAdapter(adapter);
        binding.shimmerLayout.setVisibility(View.VISIBLE);
        binding.homeRecyclerView.setVisibility(View.INVISIBLE);

        adapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);


        postViewModel.recyclerViewLayoutMT.observe(getViewLifecycleOwner(), layout -> {
            Log.w(TAG, "getSavedLayout: called");
            switch (layout) {
                case "cardLayout":
                    binding.loadMoreBtn.setVisibility(View.VISIBLE);
                    binding.homeRecyclerView.setLayoutManager(layoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
                    adapter.setViewType(0);

                    break;
                case "cardMagazineLayout":
                    binding.loadMoreBtn.setVisibility(View.VISIBLE);
                    binding.homeRecyclerView.setLayoutManager(layoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
                    adapter.setViewType(1);
                    break;
                case "titleLayout":
                    binding.loadMoreBtn.setVisibility(View.GONE);
                    binding.homeRecyclerView.setLayoutManager(titleLayoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
                    adapter.setViewType(2);
                    break;
                case "gridLayout":
                    binding.loadMoreBtn.setVisibility(View.GONE);
                    binding.homeRecyclerView.setLayoutManager(gridLayoutManager);
                    binding.homeRecyclerView.setAdapter(adapter);
                    adapter.setViewType(3);
            }
        });


        if (Utils.hasNetworkAccess(requireContext())) {

            postViewModel.getPosts();

            postViewModel.postListMutableLiveData.observe(getViewLifecycleOwner(), postList -> {
                itemArrayList.addAll(postList.getItems());
                binding.shimmerLayout.stopShimmer();
                binding.shimmerLayout.setVisibility(View.GONE);
                binding.homeRecyclerView.setVisibility(View.VISIBLE);
                adapter.notifyDataSetChanged();

                Log.e(TAG, "ItemsArrayList :" + itemArrayList.get(0).getTitle());

            });


        } else {

            binding.shimmerLayout.setVisibility(View.VISIBLE);
//            binding.shimmerLayout.startShimmer();


            if (postViewModel.getAllItemsFromDataBase == null) {

                noInternetConnectionLayout();

            } else {
//                Log.e(TAG, "RoomDB Items size :" + itemsDatabase.itemDAO().getAlItems());

                binding.shimmerLayout.stopShimmer();
                binding.shimmerLayout.setVisibility(View.GONE);
                binding.emptyView.setVisibility(View.GONE);
                binding.homeRecyclerView.setVisibility(View.VISIBLE);
                postViewModel.getAllItemsFromDataBase.observe(getViewLifecycleOwner(), items -> {
                    if (items.isEmpty()) {
                        noInternetConnectionLayout();
                    } else {
                        binding.loadMoreBtn.setVisibility(View.GONE);
                        itemArrayList.addAll(items);
                        adapter.notifyDataSetChanged();
                    }
                });

            }
        }

        postViewModel.errorCode.observe(getViewLifecycleOwner(), errorCode -> {
            if (errorCode == 400) {
                Snackbar.make(requireView(), R.string.lastPost, Snackbar.LENGTH_LONG).show();
            } else {
                binding.homeRecyclerView.setVisibility(View.INVISIBLE);
                binding.emptyView.setVisibility(View.VISIBLE);
            }
        });

        binding.loadMoreBtn.setOnClickListener(view -> {
            AlertDialog dialog = Utils.setProgressDialog(requireContext());

            postViewModel.isLoading.observe(getViewLifecycleOwner(), isLoading -> {
                if (isLoading) {
                    dialog.show();
                } else {
                    dialog.dismiss();
                }
            });

            if (Utils.hasNetworkAccess(requireContext())) {
                postViewModel.getPosts();
//                Log.w(TAG, "loadMoreBtn: " + dialog.isShowing());
            } else {
                postViewModel.isLoading.postValue(true);
                postViewModel.getAllItemsFromDataBase.getValue();
                postViewModel.isLoading.postValue(false);
            }

        });

        return binding.getRoot();

    }

    private void noInternetConnectionLayout() {
        binding.shimmerLayout.stopShimmer();
        binding.shimmerLayout.setVisibility(View.GONE);
        binding.homeRecyclerView.setVisibility(View.GONE);
        binding.emptyView.setVisibility(View.VISIBLE);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        itemArrayList.clear();
        binding = null;
    }


    @Override
    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {

        inflater.inflate(R.menu.main, menu);
        super.onCreateOptionsMenu(menu, inflater);

        SearchManager searchManager = (SearchManager) requireContext().getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView = (SearchView) menu.findItem(R.id.app_bar_search).getActionView();
        searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().getComponentName()));
        searchView.setQueryHint(getResources().getString(R.string.searchForPosts));

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String keyword) {
                if (keyword.isEmpty()) {
                    Snackbar.make(requireView(), "please enter keyword to search", Snackbar.LENGTH_SHORT).show();
                }
                if (Utils.hasNetworkAccess(requireContext())) {
                    itemArrayList.clear();
                    postViewModel.getItemsBySearch(keyword);
                    adapter.notifyDataSetChanged();
                } else {
                    postViewModel.getItemsBySearchInDB(keyword);
                    postViewModel.getItemsBySearchMT.observe(getViewLifecycleOwner(), items ->
                            {
                                Log.d(TAG, "onQueryTextSubmit database called");
                                itemArrayList.clear();
                                itemArrayList.addAll(items);
                                adapter.notifyDataSetChanged();
                            }
                    );
                }
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                return false;
            }
        });


        searchView.setOnCloseListener(() -> {

            if (Utils.hasNetworkAccess(requireContext())) {
                Log.d(TAG, "setOnCloseListener: called");
                itemArrayList.clear();
                binding.emptyView.setVisibility(View.GONE);
                binding.homeRecyclerView.setVisibility(View.VISIBLE);
                postViewModel.getPosts();
                adapter.notifyDataSetChanged();
            } else {
                Log.d(TAG, "setOnCloseListener: called");
                binding.emptyView.setVisibility(View.GONE);
                binding.homeRecyclerView.setVisibility(View.VISIBLE);
                postViewModel.getAllItemsFromDataBase.observe(getViewLifecycleOwner(), items ->
                        {
                            itemArrayList.addAll(items);
                            adapter.notifyDataSetChanged();
                        }
                );
            }
            return false;
        });


        postViewModel.searchError.observe(getViewLifecycleOwner(), searchError -> {
            if (searchError) {
                Toast.makeText(requireContext(),
                        "There's no posts with this keyword", Toast.LENGTH_LONG).show();
            }
        });


    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {

        if (item.getItemId() == R.id.change_layout) {
            changeAndSaveLayout();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

The ViewModel

@HiltViewModel
public class PostViewModel extends ViewModel {

    public static final String TAG = "PostViewModel";


    private final com.blogspot.abtallaldigital.data.Repository repository;
    public final MutableLiveData<PostList> postListMutableLiveData = new MutableLiveData<>();
    public final MutableLiveData<String> finalURL = new MutableLiveData<>();
    public final MutableLiveData<String> token = new MutableLiveData<>();
    public final MutableLiveData<String> label = new MutableLiveData<>();
    public final MutableLiveData<Integer> errorCode = new MutableLiveData<>();
    public final MutableLiveData<Boolean> searchError = new MutableLiveData<>();
    public final LiveData<List<Item>> getAllItemsFromDataBase;
    public final MutableLiveData<List<Item>>
            getItemsBySearchMT = new MutableLiveData<>();
    public final MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
    public final MutableLiveData<String> recyclerViewLayoutMT = new MutableLiveData<>();
    private final Utils.DataStoreRepository dataStoreRepository;
    public final MutableLiveData<Integer> currentDestination = new MutableLiveData<>();


//    public MutableLiveData<Boolean> ifAnythingWrongHappened = new MutableLiveData<>();

    @Inject
    public PostViewModel(Repository repository, Utils.DataStoreRepository dataStoreRepository) {
        this.repository = repository;
        getAllItemsFromDataBase = repository.localDataSource.getAllItems();
        this.dataStoreRepository = dataStoreRepository;
        dataStoreRepository.readLayoutFlow
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new FlowableSubscriber<String>() {
                    @Override
                    public void onSubscribe(@NonNull Subscription s) {
                        s.request(Long.MAX_VALUE);
                    }

                    @Override
                    public void onNext(String layout) {
                        if (layout != null) {
                            recyclerViewLayoutMT.setValue(layout);
                        }
                    }

                    @Override
                    public void onError(Throwable t) {
                        Log.e(TAG, "onError: " + t.getMessage());
                        Log.e(TAG, "onError: " + t.getCause());
                    }

                    @Override
                    public void onComplete() {

                    }
                });

        dataStoreRepository.readCurrentDestination
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .firstOrError().subscribeWith(new SingleObserver<Integer>() {
            @Override
            public void onSubscribe(@NonNull Disposable d) {

            }

            @Override
            public void onSuccess(@NonNull Integer destination) {
                currentDestination.setValue(destination);
            }

            @Override
            public void onError(@NonNull Throwable e) {
                Log.e(TAG, "onError: " + e.getMessage());
            }
        });

    }

    public void saveRecyclerViewLayout(String layout) {
        dataStoreRepository.saveRecyclerViewLayout("recyclerViewLayout", layout);
    }

    public void saveCurrentDestination(int currentDestination) {
        dataStoreRepository
                .saveCurrentDestination("CURRENT_DESTINATION", currentDestination);
    }


    //    @Override
//    protected void onCleared() {
//        super.onCleared();
//        postListMutableLiveData.setValue(null);
//        finalURL.setValue(null);
//        token.setValue(null);
//    }

    public void getPosts() {
        Log.e(TAG, finalURL.getValue());

        isLoading.setValue(true);
        repository.remoteDataSource.getPostList(finalURL.getValue())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Response<PostList>>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull Response<PostList> postListResponse) {

                        if (postListResponse.isSuccessful()) {
                            if (postListResponse.body() != null
                                    && postListResponse.body().getNextPageToken() != null) {
                                Log.e(TAG, postListResponse.body().getNextPageToken());
                                token.setValue(postListResponse.body().getNextPageToken());
                                isLoading.setValue(false);
                            }
                            postListMutableLiveData.setValue(postListResponse.body());

                            for (int i = 0; i < postListResponse.body().getItems().size(); i++) {
                                repository.localDataSource.insertItem(postListResponse.body()
                                        .getItems().get(i))
                                        .subscribeOn(Schedulers.io())
                                        .observeOn(AndroidSchedulers.mainThread())
                                        .subscribe(new CompletableObserver() {
                                            @Override
                                            public void onSubscribe(@NonNull Disposable d) {

                                            }

                                            @Override
                                            public void onComplete() {

                                            }

                                            @Override
                                            public void onError(@NonNull Throwable e) {

                                            }
                                        });
                            }

                            finalURL.setValue(finalURL.getValue() + "&pageToken=" + token.getValue());
                        } else {
                            isLoading.setValue(false);
                            errorCode.setValue(postListResponse.code());
                            Log.e(TAG, "onNext: " + postListResponse.code());
                            Log.e(TAG, "onNext: " + postListResponse.errorBody());
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        isLoading.setValue(false);
                        Log.e(TAG, e.getMessage() + e.getCause());
//                        ifAnythingWrongHappened.setValue(true);
                        if (e instanceof HttpException) {
                            errorCode.setValue(((HttpException) e).code());
                        }
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }


    public void getPostListByLabel() {

        isLoading.setValue(true);
        Log.e(TAG, finalURL.getValue());

        repository.remoteDataSource.getPostListByLabel(finalURL.getValue())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Response<com.blogspot.abtallaldigital.pojo.PostList>>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull Response<com.blogspot.abtallaldigital.pojo.PostList> postListResponse) {
                        if (postListResponse.isSuccessful()) {
                            if (postListResponse.body() != null) {
//                                Log.e(TAG, postListResponse.body().getNextPageToken());
                                token.setValue(postListResponse.body().getNextPageToken());
                            }
                            postListMutableLiveData.setValue(postListResponse.body());
                            isLoading.setValue(false);
                            finalURL.postValue(Constants.getBaseUrlPostsByLabel()
                                    + "posts?labels=" + label.getValue() + "&pageToken="
                                    + token.getValue()
                                    + "&key=" + Constants.getKEY());
                        } else {
                            isLoading.setValue(false);
                            errorCode.setValue(postListResponse.code());
                            Log.e(TAG, "onNext: " + postListResponse.code());
                            Log.e(TAG, "onNext: " + postListResponse.errorBody());
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        isLoading.setValue(false);
                        Log.e(TAG, e.getMessage() + e.getCause());
                        if (e instanceof HttpException) {
                            errorCode.setValue(((HttpException) e).code());
                        }
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }

    public void getItemsBySearch(String keyword) {

        isLoading.setValue(true);
        searchError.setValue(false);

        String url = Constants.getBaseUrl() +
                "search?q=" + keyword + "&key=" + Constants.getKEY();

        Log.e(TAG, "getItemsBySearch: " + url);

        repository.remoteDataSource.getPostListBySearch(url)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Response<com.blogspot.abtallaldigital.pojo.PostList>>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull Response<PostList> postListResponse) {

                        if (postListResponse.isSuccessful()) {
                            if (postListResponse.body() != null
                                    && postListResponse.body().getNextPageToken() != null) {
                                Log.e(TAG, postListResponse.body().getNextPageToken());
                                token.setValue(postListResponse.body().getNextPageToken());
                                isLoading.setValue(false);
                            }
                            postListMutableLiveData.setValue(postListResponse.body());
                        } else {
                            isLoading.setValue(false);
                            searchError.setValue(true);
                            Log.e(TAG, "onNext: list is null");
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        isLoading.setValue(false);
                        Log.e(TAG, e.getMessage() + e.getCause());
//                        ifAnythingWrongHappened.setValue(true);
                        if (e instanceof HttpException) {
                            errorCode.setValue(((HttpException) e).code());
                        }
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }

    public void getItemsBySearchInDB(String keyword) {
        Log.d(TAG, "getItemsBySearchInDB: called");
        repository.localDataSource.getItemsBySearch(keyword)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<List<com.blogspot.abtallaldigital.pojo.Item>>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull List<Item> items) {
                        if (items.isEmpty()) {
                            searchError.setValue(true);
                            Log.e(TAG, "onNext: list is empty");
                        } else {
                            getItemsBySearchMT.setValue(items);
                            Log.d(TAG, "onNext: " + items.size());
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        searchError.setValue(true);
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }

    public void insertFavorites(FavoritesEntity favoritesEntity) {
        repository.localDataSource.insertFavorites(favoritesEntity)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new CompletableObserver() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onComplete() {

                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        Log.e(TAG, "onError: " + e.getMessage());
                    }
                });
    }

    public LiveData<List<FavoritesEntity>> getAllFavorites() {
        return repository.localDataSource.getAllFavorites();
    }

    public void deleteFavoritePost(FavoritesEntity favoritesEntity) {
        repository.localDataSource.deleteFavorite(favoritesEntity);
    }

    public void deleteAllFavorites() {
        repository.localDataSource.deleteAllFavorites()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new CompletableObserver() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onComplete() {

                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        Log.e(TAG, "onError: " + e.getMessage());
                    }
                });


    }


}
Dr Mido
  • 2,414
  • 4
  • 32
  • 72

2 Answers2

2

Try initializing the adapter in onCreate instead of onViewCreated Then you can set the adapter on recyclerview in onViewCreated.

Also use try using LiveData Event Wrapper : https://stackoverflow.com/a/51762972/9854554

LiveData Event Wrapper in Java : https://stackoverflow.com/a/56072658

It will prevent livedata observer from getting data again on fragment resume (when we subscribe again on the livedata)

Naresh NK
  • 1,036
  • 1
  • 9
  • 16
  • Thank you, the adapter initializing was in `onCreateView` I moved it in `onCreate` then it's already setting the `setStateRestorationPolicy` in `onViewCreated`, the link you provided using kotlin I using java in this project, can you check my edit? I added the full ViewModel code – Dr Mido Nov 13 '21 at 09:37
  • Is it working as expected after moving in onCreate? – Naresh NK Nov 14 '21 at 03:41
  • no it doesn't work, the app is crashed I got `E/AndroidRuntime: FATAL EXCEPTION: main Process: com.blogspot.abtallaldigital, PID: 3497 java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference at com.blogspot.abtallaldigital.adapters.PostAdapter.getItemCount(PostAdapter.java:1) at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:14)` – Dr Mido Nov 14 '21 at 09:04
  • That's because your item list is still being assigned in onViewcreated or onCreateView. Assign that list before adapter. – Naresh NK Nov 14 '21 at 10:42
  • ok after moving the assigning of list and viewmodel to onCreate the app is running but there's no effect, the same issue – Dr Mido Nov 14 '21 at 11:24
  • I have added stackoverflow answer link for LiveData event wrapper in java. Check the edited answer. Also check your code if you are refreshing your adapter in onResume or onViewCreated, don't do that if the adapter already has items use condition if(adapter.getItemCount() <= 0) than update adapter (in case of onResume/onViewCreated). – Naresh NK Nov 16 '21 at 03:32
0

you are set recycler view data in onViewCreated which called everytime, i recommend viewmodel for better lifecycle handing, use LiveData or StateFlow in viewModel to prove your data integrity