Android 官方架构组件 Paging:分页库的设计美学

阅读 4347
收藏 280
2018-07-06
原文链接:mp.weixin.qq.com

本文由却把清梅嗅投稿作者博客:

https://www.jianshu.com/u/df76f81fe3ff

前言

我是一个崇尚 开源 的Android开发者,在过去的一段时间里,我研究了Github上的一些优秀的开源库,这些库源码中那些 天马行空设计思想 令我沉醉其中。

在我职业生涯的 伊始,我没有接触过 技术大牛, 但是 阅读源码 可以让我零距离碰撞 全球行业最顶尖工程师们 的思想,我渐渐爱上了 源码阅读

在感叹这些 棒极了 的设计方式时,我也尝试去 模仿 他们的代码风格。后来朋友问我代码中为什么有这么多 设计模式 时,我才发现,单例代理工厂装饰Builder ,甚至更多,当初这些书上怎么也捋不清的设计模式,现在的我正在潜移默化使用它们,这不是 夸张,我在写这些代码时,它们似乎就应该这么用。

今年年初,我尝试开源了一个 灵活可高度定制 的Android图片选择框架 RxImagePicker 。这个库获得了部分认可,当然意见和建议也接踵而来,我很快认识到了自己目前能力的不足—— 通过 组合 的方式 将多个优秀的库封装在一起 ,并不是就意味着真正拥有了 组织架构 的能力,而自己对于架构的掌握能力,目前还有很多不足之处。

我意识到自己的不足,于是我积极寻找 更多优秀的架构,试图通过 源码 学习更多API之外的一些东西: 编程思想架构设计

很快,我找到了一个很优秀的库,Paging—— 它同样做到了 业务层与UI层之间 的隔离,并且,它的设计更为 优秀

Paging Libray

在不久前的Google 2018 I/O大会上,Google正式推出了AndroidJetpack  ——这是一套组件、工具和指导,可以帮助开发者构建出色的 Android 应用,AndroidJetpack 隆重推出了一个新的分页组件:Paging Library

我尝试研究了Paging Library,并分享给大家,本文的目标是阐述:

  • 1.了解并如何使用 Paging

  • 2.知道 Paging 中每个类的 职责,并了解掌握其 原理

  • 3.站在设计者的角度,彻底搞懂 Paging设计思想

本文不是 Paging API 使用代码的展示,但通过本文 彻底搞懂 它的原理之后,API的使用也只是 顺手拈来

它是什么,怎么用?

一句话概述: Paging 可以使开发者更轻松在 RecyclerView分页加载数据

1.官方文档

官方文档 永远是最接近 正确核心理念 的参考资料 —— 在不久之后,本文可能会因为框架本身API的迭代更新而 毫无意义,但官方文档不会,即使在最恶劣的风暴中,它依然是最可靠的 指明灯

https://developer.android.com/topic/libraries/architecture/paging/

其次,一个好的Demo能够起到重要的启发作用, 这里我推荐这个Sample:

https://github.com/googlesamples/android-sunflower

因为刚刚发布的原因,目前Paging的中文教程 比较匮乏,许多资料的查阅可能需要开发者 自备梯子

2.分页效果

在使用之前,我们需要搞明白的是,目前Android设备中比较主流的两种 分页模式,用我的语言概述,大概是:

  • 传统的 上拉加载更多 分页效果

  • 无限滚动 分页效果

从语义上来讲,我的描述有点不太直观,不了解的读者估计会很迷糊。

举个例子,传统的 上拉加载更多 分页效果,应该类似 淘宝APP 这种,滑到底部,再上拉显示footer,才会加载数据:

无限滚动 分页效果,应该像是 京东APP 这样,如果我们慢慢滑动,当滑动了一定量的数据(这个阈值一般是数据总数的某个百分比)时,会自动请求加载下一页的数据,如果我们继续滑动,到达一定量的数据时,它会继续加载下一页数据,直到加载完所有数据——在用户看来,就好像是一次就加载出所有商品一样:

很明显,无限滚动 分页效果带来的用户体验更好,不仅是京东,包括 知乎 等其它APP,所采用的分页加载方式都是 无限滚动 的模式,而 Paging 也正是以无限滚动 的分页模式而设计的库。

3.Sample展示

我写了一个Paging的sample,它最终的效果是这样:

sample_paging

项目结构图如下,这可以帮你尽快了解sample的结构:

我把这个sample的源码托管在了我的github上,你可以通过 点我查看源码 。

