blob: 9f1d4dd51cc0597282340d1d4b63ce21917da36d [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.paging;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
/**
* {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting paged data from
* {@link PagedList}s in a {@link RecyclerView}.
* <p>
* This class is a convenience wrapper around {@link AsyncPagedListDiffer} that implements common
* default behavior for item counting, and listening to PagedList update callbacks.
* <p>
* While using a LiveData&lt;PagedList> is an easy way to provide data to the adapter, it isn't
* required - you can use {@link #submitList(PagedList)} when new lists are available.
* <p>
* PagedListAdapter listens to PagedList loading callbacks as pages are loaded, and uses DiffUtil on
* a background thread to compute fine grained updates as new PagedLists are received.
* <p>
* Handles both the internal paging of the list as more data is loaded, and updates in the form of
* new PagedLists.
* <p>
* A complete usage pattern with Room would look like this:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
* }
*
* class MyViewModel extends ViewModel {
* public final LiveData&lt;PagedList&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = new LivePagedListBuilder&lt;>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
*
* class MyActivity extends AppCompatActivity {
* {@literal @}Override
* public void onCreate(Bundle savedState) {
* super.onCreate(savedState);
* MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
* RecyclerView recyclerView = findViewById(R.id.user_list);
* UserAdapter&lt;User> adapter = new UserAdapter();
* viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
* recyclerView.setAdapter(adapter);
* }
* }
*
* class UserAdapter extends PagedListAdapter&lt;User, UserViewHolder> {
* public UserAdapter() {
* super(DIFF_CALLBACK);
* }
* {@literal @}Override
* public void onBindViewHolder(UserViewHolder holder, int position) {
* User user = getItem(position);
* if (user != null) {
* holder.bindTo(user);
* } else {
* // Null defines a placeholder item - PagedListAdapter will automatically invalidate
* // this row when the actual object is loaded from the database
* holder.clear();
* }
* }
* public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK =
* new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // User properties may have changed if reloaded from the DB, but ID is fixed
* return oldUser.getId() == newUser.getId();
* }
* {@literal @}Override
* public boolean areContentsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // NOTE: if you use equals, your object must properly override Object#equals()
* // Incorrectly returning false here will result in too many animations.
* return oldUser.equals(newUser);
* }
* }
* }</pre>
*
* Advanced users that wish for more control over adapter behavior, or to provide a specific base
* class should refer to {@link AsyncPagedListDiffer}, which provides the mapping from paging
* events to adapter-friendly callbacks.
*
* @param <T> Type of the PagedLists this Adapter will receive.
* @param <VH> A class that extends ViewHolder that will be used by the adapter.
*/
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncPagedListDiffer<T> mDiffer;
private final AsyncPagedListDiffer.PagedListListener<T> mListener =
new AsyncPagedListDiffer.PagedListListener<T>() {
@Override
public void onCurrentListChanged(
@Nullable PagedList<T> previousList, @Nullable PagedList<T> currentList) {
PagedListAdapter.this.onCurrentListChanged(currentList);
PagedListAdapter.this.onCurrentListChanged(previousList, currentList);
}
};
private final PagedList.LoadStateListener mLoadStateListener =
new PagedList.LoadStateListener() {
@Override
public void onLoadStateChanged(@NonNull PagedList.LoadType type,
@NonNull PagedList.LoadState state, @Nullable Throwable error) {
PagedListAdapter.this.onLoadStateChanged(type, state, error);
}
};
/**
* Creates a PagedListAdapter with default threading and
* {@link androidx.recyclerview.widget.ListUpdateCallback}.
*
* Convenience for {@link #PagedListAdapter(AsyncDifferConfig)}, which uses default threading
* behavior.
*
* @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
* compare items in the list.
*/
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.addPagedListListener(mListener);
mDiffer.addLoadStateListener(mLoadStateListener);
}
protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addPagedListListener(mListener);
mDiffer.addLoadStateListener(mLoadStateListener);
}
/**
* Set the new list to be displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param pagedList The new list to be displayed.
*/
public void submitList(@Nullable PagedList<T> pagedList) {
mDiffer.submitList(pagedList);
}
/**
* Set the new list to be displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
* <p>
* The commit callback can be used to know when the PagedList is committed, but note that it
* may not be executed. If PagedList B is submitted immediately after PagedList A, and is
* committed directly, the callback associated with PagedList A will not be run.
*
* @param pagedList The new list to be displayed.
* @param commitCallback Optional runnable that is executed when the PagedList is committed, if
* it is committed.
*/
public void submitList(@Nullable PagedList<T> pagedList,
@Nullable final Runnable commitCallback) {
mDiffer.submitList(pagedList, commitCallback);
}
@Nullable
protected T getItem(int position) {
return mDiffer.getItem(position);
}
@Override
public int getItemCount() {
return mDiffer.getItemCount();
}
/**
* Returns the PagedList currently being displayed by the Adapter.
* <p>
* This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
* because a diff is computed asynchronously between the new list and the current list before
* updating the currentList value. May be null if no PagedList is being presented.
*
* @return The list currently being displayed.
*
* @see #onCurrentListChanged(PagedList, PagedList)
*/
@Nullable
public PagedList<T> getCurrentList() {
return mDiffer.getCurrentList();
}
/**
* Called when the current PagedList is updated.
* <p>
* This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't
* needed (such as when the first list is passed, or the list is cleared). In either case,
* PagedListAdapter will simply call
* {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}.
* <p>
* This method will <em>not</em>be called when the Adapter switches from presenting a PagedList
* to a snapshot version of the PagedList during a diff. This means you cannot observe each
* PagedList via this method.
*
* @deprecated Use the two argument variant instead:
* {@link #onCurrentListChanged(PagedList, PagedList)}
*
* @param currentList new PagedList being displayed, may be null.
*
* @see #getCurrentList()
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
}
/**
* Called when the current PagedList is updated.
* <p>
* This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't
* needed (such as when the first list is passed, or the list is cleared). In either case,
* PagedListAdapter will simply call
* {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}.
* <p>
* This method will <em>not</em>be called when the Adapter switches from presenting a PagedList
* to a snapshot version of the PagedList during a diff. This means you cannot observe each
* PagedList via this method.
*
* @param previousList PagedList that was previously displayed, may be null.
* @param currentList new PagedList being displayed, may be null.
*
* @see #getCurrentList()
*/
public void onCurrentListChanged(
@Nullable PagedList<T> previousList, @Nullable PagedList<T> currentList) {
}
/**
* Called when the LoadState for a particular type of load (START, END, REFRESH) has
* changed.
* <p>
* REFRESH events can be used to drive a {@code SwipeRefreshLayout}, or START/END events
* can be used to drive loading spinner items in the Adapter.
*
* @param type Type of load - START, END, or REFRESH.
* @param state State of load - IDLE, LOADING, DONE, ERROR, or RETRYABLE_ERROR
* @param error Error, if in an error state, null otherwise.
*/
public void onLoadStateChanged(@NonNull PagedList.LoadType type,
@NonNull PagedList.LoadState state, @Nullable Throwable error) {
}
/**
* Add a LoadStateListener to observe the loading state of the current PagedList.
*
* As new PagedLists are submitted and displayed, the listener will be notified to reflect
* current REFRESH, START, and END states.
*
* @param listener Listener to receive updates.
*
* @see #removeLoadStateListener(PagedList.LoadStateListener)
*/
public void addLoadStateListener(PagedList.LoadStateListener listener) {
mDiffer.addLoadStateListener(listener);
}
/**
* Remove a previously registered LoadStateListener.
*
* @param listener Previously registered listener.
* @see #addLoadStateListener(PagedList.LoadStateListener)
*/
public void removeLoadStateListener(PagedList.LoadStateListener listener) {
mDiffer.removeLoadStateListListener(listener);
}
}