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
and this example of the app with this feature
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());
}
});
}
}
CodePudding user response:
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
It will prevent livedata observer from getting data again on fragment resume (when we subscribe again on the livedata)
CodePudding user response:
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