4.使用Paging

现在你已经对 Paging 的功能有了一定的了解,我们可以开始尝试使用它了。

请注意,本小节旨在简单阐述Paging入门使用,读者不应该困惑于Kotlin语法或者Room库的使用——你只要能看懂基本流程就好了。

因此,我 更建议 读者 点击进入github,并将Sample代码拉下来阅读,仅仅是阅读—— 相比Kotlin语法和Room的API使用,理解代码的流程 更为重要。

① 在Module下的build.gradle中添加以下依赖:

def room_version = "1.1.0"
    def paging_version = "1.0.0"
    def lifecycle_version = "1.1.1"

    //Paging的依赖
    implementation "android.arch.paging:runtime:$paging_version"
    //Paging对RxJava2的原生支持
    implementation "android.arch.paging:rxjava2:1.0.0-rc1"

    //我在项目中使用了Room,这是Room的相关依赖
    implementation "android.arch.persistence.room:runtime:$room_version"
    kapt "android.arch.persistence.room:compiler:$room_version"
    implementation "android.arch.persistence.room:rxjava2:$room_version"

    implementation "android.arch.lifecycle:extensions:$lifecycle_version"

② 创建数据源

我们要展示在list中的数据,主要以 网络请求本地持久化存储 的方式获取,本文为了保证简单,数据源通过 Room数据库中 获得。

创建Student实体类:

@Entity
data class Student(@PrimaryKey(autoGenerate = true) val id: Int,
                   val name: String)

创建Dao:

@Dao
interface StudentDao {

    @Query("SELECT * FROM Student ORDER BY name COLLATE NOCASE ASC")
    fun getAllStudent(): DataSource.Factory
}

创建数据库:

@Database(entities = arrayOf(Student::class), version = 1)
abstract class StudentDb : RoomDatabase() {

    abstract fun studentDao(): StudentDao

    companion object {

        private var instance: StudentDb? = null

        @Synchronized
        fun get(context: Context): StudentDb {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                        StudentDb::class.java, "StudentDatabase")
                        .addCallback(object : RoomDatabase.Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                ioThread {
                                    get(context).studentDao().insert(
                                            CHEESE_DATA.map { Student(id = 0, name = it) })
                                }
                            }
                        }).build()
            }
            return instance!!
        }
    }
}
private val CHEESE_DATA = arrayListOf(
        "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
        "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
        "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
        "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
        "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
        "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
        "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
        "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
        "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
        "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
        "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
        "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
        "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)"
)

③ 创建Adapter和ViewHolder

这一步就很简单了,就像往常一样,我们创建一个item的layout布局文件(已省略,就是一个TextView用于显示Student的name),同时创建对应的ViewHolder:

class StudentViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.student_item, parent, false)) {

    private val nameView = itemView.findViewById(R.id.name)
    var student: Student? = null

    fun bindTo(student: Student?) {
        this.student = student
        nameView.text = student?.name
    }
}

我们的Adapter需要继承PagedListAdapter类:

class StudentAdapter : PagedListAdapter(diffCallback) {

    override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder =
            StudentViewHolder(parent)

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback() {
            override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem == newItem
        }
    }
}

④ 展示在界面上

我们创建一个ViewModel,它用于承载 与UI无关 业务代码:

class MainViewModel(app: Application) : AndroidViewModel(app) {

    val dao = StudentDb.get(app).studentDao()

    val allStudents = LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)                         //配置分页加载的数量
            .setEnablePlaceholders(ENABLE_PLACEHOLDERS)     //配置是否启动PlaceHolders
            .setInitialLoadSizeHint(PAGE_SIZE)              //初始化加载的数量
            .build()).build()

    companion object {
        private const val PAGE_SIZE = 15

        private const val ENABLE_PLACEHOLDERS = false
    }
}

最终,在Activity中,每当观察到数据源中 数据的变化,我们就把最新的数据交给Adapter去 展示

class MainActivity : AppCompatActivity() {

    private val viewModel by lazy(LazyThreadSafetyMode.NONE) {
        ViewModelProviders.of(this, object : ViewModelProvider.Factory {
            override fun  create(modelClass: Class): T = MainViewModel(application) as T
        }).get(MainViewModel::class.java)
    }

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

        val adapter = StudentAdapter()
        recyclerView.adapter = adapter
        // 将数据的变化反映到UI上
        viewModel.allStudents.observe(this, Observer { adapter.submitList(it) })
    }
}

