前提阅读
主要功能
先从Room中读取数据,当数据为空时,利用UserRemoteMediator从网络获取数据,将获取的数据写入数据库,并更新UI
adapter
PageLoadStateAdapter
java
public class PageLoadStateAdapter extends LoadStateAdapter<PageLoadStateAdapter.LoadStateViewHolder> {
private static final String TAG = "PageLoadStateAdapter";
private static final long NO_MORE_DELAY_MS = 400L;
private static final long NO_MORE_FADE_OUT_DURATION_MS = 250L;
private static final float NO_MORE_TRANSLATION_Y_DP = 12F;
private final View.OnClickListener mRetryCallback;
private final boolean isHeader;
public PageLoadStateAdapter(View.OnClickListener retryCallback, boolean isHeader) {
mRetryCallback = retryCallback;
this.isHeader = isHeader;
}
@NonNull
@Override
public LoadStateViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @NonNull LoadState loadState) {
String prefix = isHeader ? "[Header]" : "[Footer]";
Log.d(TAG, "onCreateViewHolder: " + prefix + " loadState = " + loadState);
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.load_state_item, parent, false);
return new LoadStateViewHolder(view, mRetryCallback, isHeader);
}
@Override
public void onBindViewHolder(@NonNull LoadStateViewHolder holder, @NonNull LoadState loadState) {
String prefix = isHeader ? "[Header]" : "[Footer]";
Log.d(TAG, "onBindViewHolder: " + prefix + " loadState = " + loadState);
holder.bind(loadState);
}
@Override
public int getStateViewType(@NonNull LoadState loadState) {
return super.getStateViewType(loadState);
}
@Override
public boolean displayLoadStateAsItem(@NonNull LoadState loadState) {
boolean isLoading = loadState instanceof LoadState.Loading;
boolean isError = loadState instanceof LoadState.Error;
boolean isNotLoading = loadState instanceof LoadState.NotLoading;
boolean isNotLoadingTrue = loadState instanceof LoadState.NotLoading && loadState.getEndOfPaginationReached();
if (isInterceptNotLoadingTrue) {
return isLoading || isError || isNotLoadingTrue;
} else {
return isLoading || isError || isNotLoading;
}
}
public static class LoadStateViewHolder extends RecyclerView.ViewHolder {
private final LinearLayout mLoadingView;
private final LinearLayout mErrorView;
private final LinearLayout mNoMoreView;
private final Button mRetry;
private final boolean isHeader;
private final Runnable mShowNoMoreRunnable;
private final Runnable mHideNoMoreRunnable;
LoadStateViewHolder(@NonNull View itemView, @NonNull View.OnClickListener retryCallback, boolean isHeader) {
super(itemView);
this.isHeader = isHeader;
mLoadingView = itemView.findViewById(R.id.header_footer_loading_view);
mErrorView = itemView.findViewById(R.id.header_footer_error_view);
mNoMoreView = itemView.findViewById(R.id.header_footer_empty_view);
mRetry = itemView.findViewById(R.id.header_footer_error_retry);
mShowNoMoreRunnable = this::showNoMore;
mHideNoMoreRunnable = this::hideNoMore;
mRetry.setOnClickListener(retryCallback);
}
void bind(LoadState loadState) {
itemView.removeCallbacks(mShowNoMoreRunnable);
itemView.removeCallbacks(mHideNoMoreRunnable);
mNoMoreView.animate().cancel();
mNoMoreView.setAlpha(1f);
mNoMoreView.setTranslationY(0f);
if (loadState instanceof LoadState.Loading) {
itemView.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.VISIBLE);
mErrorView.setVisibility(View.GONE);
mNoMoreView.setVisibility(View.GONE);
return;
}
if (loadState instanceof LoadState.Error) {
itemView.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.GONE);
mErrorView.setVisibility(View.VISIBLE);
mNoMoreView.setVisibility(View.GONE);
return;
}
if (loadState instanceof LoadState.NotLoading && loadState.getEndOfPaginationReached()) {
itemView.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.VISIBLE);
mErrorView.setVisibility(View.GONE);
mNoMoreView.setVisibility(View.GONE);
itemView.postDelayed(mShowNoMoreRunnable, NO_MORE_DELAY_MS);
// showNoMore();
return;
}
hideAll();
}
private void showNoMore() {
itemView.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
mNoMoreView.animate().cancel();
mNoMoreView.setAlpha(1f);
mNoMoreView.setTranslationY(0f);
mNoMoreView.setVisibility(View.VISIBLE);
itemView.postDelayed(mHideNoMoreRunnable, NO_MORE_DELAY_MS);
}
private void hideNoMore() {
float translationY = mNoMoreView.getResources().getDisplayMetrics().density * NO_MORE_TRANSLATION_Y_DP;
mNoMoreView.animate()
.alpha(0f)
.translationY(isHeader ? -translationY : translationY)
.setDuration(NO_MORE_FADE_OUT_DURATION_MS)
.withEndAction(() -> {
mNoMoreView.setAlpha(1f);
mNoMoreView.setTranslationY(0f);
hideAll();
})
.start();
}
private void hideAll() {
itemView.setVisibility(View.GONE);
mLoadingView.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
mNoMoreView.animate().cancel();
mNoMoreView.setAlpha(1f);
mNoMoreView.setTranslationY(0f);
mNoMoreView.setVisibility(View.GONE);
}
}
}
UserAdapter
java
public class UserAdapter extends PagingDataAdapter<User, UserAdapter.UserViewHolder> {
private static final String TAG = "UserAdapter";
// 比较规则(用于判断数据是否相同)
private static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(User oldItem, User newItem) {
return Objects.equals(oldItem.id, newItem.id);
}
@Override
public boolean areContentsTheSame(User oldItem, User newItem) {
return Objects.equals(oldItem.name, newItem.name) && Objects.equals(oldItem.num, newItem.num);
}
};
public UserAdapter() {
super(DIFF_CALLBACK);
}
@NonNull
@Override
public UserAdapter.UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.d(TAG, "onCreateViewHolder: ");
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull UserAdapter.UserViewHolder holder, int position) {
User user = getItem(position);
Log.d(TAG, "onBindViewHolder: position = " + position + ", user = " + user);
if (user != null) {
holder.bind(user);
}
}
public static class UserViewHolder extends RecyclerView.ViewHolder {
TextView tvName, tvNum;
UserViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
tvNum = itemView.findViewById(R.id.tv_num);
}
void bind(User user) {
tvName.setText(user.name);
tvNum.setText(String.valueOf(user.num));
}
}
}
bean
BackendService
java
public class BackendService {
private static final String TAG = "BackendService";
private final List<User> mUsers;
// todo:测试加载失败的UI显示
private static final boolean testErrorFirstIn = false;
// todo:测试Header失败的UI显示
private static final boolean testErrorHeaderLoad = false;
public static final int LOAD_SIZE = 20;
public static final int MAX_SIZE = 200;
public static final int FIRST_PAGE = 1;
public static final int LAST_PAGE = MAX_SIZE / LOAD_SIZE;
public enum LoadType {
NONE,
Header,
Footer,
HeaderAndFooter;
}
public static final LoadType isNeedHeaderAndFooter = LoadType.HeaderAndFooter;
public BackendService() {
mUsers = new ArrayList<>();
for (int i = 0; i < MAX_SIZE; i++) {
mUsers.add(new User(UUID.randomUUID().toString(), "王", i));
mUsers.add(new User(UUID.randomUUID().toString(), "刘", i));
}
}
// 模拟从网络加载数据
public Single<BaseResponse<UserResponse>> loadUsersFromNetwork(String query, int page) {
return Single.fromCallable(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
return new BaseResponse<>(); // 返回空响应
}
Log.d(TAG, "loadUsersFromNetwork: query = " + query + ", page = " + page);
if (testErrorFirstIn) { // todo:模拟首次进入网络加载失败
return new BaseResponse<>(502, "network error", null);
}
if (testErrorHeaderLoad && page == 2) { // todo:默认加载第2页时,模拟Header加载失败
return new BaseResponse<>(502, "network error", null);
}
UserResponse response = new UserResponse();
// 计算当前页的起始和结束范围
int start = (page - 1) * LOAD_SIZE;
int end = page * LOAD_SIZE;
Log.d(TAG, "loadUsersFromNetwork: start = " + start + ", end = " + end);
/*for (int i = start; i <= end; i++) {
User user = mUsers.get(i);
if (query.equals(user.name)) {
response.users.add(user);
}
}*/
for (User user : mUsers) {
if (query.equals(user.name) && user.num >= start && user.num < end) {
response.users.add(user);
}
}
if (page == FIRST_PAGE) {
response.preKey = null;
response.nextKey = page + 1;
} else if (page == LAST_PAGE) {
response.preKey = page - 1;
response.nextKey = null;
} else {
response.preKey = page - 1;
response.nextKey = page + 1;
}
Log.d(TAG, "loadUsersFromNetwork: response.preKey = " + response.preKey + ", currentPage = " + page + ", response.nextKey = " + response.nextKey);
return new BaseResponse<>(200, "success", response);
});
}
}
BaseResponse
java
public class BaseResponse<T> {
public int code = 200;
public String message;
public T data;
public BaseResponse() {
}
public BaseResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
@NonNull
@Override
public String toString() {
return "BaseResponse{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
RemoteKey
java
@Entity(tableName = "remote_keys")
public class RemoteKey {
@NonNull
@PrimaryKey
public String query;
public Integer prevKey;
public Integer nextKey;
public RemoteKey(@NonNull String query, Integer prevKey, Integer nextKey) {
this.query = query;
this.prevKey = prevKey;
this.nextKey = nextKey;
}
}
User
java
@Entity(tableName = "users")
public class User {
@NonNull
@PrimaryKey
public String id;
public String name;
public int num;
public User(@NonNull String id, String name, int num) {
this.id = id;
this.name = name;
this.num = num;
}
@NonNull
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", num='" + num + '\'' +
'}';
}
}
UserResponse
java
public class UserResponse {
public List<User> users = new ArrayList<>();
public Integer preKey;
public Integer nextKey;
@Override
public String toString() {
return "UserResponse{" +
"users=" + users +
", preKey=" + preKey +
", nextKey=" + nextKey +
'}';
}
}
dao
AppDatabase
java
@Database(entities = {User.class, RemoteKey.class}, version = 6)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
/** 设为 true 模拟首次安装(清空数据库 + SharedPreferences),测试完改回 false */
public static final boolean TEST_FIRST_LAUNCH = true;
public abstract UserDao userDao();
public abstract RemoteKeyDao remoteKeyDao();
public static AppDatabase getInstance() {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
Context context = MyApplication.getContext();
if (TEST_FIRST_LAUNCH) {
context.deleteDatabase("app_database");
context.getSharedPreferences("app_cache", Context.MODE_PRIVATE)
.edit().clear().apply();
}
INSTANCE = Room.databaseBuilder(
context,
AppDatabase.class,
"app_database"
).fallbackToDestructiveMigration().build();
}
}
}
return INSTANCE;
}
}
RemoteKeyDao
java
@Dao
public interface RemoteKeyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertOrReplace(RemoteKey remoteKey);
@Query("SELECT * FROM remote_keys WHERE query like :query")
RemoteKey getRemoteKeyByQuery(String query);
@Query("DELETE FROM remote_keys WHERE query = :query")
void deleteByQuery(String query);
}
UserDao
java
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUsers(List<User> users);
@Query("DELETE FROM users where name like :name")
int deleteUser(String name);
@Query("SELECT * FROM users WHERE name like :name ORDER BY num ASC")
PagingSource<Integer, User> getUsers(String name);
@Query("SELECT * FROM users WHERE name like :name AND num >= :startNum AND num < :endNum ORDER BY num ASC")
List<User> getUsersInRange(String name, int startNum, int endNum);
@Query("SELECT * FROM users where name like :name")
List<User> getUsersAfterInsert(String name);
}
data
UserDbPagingSource
java
public class UserDbPagingSource extends RxPagingSource<Integer, User> {
private static final String TAG = "UserDbPagingSource";
private static final String TABLE_USERS = "users";
private final UserDao userDao;
private final String query;
private final int initialPage;
private InvalidationTracker.Observer observer;
public UserDbPagingSource(@NonNull String query, int initialPage) {
this.userDao = AppDatabase.getInstance().userDao();
this.query = query;
this.initialPage = initialPage;
// 订阅 users 表的失效通知;RemoteMediator 写库后会触发,从而让本 PagingSource 重建并重新查询
this.observer = new InvalidationTracker.Observer(new String[]{TABLE_USERS}) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
Log.d(TAG, "onInvalidated: data come back, reload from tables=" + tables);
invalidate();
}
};
AppDatabase.getInstance().getInvalidationTracker().addObserver(observer);
registerInvalidatedCallback(new Function0<Unit>() {
@Override
public Unit invoke() {
AppDatabase.getInstance().getInvalidationTracker().removeObserver(observer);
return Unit.INSTANCE;
}
});
}
@Nullable
@Override
public Integer getRefreshKey(@NonNull PagingState<Integer, User> state) {
Log.d(TAG, "getRefreshKey: state= " + state);
Integer anchor = state.getAnchorPosition();
if (anchor == null) {
return initialPage;
}
LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchor);
if (anchorPage == null) {
return initialPage;
}
Integer prevKey = anchorPage.getPrevKey();
if (prevKey != null) {
return prevKey + 1;
}
Integer nextKey = anchorPage.getNextKey();
if (nextKey != null) {
return nextKey - 1;
}
return initialPage;
}
@NonNull
@Override
public Single<LoadResult<Integer, User>> loadSingle(@NonNull LoadParams<Integer> params) {
if (params.getKey() == null) {
return Single.just(new LoadResult.Error<>(new Throwable("params.getKey() == null")));
}
int page = params.getKey();
int start = (page - 1) * BackendService.LOAD_SIZE;
int end = page * BackendService.LOAD_SIZE;
Log.d(TAG, "loadSingle: page=" + page + ", range=[" + start + "," + end + ")");
return Single.fromCallable(() -> userDao.getUsersInRange(query, start, end))
.subscribeOn(Schedulers.io())
.map(list -> toPage(list, page))
.onErrorReturn(LoadResult.Error::new);
}
private LoadResult<Integer, User> toPage(List<User> list, int page) {
if (list == null) {
return new LoadResult.Error<>(new Throwable("list == null"));
}
if (list.isEmpty()) {
Log.d(TAG, "toPage: list.isEmpty() load from network");
return new LoadResult.Page<>(list, null, null);
}
Integer prev = (page > 1) ? page - 1 : null;
Integer next = (list.size() == BackendService.LOAD_SIZE) ? page + 1 : null;
Log.d(TAG, "toPage: loadSuccess form db page=" + page + ", size=" + list.size()
+ ", prev=" + prev + ", next=" + next);
return new LoadResult.Page<>(list, prev, next);
}
@Override
public boolean getJumpingSupported() {
return true;
}
}
UserPagingSource
java
public class UserPagingSource extends RxPagingSource<Integer, User> {
private final static String TAG = "UserPagingSource";
private BackendService mBackendService;
private String mQuery;
private final int initialPage;
UserPagingSource(@NonNull String query, int initialPage) {
mBackendService = new BackendService();
mQuery = query;
this.initialPage = initialPage;
}
// 确定刷新时从哪个位置开始加载数据,以保持用户的浏览位置
@Nullable
@Override
public Integer getRefreshKey(@NonNull PagingState<Integer, User> state) {
// 获取锚点位置(当前可见的第一个位置)
Log.d(TAG, "getRefreshKey: state= " + state);
Integer anchorPosition = state.getAnchorPosition();
if (anchorPosition == null) {
return null; // 没有可见位置,无法确定刷新键
}
int targetPage = anchorPosition / BackendService.LOAD_SIZE + 1; // 避免快速滑动时,state.closestPageToPosition(anchorPosition)返回1导致每次重头循环加载
Log.d(TAG, "getRefreshKey: calculated targetPage= " + targetPage);
return targetPage;
// 找到距离锚点位置最近的页面
/*LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
Log.d(TAG, "getRefreshKey: anchorPage= " + anchorPage);
if (anchorPage == null) {
return null; // 没有找到页面
}
// 获取前一页的键
Integer prevKey = anchorPage.getPrevKey();
Log.d(TAG, "getRefreshKey: prevKey= " + prevKey);
if (prevKey != null) {
return prevKey + 1; // 返回下一页的键
}
// 获取下一页的键
Integer nextKey = anchorPage.getNextKey();
Log.d(TAG, "getRefreshKey: nextKey= " + nextKey);
if (nextKey != null) {
return nextKey - 1; // 返回前一页的键
}
// 都没有键,返回 null(初始页)
return null;*/
}
@NonNull
@Override
public Single<LoadResult<Integer, User>> loadSingle(@NonNull LoadParams<Integer> loadParams) {
if (loadParams.getKey() == null) {
return Single.just(new LoadResult.Error<>(new Throwable("params.getKey() == null")));
}
int page = loadParams.getKey();
Log.d(TAG, "loadSingle: page = " + page);
// 返回数据页
return mBackendService.loadUsersFromNetwork(mQuery, page)
.subscribeOn(Schedulers.io())
.map(response -> toLoadResult(response, page))
.onErrorReturn(throwable -> {
Log.w(TAG, "加载失败: " + throwable.getMessage());
return new LoadResult.Error<>(throwable);
});
}
private LoadResult<Integer, User> toLoadResult(
@NonNull BaseResponse<UserResponse> baseResponse, int currentPage) {
Log.d(TAG, "toLoadResult: baseResponse = " + baseResponse);
switch (baseResponse.code) {
case 502:
return new LoadResult.Error<>(new Throwable(baseResponse.message));
case 200:
UserResponse userResponse = baseResponse.data;
if (userResponse.users == null) {
return new LoadResult.Error<>(new Throwable("数据加载失败"));
}
return handleSuccess(userResponse, currentPage);
}
return new LoadResult.Error<>(new Throwable("unknown error"));
}
private LoadResult.Page<Integer, User> handleSuccess(@NonNull UserResponse userResponse, int currentPage) {
int itemsBefore = COUNT_UNDEFINED, itemsAfter = COUNT_UNDEFINED;
if (getJumpingSupported()) {
if (userResponse.preKey != null) {
itemsBefore = userResponse.preKey * BackendService.LOAD_SIZE;
}
if (userResponse.nextKey != null) {
itemsAfter = (LAST_PAGE - userResponse.nextKey + 1) * BackendService.LOAD_SIZE;
}
}
Log.d(TAG, "toLoadResult: itemsBefore = " + itemsBefore);
Log.d(TAG, "toLoadResult: itemsAfter = " + itemsAfter);
return new LoadResult.Page<>(
userResponse.users, // 加载到的数据列表
userResponse.preKey, // 加载当前页之前的数据(向上滚动),需要使用的页码
userResponse.nextKey, // 加载当前页之后的数据(向下滚动),需要使用的页码
itemsBefore, // 排在当前 data 列表第一个元素之前的数据总条数,需要返回正确数量才能快速滑动
itemsAfter);
}
@Override
public boolean getJumpingSupported() {
return isNeedHeaderAndFooter == LoadType.NONE;
}
}
UserRemoteMediator
java
@ExperimentalPagingApi
public class UserRemoteMediator extends RxRemoteMediator<Integer, User> {
private static final String TAG = "UserRemoteMediator";
private static final String PREF_LAST_UPDATED = "users_last_updated";
private static final long CACHE_TIMEOUT_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
private UserDao userDao;
private RemoteKeyDao remoteKeyDao;
private BackendService mBackendService;
private String mQuery;
private int mPage;
public UserRemoteMediator(@NonNull String query, int page) {
mBackendService = new BackendService();
userDao = AppDatabase.getInstance().userDao();
remoteKeyDao = AppDatabase.getInstance().remoteKeyDao();
mQuery = query;
mPage = page;
}
@NonNull
@Override
public Single<InitializeAction> initializeSingle() {
SharedPreferences prefs = MyApplication.getContext().getSharedPreferences("app_cache", Context.MODE_PRIVATE);
long lastUpdated = prefs.getLong(PREF_LAST_UPDATED, 0L);
long timeSpan = System.currentTimeMillis() - lastUpdated;
if (timeSpan > CACHE_TIMEOUT_MS) {
Log.d(TAG, "initialize: LAUNCH_INITIAL_REFRESH");
return Single.just(InitializeAction.LAUNCH_INITIAL_REFRESH);
} else {
Log.d(TAG, "initialize: SKIP_INITIAL_REFRESH");
return Single.just(InitializeAction.SKIP_INITIAL_REFRESH);
}
}
@NonNull
@Override
public Single<MediatorResult> loadSingle(@NonNull LoadType loadType, @NonNull PagingState<Integer, User> pagingState) {
Log.d(TAG, "loadSingle: begin loadType = " + loadType);
return Single.<MediatorResult>defer(() -> {
if (isPrintLog) {
Integer anchor = pagingState.getAnchorPosition();
User anchorUser = anchor == null ? null : pagingState.closestItemToPosition(anchor);
int loadedCount = 0;
for (PagingSource.LoadResult.Page<Integer, User> loadedPage : pagingState.getPages()) {
loadedCount += loadedPage.getData().size();
}
Log.d(TAG, "pagingState: loadType=" + loadType
+ ", anchor=" + anchor
+ ", anchorUser=" + (anchorUser == null ? "null" : anchorUser.num)
+ ", pageCount=" + pagingState.getPages().size()
+ ", loadedCount=" + loadedCount);
}
int page = -1;
RemoteKey key;
switch (loadType) {
case REFRESH:
if (BackendService.isNeedHeaderAndFooter == BackendService.LoadType.HeaderAndFooter) {
// REFRESH 一次性并发加载 mPage-1 / mPage / mPage+1 三页,单次事务写库 → 只触发 1 次 invalidate
return loadRefreshThreePages();
} else {
page = mPage; // 这里会多次触发 invalidate,HeaderAndFooter时不使用
break;
}
case PREPEND:
// anchor=null 说明 UI 尚未绑定任何 item,本次 PREPEND 只是 REFRESH 后触发的
if (pagingState.getAnchorPosition() == null && BackendService.isNeedHeaderAndFooter == BackendService.LoadType.HeaderAndFooter) {
Log.d(TAG, "loadSingle: skip PREPEND, anchor=null (initial prefetch cascade)");
return Single.just(new MediatorResult.Success(false));
}
key = remoteKeyDao.getRemoteKeyByQuery(mQuery);
if (key == null || key.prevKey == null) {
Log.d(TAG, "loadSingle: end loadType = " + loadType + ", prevKey == null");
return Single.just(new MediatorResult.Success(true));
}
page = key.prevKey;
break;
case APPEND:
// 同 PREPEND,anchor=null 时跳过 APPEND
if (pagingState.getAnchorPosition() == null && BackendService.isNeedHeaderAndFooter == BackendService.LoadType.HeaderAndFooter) {
Log.d(TAG, "loadSingle: skip APPEND, anchor=null (initial prefetch cascade)");
return Single.just(new MediatorResult.Success(false));
}
key = remoteKeyDao.getRemoteKeyByQuery(mQuery);
if (key == null || key.nextKey == null) {
Log.d(TAG, "loadSingle: end loadType = " + loadType + ", nextKey == null");
return Single.just(new MediatorResult.Success(true));
}
page = key.nextKey;
break;
}
Log.d(TAG, "loadUsersFromNetwork page = " + page);
return mBackendService.loadUsersFromNetwork(mQuery, page)
.flatMap(response -> toLoadResult(response, loadType))
.onErrorResumeNext(e -> {
Log.w(TAG, "加载失败: " + e.getMessage());
return Single.<MediatorResult>just(new MediatorResult.Error(e));
});
}).subscribeOn(Schedulers.io());
}
/**
* REFRESH 时一次拉 3 页(mPage-1 / mPage / mPage+1),并发请求、统一写库、只触发一次 invalidation。
*/
private Single<MediatorResult> loadRefreshThreePages() {
boolean hasPrev = mPage > BackendService.FIRST_PAGE;
boolean hasNext = mPage < BackendService.LAST_PAGE;
Single<BaseResponse<UserResponse>> centerSingle =
mBackendService.loadUsersFromNetwork(mQuery, mPage);
Single<BaseResponse<UserResponse>> prevSingle = hasPrev
? mBackendService.loadUsersFromNetwork(mQuery, mPage - 1)
: Single.just((BaseResponse<UserResponse>) null);
Single<BaseResponse<UserResponse>> nextSingle = hasNext
? mBackendService.loadUsersFromNetwork(mQuery, mPage + 1)
: Single.just((BaseResponse<UserResponse>) null);
Log.d(TAG, "loadUsersFromNetwork REFRESH pages = "
+ (hasPrev ? (mPage - 1) : "-") + "/" + mPage + "/" + (hasNext ? (mPage + 1) : "-"));
return Single.zip(prevSingle, centerSingle, nextSingle, RefreshTriple::new)
.flatMap(this::handleRefreshSuccess)
.onErrorResumeNext(e -> {
Log.w(TAG, "REFRESH 加载失败: " + e.getMessage());
return Single.<MediatorResult>just(new MediatorResult.Error(e));
});
}
private Single<MediatorResult> handleRefreshSuccess(RefreshTriple triple) {
BaseResponse<UserResponse>[] responses = new BaseResponse[]{triple.prev, triple.center, triple.next};
for (BaseResponse<UserResponse> r : responses) {
if (r == null) continue;
if (r.code != 200 || r.data == null || r.data.users == null) {
return Single.just(new MediatorResult.Error(
new Throwable("REFRESH 数据加载失败 code=" + r.code + ", msg=" + r.message)));
}
}
Log.d(TAG, "toLoadResult: end loadType = REFRESH, prev=" + triple.prev
+ ", center=" + triple.center + ", next=" + triple.next);
UserResponse centerData = triple.center.data;
// RemoteKey 串联:prevKey 取最左页的 preKey,nextKey 取最右页的 nextKey
Integer remotePrev = (triple.prev != null) ? triple.prev.data.preKey : centerData.preKey;
Integer remoteNext = (triple.next != null) ? triple.next.data.nextKey : centerData.nextKey;
// 合并 3 页 users 为单个 list,一次性 insert
java.util.List<User> allUsers = new java.util.ArrayList<>();
if (triple.prev != null) {
allUsers.addAll(triple.prev.data.users);
}
allUsers.addAll(centerData.users);
if (triple.next != null) {
allUsers.addAll(triple.next.data.users);
}
AppDatabase.getInstance().runInTransaction(() -> {
userDao.deleteUser(mQuery);
remoteKeyDao.deleteByQuery(mQuery);
remoteKeyDao.insertOrReplace(new RemoteKey(mQuery, remotePrev, remoteNext));
userDao.insertUsers(allUsers);
});
SharedPreferences prefs = MyApplication.getContext().getSharedPreferences("app_cache", Context.MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_UPDATED, System.currentTimeMillis()).apply();
boolean endOfPaginationReached = remoteNext == null;
return Single.just(new MediatorResult.Success(endOfPaginationReached));
}
private static class RefreshTriple {
final BaseResponse<UserResponse> prev;
final BaseResponse<UserResponse> center;
final BaseResponse<UserResponse> next;
RefreshTriple(BaseResponse<UserResponse> prev,
BaseResponse<UserResponse> center,
BaseResponse<UserResponse> next) {
this.prev = prev;
this.center = center;
this.next = next;
}
}
private Single<MediatorResult> toLoadResult(
@NonNull BaseResponse<UserResponse> baseResponse, LoadType loadType) {
Log.d(TAG, "toLoadResult: end loadType = " + loadType + ", baseResponse = " + baseResponse);
switch (baseResponse.code) {
case 502:
return Single.just(new MediatorResult.Error(new Throwable(baseResponse.message)));
case 200:
UserResponse userResponse = baseResponse.data;
if (userResponse.users == null) {
return Single.just(new MediatorResult.Error(new Throwable("数据加载失败")));
}
return handleSuccess(userResponse, loadType);
}
return Single.just(new MediatorResult.Error(new Throwable("unknown error")));
}
private Single<MediatorResult> handleSuccess(@NonNull UserResponse userResponse, LoadType loadType) {
AppDatabase.getInstance().runInTransaction(() -> {
if (loadType == LoadType.REFRESH) { // 刷新时删除旧数据
userDao.deleteUser(mQuery);
remoteKeyDao.deleteByQuery(mQuery);
remoteKeyDao.insertOrReplace(new RemoteKey(mQuery, userResponse.preKey, userResponse.nextKey));
} else if (loadType == LoadType.PREPEND) {
// 只更新 prevKey,保留原有的 nextKey
RemoteKey existing = remoteKeyDao.getRemoteKeyByQuery(mQuery);
if (existing != null) {
remoteKeyDao.insertOrReplace(new RemoteKey(mQuery, userResponse.preKey, existing.nextKey));
}
} else if (loadType == LoadType.APPEND) {
// 只更新 nextKey,保留原有的 prevKey
RemoteKey existing = remoteKeyDao.getRemoteKeyByQuery(mQuery);
if (existing != null) {
remoteKeyDao.insertOrReplace(new RemoteKey(mQuery, existing.prevKey, userResponse.nextKey));
}
}
// 插入数据库
userDao.insertUsers(userResponse.users);
//Log.d(TAG, "handleSuccess: after " + loadType + " insert, User = " + userDao.getUsersAfterInsert(mQuery));
});
SharedPreferences prefs = MyApplication.getContext().getSharedPreferences("app_cache", Context.MODE_PRIVATE);
prefs.edit().putLong(PREF_LAST_UPDATED, System.currentTimeMillis()).apply();
boolean endOfPaginationReached;
if (loadType == LoadType.PREPEND) {
endOfPaginationReached = userResponse.preKey == null;
} else {
endOfPaginationReached = userResponse.nextKey == null;
}
return Single.just(new MediatorResult.Success(endOfPaginationReached));
}
}
UserViewModel
java
public class UserViewModel extends ViewModel {
private static final String TAG = "UserViewModel";
private MediatorLiveData<PagingData<User>> userLiveData; // userLiveData 观察 currentPagingSource
private LiveData<PagingData<User>> currentPagingSource; // currentPagingSource返回分页数据
private String currentQuery;
private final CoroutineScope mViewModelScope;
private static final boolean isUseRoom = true;
public UserViewModel() {
mViewModelScope = ViewModelKt.getViewModelScope(this);
userLiveData = new MediatorLiveData<>();
}
@OptIn(markerClass = ExperimentalPagingApi.class)
public void loadUser(String query) {
if (query == null || query.isEmpty()) {
return; // 空查询不加载
}
currentQuery = query;
// 移除旧的 Paging 数据源(避免内存泄漏)
if (currentPagingSource != null) {
userLiveData.removeSource(currentPagingSource);
}
PagingConfig pagingConfig = new PagingConfig(
LOAD_SIZE, // 每次加载的数据条数
isNeedHeaderAndFooter == LoadType.NONE ? LOAD_SIZE : 1, // 距离列表末尾还有LOAD_SIZE条数据时开始预加载,显示Header或Footer时应该为1
isNeedHeaderAndFooter == LoadType.NONE, // 加载期间是否使用空白占位符,需要支持快速滑动和快速跳转时为true,显示Header或Footer时应该为false?(滑到BackendService.LOAD_SIZE显示Header或Footer等待加载)
LOAD_SIZE, // 首次加载的数据量(对应 RemoteMediator REFRESH 一次取 3 页,保证锚点不贴边)
MAX_SIZE, // 内存中最多缓存多少条数据
isNeedHeaderAndFooter == LoadType.NONE ? LOAD_SIZE * 2 : COUNT_UNDEFINED // 快速滑动超过多少个,取消增量加载,直接刷新,显示Header或Footer时不要这个参数
);
int page = isNeedHeaderAndFooter == BackendService.LoadType.HeaderAndFooter ? LAST_PAGE / 2 : FIRST_PAGE;
Pager<Integer, User> pager;
UserRemoteMediator userRemoteMediator;
Function0<PagingSource<Integer, User>> function0;
if (isUseRoom) {
userRemoteMediator = new UserRemoteMediator(query, page);
if (BackendService.isNeedHeaderAndFooter != LoadType.NONE) {
// REFRESH 一次性并发加载 mPage-1 / mPage / mPage+1 三页,单次事务写库 → 只触发 1 次 invalidate
function0 = () -> new UserDbPagingSource(query, page);
} else {
function0 = () -> AppDatabase.getInstance().userDao().getUsers(query); // 使用Room生成的会导致连续加载问题
}
} else {
function0 = () -> new UserPagingSource(query, page);
userRemoteMediator = null;
}
pager = new Pager<>(
pagingConfig,
page,
userRemoteMediator,
function0);
currentPagingSource = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), mViewModelScope); // 若不使用cachedIn,旋转时会重复订阅,导致报错
userLiveData.addSource(currentPagingSource, pagingData ->
userLiveData.setValue(pagingData) // 转发数据给观察者
);
}
public String getCurrentQuery() {
return currentQuery;
}
public LiveData<PagingData<User>> getUserLiveData() {
return userLiveData;
}
}
MainActivity
java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private RecyclerView recyclerView;
private UserAdapter adapter;
private UserViewModel viewModel;
private StateManager mStateManager;
private PageLoadStateAdapter mHeaderAdapter;
private PageLoadStateAdapter mFooterAdapter;
private float mLastY;
private boolean isFooterShow = false;
private boolean isHeaderShow = false;
private boolean mUserHasScrolled = false;
public static boolean isInterceptNotLoadingTrue = true; // 如果不拦截会一直加载到第一页,paging3问题?暂时无法解决
public static boolean isPrintLog = true;
private final CompositeDisposable mDisposables = new CompositeDisposable();
private static final LoadStates DEFAULT_LOAD_STATES = new LoadStates(
new LoadState.NotLoading(false),
new LoadState.NotLoading(false),
new LoadState.NotLoading(false)
);
private CombinedLoadStates mLastCombinedStateForPrint = null;
private LoadState mLastCombinedPrependState = new LoadState.NotLoading(false);
private LoadState mLastCombinedRefreshState = new LoadState.NotLoading(false);
private LoadState mLastCombinedAppendState = new LoadState.NotLoading(false);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: ");
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
initView();
initData();
initListener();
viewModel.loadUser("王");
// todo:测试快速滑动是否会跳过增量加载
/*new Handler().postDelayed(new Runnable() {
@Override
public void run() {
int itemCount = adapter.getItemCount();
Log.d(TAG, "itemCount =" + itemCount);
recyclerView.scrollToPosition(90); // 会同时加载相邻page的数据
}
}, 5000);*/
// todo:测试数据为空的UI显示
//viewModel.loadUser("李");
}
@Override
protected void onDestroy() {
mDisposables.clear();
super.onDestroy();
}
@SuppressLint("ClickableViewAccessibility")
private void initListener() {
viewModel.getUserLiveData().observe(this, pagingData ->
adapter.submitData(getLifecycle(), pagingData));
adapter.addLoadStateListener(loadStates -> {
printLoadStates(loadStates);
//printAdapterSnapshot();
handleCombinedPrependState(loadStates.getPrepend());
handleCombinedRefreshState(loadStates.getRefresh());
handleCombinedAppendState(loadStates.getAppend());
return Unit.INSTANCE;
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
mUserHasScrolled = true;
}
}
});
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouch: ACTION_DOWN");
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float deltaY = event.getY() - mLastY;
Log.d(TAG, "onTouch: ACTION_MOVE deltaY = " + deltaY);
switch (isNeedHeaderAndFooter) {
case Header:
case Footer:
if (isAppendEndReached()
&& !isFooterShow
&& (Math.abs(deltaY) > 100)) {
Log.d(TAG, "onTouch: mFooterAdapter.setLoadState");
isFooterShow = true;
showHeaderOrFooterNoMore(mFooterAdapter);
}
break;
case HeaderAndFooter:
if (deltaY < -100 && !isFooterShow) {
isFooterShow = true;
if (isAppendEndReached()) {
Log.d(TAG, "onTouch: isAppendEndReached");
showHeaderOrFooterNoMore(mFooterAdapter);
} /*else {
Log.d(TAG, "onTouch: !isAppendEndReached");
showHeaderOrFooterLoading(mFooterAdapter);
}*/
}
if (deltaY > 100 && !isHeaderShow) {
isHeaderShow = true;
if (isPrependEndReached()) {
Log.d(TAG, "onTouch: isPrependEndReached");
showHeaderOrFooterNoMore(mHeaderAdapter);
} /*else {
Log.d(TAG, "onTouch: !isAppendEndReached");
showHeaderOrFooterLoading(mHeaderAdapter);
}*/
}
break;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onTouch: ACTION_UP or ACTION_CANCEL");
if (isFooterShow) {
//mFooterAdapter.setLoadState(new LoadState.NotLoading(false));
isFooterShow = false;
}
if (isHeaderShow) {
//mHeaderAdapter.setLoadState(new LoadState.NotLoading(false));
isHeaderShow = false;
}
break;
}
return false;
}
});
}
private void showHeaderOrFooterNoMore(PageLoadStateAdapter adapter) {
adapter.setLoadState(new LoadState.NotLoading(false));
adapter.setLoadState(new LoadState.NotLoading(true));
}
private void showHeaderOrFooterLoading(PageLoadStateAdapter adapter) {
adapter.setLoadState(LoadState.Loading.INSTANCE);
}
private boolean isPrependEndReached() {
return mLastCombinedPrependState instanceof LoadState.NotLoading
&& mLastCombinedPrependState.getEndOfPaginationReached();
}
private boolean isAppendEndReached() {
return mLastCombinedAppendState instanceof LoadState.NotLoading
&& mLastCombinedAppendState.getEndOfPaginationReached();
}
private void handleCombinedRefreshState(LoadState freshState) {
if (mLastCombinedRefreshState != null && mLastCombinedRefreshState.equals(freshState)) {
return;
}
mLastCombinedRefreshState = freshState;
int itemCount = adapter.getItemCount();
Log.d(TAG, "handleCombinedRefreshState: freshState = " + freshState + ", itemCount = " + itemCount);
if (freshState instanceof LoadState.Loading) {
mStateManager.setState(LOADING);
} else if (freshState instanceof LoadState.Error) {
Throwable error = ((LoadState.Error) freshState).getError();
mStateManager.setState(ERROR);
mStateManager.setErrorMsg(error.getMessage());
} else if (freshState instanceof LoadState.NotLoading) {
if (itemCount == 0) {
mStateManager.setState(EMPTY);
} else {
mStateManager.setState(CONTENT);
}
}
}
private void handleCombinedPrependState(LoadState prependState) {
if (mLastCombinedPrependState != null && mLastCombinedPrependState.equals(prependState)) {
return;
}
Log.d(TAG, "handleCombinedPrependState: prependState = " + prependState);
mLastCombinedPrependState = prependState;
if (isInterceptNotLoadingTrue) {
if (mHeaderAdapter == null) {
return;
}
if (prependState instanceof LoadState.NotLoading && prependState.getEndOfPaginationReached()) { // 拦截NotLoading(true),否则导致每次隐藏再重新显示最后一项时,都会显示mNoMoreView
mHeaderAdapter.setLoadState(new LoadState.NotLoading(false));
// Header 的 Loading View 在 ConcatAdapter 中占据 position 0
// 它一直存在,当最后一页数据(0-19)被 prepend 插入时,这个 Loading View 仍然在顶部。
// Paging 内部做 DiffUtil 计算时,Loading View 的存在导致了 position 偏移错乱,RecyclerView 无法正确维持滚动位置,表现为列表跳到顶部显示 0-19 的数据。
return;
}
mHeaderAdapter.setLoadState(prependState);
}
}
private void handleCombinedAppendState(LoadState appendState) {
if (mLastCombinedAppendState != null && mLastCombinedAppendState.equals(appendState)) {
return;
}
Log.d(TAG, "handleCombinedAppendState: appendState = " + appendState);
mLastCombinedAppendState = appendState;
if (isInterceptNotLoadingTrue) {
if (mFooterAdapter == null) {
return;
}
if (appendState instanceof LoadState.NotLoading && appendState.getEndOfPaginationReached()) { // 拦截NotLoading(true),否则导致每次隐藏再重新显示最后一项时,都会显示mNoMoreView
mFooterAdapter.setLoadState(new LoadState.NotLoading(false)); // 先隐藏 Loading
return;
}
mFooterAdapter.setLoadState(appendState);
}
}
private void printAdapterSnapshot() {
if (!isPrintLog) {
return;
}
ItemSnapshotList<User> snapshot = adapter.snapshot();
StringBuilder sb = new StringBuilder("snapshot size=" + snapshot.size() + " [");
for (int i = 0; i < snapshot.size(); i++) {
User u = snapshot.get(i);
sb.append(u == null ? "null" : u.num);
if (i < snapshot.size() - 1) sb.append(",");
}
sb.append("]");
Log.d(TAG, sb.toString());
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
int first = lm != null ? lm.findFirstVisibleItemPosition() : -1;
int last = lm != null ? lm.findLastVisibleItemPosition() : -1;
Log.d(TAG, "itemCount=" + adapter.getItemCount() + ", visible=[" + first + "," + last + "]");
}
/**
* refresh 加载过程
*
* @param loadStates
*/
private void printLoadStates(CombinedLoadStates loadStates) {
StringBuilder log = new StringBuilder("CombinedLoadStates changed:");
if (mLastCombinedStateForPrint == null) {
appendLine(log, "combined", "prepend null→" + formatShort(loadStates.getPrepend())
+ ", refresh null→" + formatShort(loadStates.getRefresh())
+ ", append null→" + formatShort(loadStates.getAppend()));
} else {
appendLine(log, "combined", buildChangeLine(mLastCombinedStateForPrint, loadStates));
}
if (log.length() > "CombinedLoadStates changed:".length()) {
Log.d(TAG, "*******************");
Log.d(TAG, log.toString());
Log.d(TAG, "------------------------------------------------------");
}
mLastCombinedStateForPrint = loadStates;
}
private String buildChangeLine(CombinedLoadStates oldStates, CombinedLoadStates newStates) {
StringBuilder sb = new StringBuilder();
appendIfChanged(sb, "prepend", oldStates.getPrepend(), newStates.getPrepend());
appendIfChanged(sb, "refresh", oldStates.getRefresh(), newStates.getRefresh());
appendIfChanged(sb, "append", oldStates.getAppend(), newStates.getAppend());
return sb.toString();
}
private void appendLine(StringBuilder log, String label, String content) {
if (!content.isEmpty()) log.append("\n").append(label).append(":").append(content);
}
private void appendIfChanged(StringBuilder sb, String name, LoadState oldState, LoadState newState) {
if (oldState.equals(newState)) return;
if (sb.length() > 0) sb.append(", ");
sb.append(name).append(" ").append(formatTransition(oldState, newState));
}
private String formatTransition(LoadState oldState, LoadState newState) {
boolean bothNotLoading = oldState instanceof LoadState.NotLoading && newState instanceof LoadState.NotLoading;
if (bothNotLoading) {
return "NotLoading(" + oldState.getEndOfPaginationReached() + ")→NotLoading(" + newState.getEndOfPaginationReached() + ")";
}
return formatShort(oldState) + "→" + formatShort(newState);
}
private String formatShort(LoadState state) {
if (state instanceof LoadState.Loading) return "Loading";
if (state instanceof LoadState.NotLoading) return "NotLoading";
if (state instanceof LoadState.Error) return "Error";
return state.toString();
}
private void initData() {
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
adapter = new UserAdapter();
mHeaderAdapter = new PageLoadStateAdapter(v -> adapter.retry(), true);
mFooterAdapter = new PageLoadStateAdapter(v -> adapter.retry(), false);
switch (isNeedHeaderAndFooter) {
case Header:
linearLayoutManager.setReverseLayout(true); // 使用mFooterAdapter
case Footer:
if (isInterceptNotLoadingTrue) {
recyclerView.setAdapter(new ConcatAdapter(adapter, mFooterAdapter));
} else {
recyclerView.setAdapter(adapter.withLoadStateFooter(mFooterAdapter));
}
break;
case HeaderAndFooter:
// 这里不调用withLoadStateHeaderAndFooter,手动管理LoadState
if (isInterceptNotLoadingTrue) {
recyclerView.setAdapter(new ConcatAdapter(mHeaderAdapter, adapter, mFooterAdapter));
} else {
recyclerView.setAdapter(adapter.withLoadStateHeaderAndFooter(mHeaderAdapter, mFooterAdapter));
}
break;
default:
recyclerView.setAdapter(adapter);
break;
}
}
private void initView() {
recyclerView = findViewById(R.id.page_list_rv);
mStateManager = new StateManager.Builder(recyclerView)
.setLoadingView(R.layout.state_loading)
.setEmptyView(R.layout.state_empty)
.setErrorView(R.layout.state_error)
.setOnRetryClickListener(() -> adapter.retry())
.build();
}
}
MyApplication
java
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
StateManager
java
public class StateManager {
private static final String TAG = "StateManager";
public enum State {
LOADING, EMPTY, ERROR, CONTENT, NONE
}
private State currentState = State.NONE;
private final View contentView;
private final View loadingView;
private final View emptyView;
private final View errorView;
private View retryButton;
private TextView errorMsg;
private OnRetryClickListener onRetryClickListener;
private boolean animate = true;
private int animationDuration = 200;
private StateManager(Builder builder) {
Log.d(TAG, "StateManager: ");
this.contentView = builder.contentView;
this.loadingView = builder.loadingView;
this.emptyView = builder.emptyView;
this.errorView = builder.errorView;
this.animate = builder.isNeedAnimate;
this.animationDuration = builder.animationDuration;
this.onRetryClickListener = builder.onRetryClickListener;
initErrorView();
hideAllViews();
}
private void initErrorView() {
if (errorView != null) {
retryButton = errorView.findViewById(R.id.btn_retry);
if (retryButton != null) {
retryButton.setOnClickListener(v -> {
if (onRetryClickListener != null) {
onRetryClickListener.onRetry();
}
});
}
errorMsg = errorView.findViewById(R.id.tv_error_msg);
}
}
public void setState(State state) {
Log.d(TAG, "currentState = " + currentState + ", state = " + state);
if (currentState == state) return;
currentState = state;
hideAllViews();
switch (state) {
case LOADING:
showView(loadingView);
break;
case EMPTY:
showView(emptyView);
break;
case ERROR:
showView(errorView);
break;
case CONTENT:
showView(contentView);
break;
}
}
public void setErrorMsg(String text) {
if (errorView != null) {
errorMsg.setText(text);
}
}
private void hideAllViews() {
hideView(contentView);
hideView(loadingView);
hideView(emptyView);
hideView(errorView);
}
private void showView(View view) {
if (view == null) return;
if (animate) {
view.setAlpha(0f);
view.setVisibility(View.VISIBLE);
view.animate().alpha(1f).setDuration(animationDuration).start();
} else {
view.setVisibility(View.VISIBLE);
view.setAlpha(1f);
}
}
private void hideView(View view) {
if (view == null) return;
if (animate) {
view.animate().alpha(0f).setDuration(animationDuration)
.withEndAction(() -> view.setVisibility(View.GONE)).start();
} else {
view.setVisibility(View.GONE);
view.setAlpha(0f);
}
}
public interface OnRetryClickListener {
void onRetry();
}
// ==================== Builder 类 ====================
public static class Builder {
private ViewGroup contentParent;
private View contentView;
private View loadingView;
private View emptyView;
private View errorView;
private OnRetryClickListener onRetryClickListener;
private boolean isNeedAnimate = true;
private int animationDuration = 200;
public Builder(View contentView) {
this.contentView = contentView;
contentParent = (ViewGroup) contentView.getParent();
}
public Builder setLoadingView(int layoutResId) {
this.loadingView = LayoutInflater.from(contentParent.getContext())
.inflate(layoutResId, contentParent, false);
contentParent.addView(loadingView);
return this;
}
public Builder setEmptyView(int layoutResId) {
this.emptyView = LayoutInflater.from(contentParent.getContext())
.inflate(layoutResId, contentParent, false);
contentParent.addView(emptyView);
return this;
}
public Builder setErrorView(int layoutResId) {
this.errorView = LayoutInflater.from(contentParent.getContext())
.inflate(layoutResId, contentParent, false);
contentParent.addView(errorView);
return this;
}
public Builder setOnRetryClickListener(OnRetryClickListener listener) {
this.onRetryClickListener = listener;
return this;
}
public Builder setNeedAnimate(boolean isNeedAnimate) {
this.isNeedAnimate = isNeedAnimate;
return this;
}
public Builder setAnimationDuration(int duration) {
this.animationDuration = duration;
return this;
}
public StateManager build() {
if (contentView == null) {
throw new IllegalStateException("ContentView must be set");
}
return new StateManager(this);
}
}
}
xml
activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/page_list_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/page_list_rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
item_user.xml
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="left"
android:layout_marginTop="25dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_gravity="center"
android:text="测试用户A"
android:textSize="30sp" />
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="50dp"
android:text="20"
android:textSize="30sp" />
</LinearLayout>
load_state_item
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header_footer_loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:layout_width="30dp"
android:layout_height="30dp"
android:indeterminateBehavior="repeat" />
</LinearLayout>
<LinearLayout
android:id="@+id/header_footer_error_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载失败"
android:textSize="20sp" />
<Button
android:id="@+id/header_footer_error_retry"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginLeft="20dp"
android:text="重试"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/header_footer_empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无更多数据"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
state_empty.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/page_empty_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="数据为空" />
</LinearLayout>
state_error.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/page_error_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_error_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载失败信息"
android:textSize="30sp" />
<Button
android:id="@+id/btn_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重新加载"
android:layout_marginTop="20dp"
android:textSize="20sp" />
</LinearLayout>
state_loading.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/page_loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/page_loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateBehavior="repeat" />
<TextView
android:id="@+id/page_loading_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:textSize="30sp"
android:text="加载中...." />
</LinearLayout>