Android大量數(shù)據(jù)加載—Paging的使用

Paging主要是用來結(jié)合RecyclerView進(jìn)行使用的。它的作用是能夠逐漸地、優(yōu)雅地加載所需要加載的數(shù)據(jù)。也就是一種分頁方案。

Paging每次只會加載總數(shù)據(jù)的一小部分。因此它有下面的兩個優(yōu)點(diǎn):

  • 數(shù)據(jù)加載要求更小的帶寬以及更少的系統(tǒng)資源。
  • 在資源發(fā)生改變的情況下,app依然能夠很快的做出響應(yīng)。

Paging主要的類介紹

PagedList

這個類是用來存儲加載的數(shù)據(jù)。PagedList中所需要的數(shù)據(jù)都是從下面要講的DataSource中進(jìn)行加載的。

DataSource

DataSource顧名思義就是數(shù)據(jù)來源。這類提供加載所需的數(shù)據(jù)。也就是在這個類中進(jìn)行數(shù)據(jù)的獲取操作。數(shù)據(jù)源可以是DataBase也可以是服務(wù)器。

DataSource的三個子類:

  • PositionalDataSource: 主要用于加載數(shù)據(jù)可數(shù)有限的數(shù)據(jù)。比如加載本地數(shù)據(jù)庫,這種情況下用戶可以通過比如說像通訊錄按姓的首字母查詢的情況。能夠跳轉(zhuǎn)到任意的位置。
  • ItemKeyedDataSource:主要用于加載逐漸增加的數(shù)據(jù)。比如說網(wǎng)絡(luò)請求的數(shù)據(jù)隨著不斷的請求得到的數(shù)據(jù)越來越多。然后它適用的情況就是通過N-1item的數(shù)據(jù)來獲取Nitem數(shù)據(jù)的情況。比如說Github的api。
  • PageKeyedDataSource:這個和ItemKeyedDataSource有些相似,都是針對那種不斷增加的數(shù)據(jù)。這里網(wǎng)絡(luò)請求得到數(shù)據(jù)是分頁的。比如說知乎日報的news的api。

DataSource.Factory

這個接口的實現(xiàn)類主要是用來獲取的DataSource的。

PagedListAdapter

這個Adapter繼承自RecyclerView.Adapter。如果要使用Paging,就需要讓實現(xiàn)的RecyclerView的Adapter繼承自PagedListAdapter。這個抽象類實現(xiàn)關(guān)于PagedList相關(guān)的東西。

LivePagedListBuilder

通過這個類來生成對應(yīng)的PagedList。

準(zhǔn)備

// RxJava
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"

// Retrofit
implementation "com.squareup.retrofit2:adapter-rxjava2:2.4.0"
implementation "com.squareup.retrofit2:converter-gson:2.4.0"
implementation "com.squareup.retrofit2:retrofit:2.4.0"

// ViewModel and LiveData
def lifecycle_version = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
kapt "android.arch.lifecycle:compiler:$lifecycle_version"

// Room
def room_version = "1.1.1"
implementation "android.arch.persistence.room:runtime:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
implementation "android.arch.persistence.room:rxjava2:$room_version"

// Paging
def paging_version = "1.0.0"
implementation "android.arch.paging:runtime:$paging_version"

// Glide
implementation "com.github.bumptech.glide:glide:4.8.0"
kapt 'com.github.bumptech.glide:compiler:4.8.0'

應(yīng)該大家都清楚這些庫吧。這里就不一一解釋了。這里進(jìn)行舉例說明會用到Room、ViewModel、LiveData,請還不了解的朋友去看我的另外幾篇博客。

PositionalDataSource的使用

數(shù)據(jù)庫部分

@Entity
data class Person(@PrimaryKey(autoGenerate = true) val id: Int, val name: String)
@Dao
interface PersonDao {
    @Insert
    fun insertPerson(person: Person)

    @Insert
    fun insertPersons(persons: List<Person>)

    @Delete
    fun deletePerson(person: Person)

    @Query("SELECT * FROM Person ORDER BY name COLLATE NOCASE ASC")
    fun getAllPersons(): DataSource.Factory<Int, Person>
}

關(guān)于Dao這里需要解釋一下。getAllPersons返回一個DataSource.Factory。在之后會通過LivePagedListBuilder來構(gòu)建PagedList。

@Database(entities = [Person::class], version = 1)
abstract class PersonDatabase : RoomDatabase() {
    abstract fun personDao(): PersonDao