到这里,Paging 最基本的使用就已经讲解完毕了。您可以通过运行预览和示例 基本一致 的效果,如果有疑问,可以点我查看源码 。

从入门到放弃?

阅读到这里,我相信不少朋友会有这样一个想法—— 这个库看起来感觉好麻烦,我为什么要用它呢?

我曾经写过一篇标题很浮夸的博客:0行Java代码实现RecyclerView—— 文中我提出了一种使用DataBinding 不需要哪怕一行Java代码就能实现列表/多类型列表的方式,但是最后我也提到了,这只是一种思路,这种简单的方式背后,可能会隐藏着 严重耦合 的情况—— "一行代码实现XXX" 的库屡见不鲜,它们看上去很 简单 ,但是真正做到 灵活,松耦合 的库寥寥无几,我认为这种方式是有缺陷的。

因此,简单并不意味着设计思想的优秀,“看起来很麻烦” 也不能成为否认 Paging 的理由,本文不会去阐述 Paging 在实际项目中应该怎么用,且不说代码正确性与否,这种做法本身就会固定一个人的思维。但如果你理解了 Paging本身原理 的话,相信掌握其用法 也就不在话下了。

Paging原理详解

先上一张图

image

这是官方提供的非常棒的原理示意图,简单概括一下:

  • DataSource: 数据源,数据的改变会驱动列表的更新,因此,数据源是很重要的

  • PageList:  核心类,它从数据源取出数据,同时,它负责控制 第一次默认加载多少数据,之后每一次加载多少数据,如何加载等等,并将数据的变更反映到UI上。

  • PagedListAdapter: 适配器,RecyclerView的适配器,通过分析数据是否发生了改变,负责处理UI展示的逻辑(增加/删除/替换等)。

1.创建数据源

我们思考一个问题,将数据作为列表展示在界面上,我们首先需要什么。

数据源,是的,在Paging中,它被抽象为 DataSource , 其获取需要依靠 DataSource 的内部工厂类 DataSource.Factory ,通过create()方法就可以获得DataSource 的实例:

public abstract static class Factory {
     public abstract DataSource create();
}

数据源一般有两种选择,远程服务器请求 或者 读取本地持久化数据——这些并不重要,本文我们以Room数据库为例:

@Dao
interface StudentDao {

    @Query("SELECT * FROM Student ORDER BY name COLLATE NOCASE ASC")
    fun getAllStudent(): DataSource.Factory
}

Paging可以获得 Room的原生支持,因此作为示例非常合适,当然我们更多获取 数据源 是通过 API网络请求,其实现方式可以参考官方Sample,本文不赘述。

现在我们创建好了StudentDao,接下来就是展示UI了,在那之前,我们需要配置好PageList。

2.配置PageList

上文我说到了PageList的作用:

  • 1.从数据源取出数据

  • 2.负责控制 第一次默认加载多少数据,之后每一次加载多少数据,如何加载 等等

  • 3.将数据的变更反映到UI上。

我们仔细想想,这是有必要配置的,因此我们需要初始化PageList:

val allStudents = LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
            .setPageSize(15)                         //配置分页加载的数量
            .setEnablePlaceholders(false)     //配置是否启动PlaceHolders
            .setInitialLoadSizeHint(30)              //初始化加载的数量
            .build()).build()

我们按照上面分的三个职责来讲:

  • 从数据源取出数据

很显然,这对应的是 dao.getAllStudent() ,通过数据库取得最新数据,如果是网络请求,也应该对应API的请求方法,返回值应该是DataSource.Factory类型。

  • 进行相关配置

PageList提供了 PagedList.Config 类供我们进行实例化配置,其提供了4个可选配置:

 public static final class Builder {
            //  省略其他Builder内部方法
            private int mPageSize = -1;    //每次加载多少数据
            private int mPrefetchDistance = -1;   //距底部还有几条数据时,加载下一页数据
            private int mInitialLoadSizeHint = -1; //第一次加载多少数据
            private boolean mEnablePlaceholders = true; //是否启用占位符,若为true,则视为固定数量的item
}
  • 将变更反映到UI上这个指的是 LivePagedListBuilder,而不是 PagedList.Config.Builder,它可以设置 获取数据源的线程边界Callback,但是一般来讲可以不用配置,大家了解一下即可。

经过以上操作,我们的PageList设置好了,接下来就可以配置UI相关了。

3.配置Adapter

