You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

旋转设备时恢复RecyclerView状态的解决方案咨询

解决RecyclerView无限滚动在配置变化和页面返回时的状态恢复问题

首先咱们得明确几个需要恢复的核心状态:已加载的数据列表、当前请求的页码、RecyclerView的滚动位置,还有无限滚动监听器的运行状态(比如是否正在加载下一页、是否已经加载完所有数据)。下面分步骤拆解解决方案:

1. 用ViewModel保存核心数据(推荐方案)

onSaveInstanceState的Bundle容量有限,而且配置变化时Activity会重建,用ViewModel可以让数据在重建过程中完整保留,避免重复发起网络请求,这也是现代Android开发的标准做法。

实现ViewModel:

class MovieViewModel : ViewModel() {
    // 保存已加载的电影列表
    val movieList = mutableListOf<Movie>()
    // 当前请求的页码
    var currentPage = 1
    // 是否正在加载数据(防止重复请求)
    var isLoading = false
    // 是否已经加载完所有数据
    var hasLoadedAll = false

    // 封装网络请求逻辑,替代原有的AsyncTask
    fun loadMovies(page: Int, onSuccess: (List<Movie>) -> Unit) {
        if (isLoading || hasLoadedAll) return
        isLoading = true
        
        // 用Coroutines处理异步请求(替代已废弃的AsyncTask)
        viewModelScope.launch(Dispatchers.IO) {
            val fetchedMovies = fetchMoviesFromNetwork(page)
            withContext(Dispatchers.Main) {
                movieList.addAll(fetchedMovies)
                onSuccess(fetchedMovies)
                currentPage = page
                isLoading = false
                // 如果返回空数据,说明没有更多内容了
                hasLoadedAll = fetchedMovies.isEmpty()
            }
        }
    }

    // 实际网络请求逻辑,替换成你自己的接口调用
    private suspend fun fetchMoviesFromNetwork(page: Int): List<Movie> {
        // 这里写你的网络请求代码,比如Retrofit调用
    }
}

在Activity中使用ViewModel:

class MovieActivity : AppCompatActivity() {
    private lateinit var viewModel: MovieViewModel
    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: MovieAdapter
    private lateinit var scrollListener: EndlessRecyclerViewScrollListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movie)

        // 初始化ViewModel,配置变化时会自动保留实例
        viewModel = ViewModelProvider(this)[MovieViewModel::class.java]

        // 初始化RecyclerView和Adapter
        recyclerView = findViewById(R.id.recyclerView)
        adapter = MovieAdapter(viewModel.movieList)
        recyclerView.adapter = adapter
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager

        // 恢复RecyclerView的滚动位置
        savedInstanceState?.getParcelable<Parcelable>("RECYCLER_LAYOUT")?.let {
            layoutManager.onRestoreInstanceState(it)
        }

        // 初始化无限滚动监听器,并恢复之前的状态
        scrollListener = object : EndlessRecyclerViewScrollListener(layoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.loadMovies(page) {
                    adapter.notifyDataSetChanged()
                }
            }
        }
        // 从ViewModel中恢复监听器的状态
        scrollListener.currentPage = viewModel.currentPage
        scrollListener.isLoading = viewModel.isLoading
        scrollListener.hasLoadedAllItems = viewModel.hasLoadedAll
        recyclerView.addOnScrollListener(scrollListener)

        // 如果ViewModel中没有数据,才发起首次请求
        if (viewModel.movieList.isEmpty()) {
            viewModel.loadMovies(1) {
                adapter.notifyDataSetChanged()
            }
        } else {
            // 已有数据,直接刷新Adapter即可
            adapter.notifyDataSetChanged()
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // 保存RecyclerView的滚动位置
        recyclerView.layoutManager?.let {
            outState.putParcelable("RECYCLER_LAYOUT", it.onSaveInstanceState())
        }
    }

    // 从其他页面返回时,ViewModel已保留数据,无需额外操作,若有需要可在onResume中刷新Adapter
    override fun onResume() {
        super.onResume()
        adapter.notifyDataSetChanged()
    }
}

2. 调整EndlessRecyclerViewScrollListener,让状态可修改

你的滚动监听器需要暴露核心状态变量,方便重建后恢复:

abstract class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager) : RecyclerView.OnScrollListener() {
    // 暴露状态变量,允许外部修改
    var currentPage = 1
    var isLoading = false
    var hasLoadedAllItems = false
    // 距离底部多少个item时触发加载
    private val visibleThreshold = 5

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val totalItemCount = layoutManager.itemCount
        val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()

        if (!isLoading && !hasLoadedAllItems && lastVisibleItemPosition >= totalItemCount - visibleThreshold) {
            isLoading = true
            currentPage++
            onLoadMore(currentPage, totalItemCount, recyclerView)
        }
    }

    abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)

    // 可选:重置状态的方法,用于下拉刷新场景
    fun resetState() {
        currentPage = 1
        isLoading = false
        hasLoadedAllItems = false
    }
}

3. 替代AsyncTask(可选但强烈推荐)

AsyncTask已经被官方废弃,且在配置变化时容易出现内存泄漏、无效回调等问题。用ViewModel+Coroutines的组合,既能保证异步请求的生命周期安全,又能避免重复请求的问题。如果坚持使用AsyncTask,需要注意:

  • 在Activity的onDestroy中调用task.cancel(true)取消任务,避免内存泄漏
  • 不要直接在AsyncTask中引用Activity控件,改用弱引用或通过ViewModel传递结果
  • 重建后先检查是否已有数据,避免重复发起请求

4. 从其他页面返回的状态恢复

因为ViewModel会在页面跳转时保留数据,所以从其他页面返回时,只要确保Adapter的数据是最新的即可(如果用LiveData配合ViewModel,数据变化会自动通知Adapter更新,无需手动调用notifyDataSetChanged)。

核心总结

  • 用ViewModel持久化数据和请求状态,避免配置变化时丢失
  • 保存并恢复RecyclerView的滚动位置
  • 让EndlessScrollListener的核心状态可外部修改,方便重建后恢复
  • 避免重复发起网络请求,提升用户体验和性能

内容的提问来源于stack exchange,提问作者Mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:19:05