    companion object {
        private var INSTANCE: PersonDatabase? = null

        fun get(context: Context): PersonDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context, PersonDatabase::class.java,    
                                                "PersonDatabase")
                        .addCallback(object : RoomDatabase.Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                fillDatabase(context)
                            }
                        })
                        .build()
            }
            return INSTANCE!!
        }

        private fun fillDatabase(context: Context) {
            ioThread {
                CHEESE_DATA.map {
                    get(context).personDao().insertPerson(Person(0, it))
                }
            }
        }
    }
}

private val EXECUTOR = Executors.newSingleThreadExecutor()

fun ioThread(f: () -> Unit) {
    EXECUTOR.execute(f)
}

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)",
        "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves",
        "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur",
        "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon",
        "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin",
        "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)",
        "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine",
        "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza",
        "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)",
        "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta",
        "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie",
        "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat",
        "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano",
        "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain",
        "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou",
        "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar",
        "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno",
        "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack",
        "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper",
        "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)",
        "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese",
        "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza",
        "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley",
        "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino",
        "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina",
        "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby",
        "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin",
        "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester",
        "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue",
        "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz",
        "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich",
        "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue",
        "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle",
        "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia",
        "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis",
        "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus",
        "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison",
        "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois",
        "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse",
        "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese",
        "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise",
        "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra",
        "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola",
        "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost",
        "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel",
        "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve",
        "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi",
        "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti",
        "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve",
        "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster",
        "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg",
        "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa",
        "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella    Affine",
        "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese",
        "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere",
        "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire",
        "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou",
        "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger",
        "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings",
        "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse",
        "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam",
        "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego",
        "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin",
        "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)",
        "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse",
        "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda",
        "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte",
        "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio",
        "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne",
        "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)",
        "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster",
        "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel",
        "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca",
        "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre",
        "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty",
        "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela",
        "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano",
        "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage",
        "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry",
        "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid",
        "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn",
        "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse",
        "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin",
        "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin",
        "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre",
        "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone",
        "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark",
        "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit",
        "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia",
        "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)",
        "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna",
        "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera",
        "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou",
        "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder",
        "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort",
        "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr",
        "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin",
        "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre",
        "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss",
        "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela",
        "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda",
        "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain",
        "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese",
        "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale",
        "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie",
        "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri",
        "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar",
        "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance",
        "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes",
        "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet",
        "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe",
        "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa",
        "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois",
        "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue",
        "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington",
        "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou",
        "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
        "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano")

名字很長,為了方便大家直接拿去用這里全部放上去了。這里實現(xiàn)的是在數(shù)據(jù)庫初次構(gòu)建的時候?qū)⑾旅娴臄?shù)據(jù)存儲在數(shù)據(jù)庫中

DataSource、PagedList部分

class PersonViewModel(application: Application) : AndroidViewModel(application) {
    companion object {
        private const val PAGE_SIZE = 30
        private const val ENABLE_PLACEHOLDER = true
    }
    
    private val mPersonDao = PersonDatabase.get(application).personDao()

    val persons = LivePagedListBuilder(mPersonDao.getAllPersons(), PagedList
            .Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setEnablePlaceholders(ENABLE_PLACEHOLDER).build()).build()
}

這里先通過RoomDatabase來獲取一個PersonDao對象。再通過LivePagedListBuilder獲取一個PagedList。

PagedList.Config是用于對PagedList進(jìn)行構(gòu)建配置的類。其中PAGE_SIZE用于指定每頁數(shù)據(jù)量。ENABLE_PLACEHOLDER表示是否將未加載的數(shù)據(jù)以null存儲在在PageList中。具體的效果試了就知道了。

PagedListAdapter部分

class PersonRecAdapter(val context: Context) : PagedListAdapter<Person, PersonRecAdapter.PersonViewHolder>(diffCallBack) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
        return 
        PersonViewHolder(LayoutInflater.from(context).inflate(R.layout.person_rec_item, 
                                                              parent, false))
    }

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

    companion object {
        private val diffCallBack = object : DiffUtil.ItemCallback<Person>() {

            override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
                return oldItem == newItem
            }

            override fun getChangePayload(oldItem: Person, newItem: Person): Any? {
                return null
            }
        }
    }

    class PersonViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        val id = itemView.findViewById<TextView>(R.id.id)
        val name = itemView.findViewById<TextView>(R.id.name)

        fun bindTo(person: Person?){
            Log.d("PersonViewHolder", person.toString())
            id.text = person!!.id.toString()
            name.text = person.name
        }
    }
}