就像我们平时配置 RecyclerView 差不多,我们配置了ViewHolder和RecyclerView.Adapter,略微不同的是,我们需要继承PagedListAdapter:

class StudentAdapter : PagedListAdapter(diffCallback) {
    //省略 onBindViewHolder() && onCreateViewHolder()
    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback() {
            override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem == newItem
        }
    }
}

当然我们还需要传一个 DifffUtil.ItemCallback 的实例,这里需要对数据源返回的数据进行了比较处理, 它的意义是——我需要知道怎么样的比较,才意味着数据源的变化,并根据变化再进行的UI刷新操作。

ViewHolder的代码正常实现即可,不再赘述。

4.监听数据源的变更,并响应在UI上

这个就很简单了,我们在Activity中声明即可:

val adapter = StudentAdapter()
recyclerView.adapter = adapter

viewModel.allStudents.observe(this, Observer { adapter.submitList(it) })

这样,每当数据源发生改变,Adapter就会自动将 新的数据 动态反映在UI上。

分页库的设计美学

现在,我简单了解了它的原理,但是还不是很够—— 正如我前言所说的,从别人的 代码设计和思想取长补短,化为己用 ,这才是我的目的。

让我们回到最初的问题:

看起来很麻烦,那么我为什么用这个库?

我会有这种想法,我为什么不能把所有功能都封装到一个 RecyclerView的Adapter里面呢,它包含 下拉刷新上拉加载分页 等等功能。

原因很简单,因为这样做会将 业务层代码UI层 混在一起造 耦合 ,最直接就导致了 难以通过代码进行单元测试

UI层业务层 代码的隔离是优秀的设计,这样更便于 测试 ,我们可以从Google官方文档的目录结构中看到这一点:

接下来,我会尝试站在设计者的角度,尝试去理解 Paging 如此设计的原因。

1.PagedListAdapter

将分页数据作为List展示在界面上,RecyclerView 是首选,那么实现一个对应的 PagedListAdapter 当然是不错的选择。

Google对  PagedListAdapter 的职责定义的很简单,仅仅是一个被代理的对象而已,所有相关的数据处理逻辑都委托给了 AsyncPagedListDiffer

public abstract class PagedListAdapter
        extends RecyclerView.Adapter {

    protected PagedListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) {
        mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
        mDiffer.mListener = mListener;
    }

    public void submitList(PagedList pagedList) {
        mDiffer.submitList(pagedList);
    }

    protected T getItem(int position) {
        return mDiffer.getItem(position);
    }

    @Override
    public int getItemCount() {
        return mDiffer.getItemCount();
    }

   public PagedList getCurrentList() {
        return mDiffer.getCurrentList();
    }
}

当数据源发生改变时,实际上会通知 AsyncPagedListDiffersubmitList() 方法通知其内部保存的 PagedList 更新并反映在UI上:

//实际上内部存储了要展示在UI上的数据源PagedList
public class AsyncPagedListDiffer {
    //省略大量代码
    private PagedList mPagedList;
    private PagedList mSnapshot;
}

篇幅所限,我们不讨论数据是如何展示的(答案很简单,就是通过RecyclerView.Adapter的notifyItemChange()相关方法),我们有一个问题更需要去关注:

Paging 未滑到底部便开始加载数据的逻辑 在哪里?

如果你认真思考,你应该能想到正确的答案,在 getItem() 方法中执行。

public T getItem(int index) {
        //省略部分代码
        mPagedList.loadAround(index);  //如果需要,请求更多数据
        return mPagedList.get(index);  //返回Item数据
}

每当RecyclerView要展示一个新的Item时,理所应当的会通过 getItem() 方法获取相应的数据,既然如此,为何不在返回最新数据之前,判断当前的数据源是否需要 加载下一页数据 呢?

2.抽象类PagedList: 设计模式的组合美学

我们来看抽象类PagedList.loadAround(index)方法:

 public void loadAround(int index) {
        mLastLoad = index + getPositionOffset();
        loadAroundInternal(index);

        mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
        mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
        tryDispatchBoundaryCallbacks(true);
    }
    //这个方法是抽象的
    abstract void loadAroundInternal(int index);

需要再次重复的是,即使是PagedList,也有很多种不同的 数据分页策略:

这些不同的 PagedList 在处理分页逻辑上,可能有不同的逻辑,那么,作为设计者,应该做到的是,把异同的逻辑抽象出来交给子类实现(即loadAroundInternal方法),而把公共的处理逻辑暴漏出来,并向上转型交给Adapter(实际上是 AsyncPagedListDiffer)去执行分页加载的API,也就是loadAround方法。

