JetPack——Paging3+Room

前提阅读

paging3
Room

主要功能

先从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>
相关推荐
JAVA面经实录9172 小时前
操作系统(面试全覆盖)
java·计算机网络·面试
编程的一拳超人2 小时前
Maven 国内高速镜像推荐(按速度排序)
java·maven
云烟成雨TD3 小时前
Spring AI 1.x 系列【61】Spring AI 2.0 升级指南
java·人工智能·spring
lulu12165440784 小时前
OpenRouter Fusion 多模型融合架构深度拆解:预算级模型组团打平 Fable 5,多模型协作才是 AGI 的正确打开方式?
java·人工智能·架构·ai编程·agi
雨辰AI4 小时前
生产级实测:SpringBoot3 + 达梦数据库接口从 200ms 优化至 20ms 完整调优指南
java·数据库·spring boot·后端·政务
(Charon)4 小时前
【C++ 面试高频:内存管理、RAII 和智能指针详解】
java·开发语言·word
凡人叶枫4 小时前
Effective C++ 条款39:明智而审慎地使用 private 继承
java·数据库·c++·嵌入式开发
轻刀快马5 小时前
跨越软硬件的共鸣(二):从 Cache 写策略看 Redis 与 DB 的一致性博弈
java·开发语言·redis·计算机组成原理
折哥的程序人生 · 物流技术专研5 小时前
Java 23 种设计模式:从踩坑到精通 | 装饰器模式 —— 比继承更灵活的扩展方式,你用过吗?
java·装饰器模式·java面试·结构型模式·java设计模式·javaio·从踩坑到精通