在對下面的內(nèi)容解釋之前。先說一下onBindViewHolder有兩個重載方法。一個是我們熟悉的onBindViewHolder(holder: PersonViewHolder, position: Int),另一個是onBindViewHolder(holder: PersonViewHolder, position: Int, payloads: MutableList<Any>)。其中帶payloads的方法默認(rèn)實現(xiàn)是調(diào)用不帶payloads的。

PagedListAdapter需要說的地方就是它接收一個DiffUtil.ItemCallback參數(shù)進(jìn)行對象的構(gòu)建。這個用于在PagedListAdapter的PageList發(fā)生變化后進(jìn)行比對。如果areItemsTheSame返回true、areContentsTheSame返回false就會先調(diào)用帶payloads的onBindViewHolder再根據(jù)前面的onBindViewHolder的情況看是否調(diào)用不帶payloads的onBindViewHolder。這里怎么判斷是否改變就是根據(jù)具體情況實現(xiàn)具體的邏輯了。

Adapter中布局很簡單,這里節(jié)省代碼就不列出來了。

Activity使用部分

class MainActivity : AppCompatActivity() {

    private lateinit var mPersonViewModel: PersonViewModel

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

        mPersonViewModel = ViewModelProviders.of(this).get(PersonViewModel::class.java)
        val adapter = PersonRecAdapter(this)
        rv.adapter = adapter
        mPersonViewModel.persons.observe(this, Observer(adapter::submitList))
    }
}

很簡單。給ViewModel中的persons這個LiveData<PagedList<Person>>進(jìn)行添加一個觀察者。在persons數(shù)據(jù)發(fā)生變化后,就調(diào)用PersonRecAdapter的submitList方法進(jìn)行數(shù)據(jù)的添加。

Activity的xml如下(下面的講解中的Actvity是一樣的就不會再列出了)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".paging.positional.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        android:text="Hello World!"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

這里結(jié)合Room使用就完了。然后大家肯定會驚訝從頭到尾都沒有看到PositionalDataSource的影子。其實這是Room替我們生成了一個繼承了PositionalDataSource的類——LimitOffsetDataSource的。這里就不列出來了。有興趣的可以自己去看一下這個類。

ItemKeyedDataSource

這個DataSource用在N-1的item的內(nèi)容中的某個信息指向N的item。相當(dāng)于Item之間通過鏈表鏈著一樣。Github獲取帳號的API就是典型的這樣的API。因此這里以Github的API進(jìn)行舉例。

下面是github獲取帳號的api。其中需要注意一點(diǎn)的就是如果一個IP地址對這個api使用超過一定的流量,會有段時間靜止訪問

https://api.github.com/users?since=0?per_page=30

網(wǎng)絡(luò)請求部分:

interface GitHubService {
    @GET("users")
    fun getGithubAccount(@Query("since") id: Long, @Query("per_page") perPage: Int):
            Observable<List<GithubAccount>>
}
object ApiGenerate {
    private val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
            
    fun getGitHubService(): GitHubService = retrofit.create(GitHubService::class.java)
}
data class GithubAccount(
        var login: String? = null,
        var id: Int = 0,
        var node_id: String? = null,
        var avatar_url: String? = null,
        var gravatar_id: String? = null,
        var url: String? = null,
        var html_url: String? = null,
        var followers_url: String? = null,
        var following_url: String? = null,
        var gists_url: String? = null,
        var starred_url: String? = null,
        var subscriptions_url: String? = null,
        var organizations_url: String? = null,
        var repos_url: String? = null,
        var events_url: String? = null,
        var received_events_url: String? = null,
        var type: String? = null,
        var isSite_admin: Boolean = false)

上面相信都沒有問題。

class ExecuteOnceObserver<T>(val onExecuteOnceNext: (T) -> Unit = {},
                             val onExecuteOnceComplete: () -> Unit = {},
                             val onExecuteOnceError: (Throwable) -> Unit = {}) : Observer<T> {
    private var mDisposable: Disposable? = null

    override fun onComplete() {
        onExecuteOnceComplete()
    }

    override fun onSubscribe(d: Disposable) {
        mDisposable = d
    }

    override fun onNext(t: T) {
        try {
            onExecuteOnceNext(t)
            this.onComplete()
        } catch (e: Throwable) {
            this.onError(e)
        } finally {
            if (mDisposable != null && !mDisposable!!.isDisposed) {
                mDisposable!!.dispose()
            }
        }
    }
    override fun onError(e: Throwable) {
        onExecuteOnceError(e)
    }
}