好处显而易见,对于Adapter来说,它只需要知道,在我需要请求分页数据时,调用PagedList的loadAround方法即可,至于 是PagedList的哪个子类,内部执行什么样的分页逻辑,Adapter并不关心

这些PagedList的不同策略的逻辑,是在PagedList.create()方法中进行的处理:

 private static  PagedList create(@NonNull DataSource dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            //省略其他代码
            //返回ContiguousPagedList
            return new ContiguousPagedList<>(contigDataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    key,
                    lastLoad);
        } else {
            //返回TiledPagedList
            return new TiledPagedList<>((PositionalDataSource) dataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    (key != null) ? (Integer) key : 0);
        }
    }

PagedList是一个抽象类,实际上它的作用是 通过Builder实例化PagedList真正的对象

通过Builder.build()调用create()方法,决定实例化哪个PagedList的子类:

 public PagedList build() {
            return PagedList.create(
                    mDataSource,
                    mNotifyExecutor,
                    mFetchExecutor,
                    mBoundaryCallback,
                    mConfig,
                    mInitialKey);
        }

Builder模式是非常耳熟能详的设计模式,它的好处是作为API的门面,便于开发者更简单上手并进行对应的配置。

不同的PagedList对应不同的DataSource,比如:

class ContiguousPagedList extends PagedList implements PagedStorage.Callback {

    ContiguousPagedList(
            //请注意这行,ContiguousPagedList内部需要ContiguousDataSource
            @NonNull ContiguousDataSource dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback boundaryCallback,
            @NonNull Config config,
            final @Nullable K key,
            int lastLoad) {
            //.....
    }


abstract class ContiguousDataSource extends DataSource {
      //......
}

class TiledPagedList extends PagedList implements PagedStorage.Callback {

    TiledPagedList(
            //请注意这行,TiledPagedList内部需要PositionalDataSource
            @NonNull PositionalDataSource dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback boundaryCallback,
            @NonNull Config config,
            int position) {
           //......
    }
}

public abstract class PositionalDataSource extends DataSource {
    //......
}

回到create()方法中,我们看到dataSource此时也仅仅是接口类型的声明:

private static  PagedList create(
            //其实这时候dataSource只是作为DataSource类型的声明
            @NonNull DataSource dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
}

实际上,create方法的作用是,通过将不同的DataSource,作为依赖实例化对应的PagedList,除此之外,还有对DataSource的对应处理,或者Wrapper(再次包装,详情请参考源码的create方法,篇幅所限本文不再叙述)。

这个过程中,通过Builder,将 多种数据源(DataSource)多种分页策略(PagedList) 互相进行组合,并 向上转型 交给 适配器(Adapter) ,然后Adapter将对应的功能 委托 给了 代理类的AsyncPagedListDiffer 处理——这之间通过数种设计模式的组合,最终展现给开发者的是一个 简单且清晰 的API接口去调用,其设计的精妙程度,远非笔者这种普通的开发者所能企及。

3.更多

实际上,笔者上文所叙述的内容只是 Paging 的冰山一角,其源码中,还有很多很值得学习的优秀思想,本文无法一一列举,比如 线程的切换(加载分页数据应该在io线程,而反映在界面上时则应该在ui线程),再比如库 对多种响应式数据类型的支持(LiveData,RxJava),这些实用的功能实现,都通过 Paging 优秀的设计,将其复杂的实现封装了起来,而将简单的API暴露给开发者调用,有兴趣的朋友可以去研究一下。

小结&&吐槽

本文的发布也算是 历经坎坷—— 初稿完成时间为6月初,但拖了一个月才发出来,原因就是本文写好之后,本来准备第二天发表,结果第二天早上一起来,发现 这篇花费了3天晚上总结的文章,竟然没有能成功保存!

我自认为我内心还算比较强大,但还是差点崩溃,写过博客的同学都知道,这样一篇6k+的博客,所需要耗费很多心血才能完成。

我重新整理了思路写了一遍——收获是有的,再一次翻阅源码的同时,我又加深了对Paging设计思想的理解,这个过程很痛苦,但是也值得。

当然,即使如此,笔者水平有限,也难免文中内容有理解错误之处,也希望能有朋友不吝赐教,共同讨论一起进步。

本文demo可以在这里找到: https://github.com/qingmei2/SamplePaging

— — — END — — —

近期文章回顾

评论

查看更多 >