這是一個用于執(zhí)行一次onNext就被銷毀的Observer工具類。

DataSource、PagedList部分

class ByItemDataSource : ItemKeyedDataSource<Long, GithubAccount>() {

    private val mGitHubService by lazy {
        ApiGenerate.getGitHubService()
    }

    override fun loadInitial(params: LoadInitialParams<Long>, callback: 
    LoadInitialCallback<GithubAccount>) {
        mGitHubService.getGithubAccount(0, params.requestedLoadSize)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .subscribe(ExecuteOnceObserver({
                    callback.onResult(it)
                }))
    }

    override fun loadAfter(params: LoadParams<Long>, callback: 
    LoadCallback<GithubAccount>) {
        mGitHubService.getGithubAccount(params.key, params.requestedLoadSize)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .subscribe(ExecuteOnceObserver(onExecuteOnceNext = {
                    callback.onResult(it)
                }))
    }

    override fun loadBefore(params: LoadParams<Long>, callback: 
    LoadCallback<GithubAccount>) {
        //由于這里不需要向上加載因此省略此處
    }

    override fun getKey(item: GithubAccount): Long = item.id.toLong()
}

ItemKeyedDataSource的子類需要實現(xiàn)loadInitial、loadAfter、loadBefore和getKey方法。它們分別的作用如下:

  • loadInitial:此方法之后在用DataSource構(gòu)建PageList的時候才會調(diào)用一次。用于進(jìn)行加載初始化。
  • loadAfter:在每次RecyclerView滑動到底部沒有數(shù)據(jù)的時候就會調(diào)用此方法進(jìn)行數(shù)據(jù)的加載。
  • loadBefore:在每次RecyclerView滑動到頂部沒有數(shù)據(jù)的時候就會調(diào)用此方法進(jìn)行數(shù)據(jù)的加載。
  • getKey: 這返回下一個loadAfter調(diào)用所需要用到的key。就相當(dāng)于鏈表的指針。

其中三個load方法都是通過LoadInitialCallback、LoadCallback來將數(shù)據(jù)傳給PagedList的。

經(jīng)過上面每個方法的解釋應(yīng)該沒有問題。Github的api就相當(dāng)于id作為了鏈表的指針了。

class ByItemDataSourceFactory : DataSource.Factory<Long, GithubAccount>() {
    override fun create(): DataSource<Long, GithubAccount> = ByItemDataSource()
}

就是將上面DataSource作一個返回很簡單。

PagedListAdapter部分

class ByItemAdapter : PagedListAdapter<GithubAccount, ByItemAdapter.ByItemViewHolder>(diffCallback) {

    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<GithubAccount>() {
            override fun areItemsTheSame(oldItem: GithubAccount, newItem: GithubAccount): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: GithubAccount, newItem: GithubAccount): Boolean {
                return oldItem == newItem
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ByItemViewHolder {
        return ByItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.by_item_rec_adapter, parent, false))
    }

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

    class ByItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val mId: TextView = itemView.findViewById(R.id.id)
        private val mName: TextView = itemView.findViewById(R.id.name)

        fun bindTo(account: GithubAccount?) {
            account?.let {
                mId.text = it.id.toString()
                mName.text = it.login
            }
        }
    }
}

這個和前面講PositionalDataSource處的Adapter重點(diǎn)是一樣的這里就不重復(fù)了。

Adapter的代碼很簡單就不列出來了。

Activity中的使用

class ByItemActivity : AppCompatActivity() {

    private lateinit var mByItemViewModel: ByItemViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_by_item)
        mByItemViewModel = ViewModelProviders.of(this).get(ByItemViewModel::class.java)

        val adapter = ByItemAdapter()
        rv.adapter = adapter
        mByItemViewModel.accounts.observe(this, Observer(adapter::submitList))
    }
}

很簡單和前面一樣的設(shè)置監(jiān)聽器和數(shù)據(jù)。

PageKeyedDataSource的使用

這個用服務(wù)器本身是分頁實現(xiàn)的,然后我們通過其返回的數(shù)據(jù)每一頁的數(shù)據(jù)得到下一頁的key。典型的知乎日報的api就是這樣的。

這是知乎日報查看過往消息的api:

https://news-at.zhihu.com/api/4/news/before/20180823

網(wǎng)絡(luò)請求部分:

interface NewsService {
    @GET("before/{time}")
    fun getNews(@Path("time")time: Long): Observable<News>
}
object ApiGenerate {

    private val retrofit = Retrofit.Builder()
            .baseUrl("https://news-at.zhihu.com/api/4/news/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
    fun getNewsService(): NewsService = retrofit.create(NewsService::class.java)
}
class News(var date: String = "",
           var stories: List<StoriesBean> = emptyList()) {

    class StoriesBean {
        var type: Int = 0
        var id: Int = 0
        var ga_prefix: String? = null
        var title: String? = null
        var images: List<String>? = null
    }
}
class ByPageDataSource : PageKeyedDataSource<Long, News.StoriesBean>() {

    private lateinit var mNewsService: NewsService
    private val mDate = Calendar.getInstance().apply {
        add(Calendar.DATE, 1)
    }
    
    override fun loadInitial(params: LoadInitialParams<Long>, callback: 
                             LoadInitialCallback<Long, News.StoriesBean>) {
        mNewsService = ApiGenerate.getNewsService()
        mNewsService.getNews(SimpleDateFormat("yyyyMMdd", 
                                              Locale.CHINA).format(mDate.time).toLong())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .subscribe(ExecuteOnceObserver(onExecuteOnceNext = {
                    callback.onResult(it.stories, 0, it.date.toLong())
                }))
    }

    override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Long, 
                           News.StoriesBean>) {
        mNewsService.getNews(params.key)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .subscribe(ExecuteOnceObserver(onExecuteOnceNext = {
                    callback.onResult(it.stories, it.date.toLong())
                }))
    }

    override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<Long, 
                            News.StoriesBean>) {
        //這里不需要向上加載,因此無須實現(xiàn)
    }
}

對于PageKeyedDataSource的子類有三個要實現(xiàn)方法loadInitial、loadAfter和loadBefore。其中三個方法的作用和ItemKeyedDataSource是一樣的。只不過這里L(fēng)oadInitialCallback、LoadCallback和ItemKeyedDataSource不一樣。這個就請自己去它的不同api了。其中ExecuteOnceObserver就是前面的一樣的。

DataSource、PagedList部分

class ByPageDataSourceFactory : DataSource.Factory<Long, News.StoriesBean>() {
    override fun create(): DataSource<Long, News.StoriesBean> = ByPageDataSource()
}
class ByPageViewModel : ViewModel() {
    val stories = LivePagedListBuilder(ByPageDataSourceFactory(),
            PagedList.Config.Builder()
                    .setPageSize(30)
                    .setEnablePlaceholders(false).build()).build()
}

這里我們設(shè)置的pageSize并沒有用再DataSource中我們并沒有使用到。但是這個值必須是個正數(shù)。

PagedListAdapter部分

class ByPageAdapter : PagedListAdapter<News.StoriesBean, ByPageAdapter.ByItemViewHolder>(diffCallback) {


    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<News.StoriesBean>() {
            override fun areItemsTheSame(oldItem: News.StoriesBean, newItem: 
                                         News.StoriesBean): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: News.StoriesBean, newItem: 
                                            News.StoriesBean): Boolean {
                return oldItem == newItem
            }

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ByItemViewHolder {
        return 
     ByItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.by_page_rec_item, parent, false))
    }

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

    class ByItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private lateinit var imageView: ImageView
        private lateinit var textView: TextView
        
        fun bindTo(story: News.StoriesBean?) {
            imageView = itemView.findViewById(R.id.iv)
            textView = itemView.findViewById(R.id.tv)

            story?.let {
                Glide.with(imageView.context).load(it.images!![0]).into(imageView)
                textView.text = it.title
            }
        }
    }
}

Adapter的布局很簡單就不列出來了

Activity使用部分

class ByPageActivity : AppCompatActivity() {

    private lateinit var mByPageViewModel: ByPageViewModel

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

        mByPageViewModel = ViewModelProviders.of(this).get(ByPageViewModel::class.java)
        val adapter = ByPageAdapter()
        rv.adapter = adapter
        mByPageViewModel.stories.observe(this, Observer(adapter::submitList))
    }
}

總結(jié)

到此為止圍繞著三個DataSource都已將講完了。經(jīng)過大家的代碼實踐,這里在給大家貼上官方的一張圖。相信大家不會對其中的關(guān)系不會太難理解:

Paging 原理

參考

官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容