JetPack :Paging、WorkManager、Slices介紹和使用方式

本文由玉剛說寫作平臺提供寫作贊助,版權(quán)歸玉剛說微信公眾號所有
原作者:Mr.s(豬_隊(duì)友)
版權(quán)聲明:未經(jīng)玉剛說許可,不得以任何形式轉(zhuǎn)載

今年谷歌I/O大會(huì),谷歌發(fā)布了 Android Jetpack.這是新一代組件、工具和架構(gòu)指導(dǎo),用谷歌官方的話就是旨在加快開發(fā)者的 Android 應(yīng)用開發(fā)速度。Android Jetpack 組件將現(xiàn)有的支持庫與架構(gòu)組件聯(lián)系起來,并將它們分成四個(gè)類別:

結(jié)構(gòu)圖.png

Android Jetpack 組件以“未捆綁的”庫形式提供,這些庫不是基礎(chǔ) Android 平臺的一部分。這就意味著,我們可以根據(jù)自己的需求采用每一個(gè)組件。在新的 Android Jetpack 功能發(fā)布后,我們可以將其添加到自己的應(yīng)用中,將我們的應(yīng)用部署到應(yīng)用商店并向用戶提供新功能,如果我們的行動(dòng)足夠快,所有這些可以在一天內(nèi)完成!

那么谷歌發(fā)布JetPack的目的是什么呢?

三大優(yōu)點(diǎn).png
  • 加速開發(fā)
    組件可單獨(dú)采用,但可以一起使用,同時(shí)利用Kotlin語言功能,提高工作效率。(ps:Kotlin可以大大減少代碼量,據(jù)說可以減少到1/3,請一定要學(xué)習(xí)kotlin)
  • 減少并消除樣板代碼
    Android Jetpack管理諸如后臺任務(wù),導(dǎo)航和生命周期管理等繁瑣的活動(dòng),因此我們可以專注提高應(yīng)用品質(zhì)等其他方面。
  • 構(gòu)建高品質(zhì),強(qiáng)大的應(yīng)用
    以現(xiàn)代設(shè)計(jì)實(shí)踐為基礎(chǔ),Android Jetpack組件可降低崩潰次數(shù)并減少內(nèi)存泄漏,并向后兼容。

除了這三點(diǎn)外,谷歌想給開發(fā)者定制一套標(biāo)準(zhǔn),比如框架的標(biāo)準(zhǔn),我們平時(shí)MVC,MVP,MVVM等等,現(xiàn)在谷歌自己搞了一套MVP-CLEAN。
我們從JetPack的四大部分也可以看出,谷歌想要結(jié)束混亂的局面,給開發(fā)者一個(gè)規(guī)范,這個(gè)對我們開發(fā)者也是一件好事,跟著官方走總不會(huì)差的。更多參見App體系結(jié)構(gòu)指南

那么我在這一篇給大家介紹一下Paging、WorkManager、Slices和他們的使用方式,篇幅較長,請大家酌情找尿點(diǎn)。

Paging(分頁)

背景:
很多應(yīng)用程序從包含大量項(xiàng)目的數(shù)據(jù)源中獲取數(shù)據(jù),但一次只顯示一小部分?jǐn)?shù)據(jù)。加載應(yīng)用程序中顯示的數(shù)據(jù)可能很大并且代價(jià)高昂,因此要避免一次下載,創(chuàng)建或呈現(xiàn)太多數(shù)據(jù)。為了可以更輕松地在我們的應(yīng)用程序中逐漸加載數(shù)據(jù)谷歌方法提供了這個(gè)組件,可以很容易地加載和現(xiàn)在的大數(shù)據(jù)集與我們的RecyclerView快速,無限滾動(dòng)。它可以從本地存儲,網(wǎng)絡(luò)或兩者加載分頁數(shù)據(jù),并且可以讓我們自定義如何加載內(nèi)容。它可以與Room,LiveData和RxJava一起使用。
Paging Libray分為三部分:DataSource, PagedList, PagedAdapter

Paging Libraray Diagram

DataSource:

它就像是一個(gè)抽水泵,而不是真正的水源,它負(fù)責(zé)從數(shù)據(jù)源加載數(shù)據(jù),可以看成是Paging Library與數(shù)據(jù)源之間的接口。
Datasource<Key, Value>是數(shù)據(jù)源相關(guān)的類,Key是加載數(shù)據(jù)的條件信息,Value是返回結(jié)果, 針對不同場景我們需要用不同的Datasource,Paging提供了三個(gè)子類來供我們選擇。

  • PageKeyedDataSource<Key, Value> :適用于目標(biāo)數(shù)據(jù)根據(jù)頁信息請求數(shù)據(jù)的場景,即Key 字段是頁相關(guān)的信息。比如請求的數(shù)據(jù)的參數(shù)中包含類似next/previous的信息。
  • ItemKeyedDataSource<Key, Value> :適用于目標(biāo)數(shù)據(jù)的加載依賴特定item的信息, 即Key字段包含的是Item中的信息,比如需要根據(jù)第N項(xiàng)的信息加載第N+1項(xiàng)的數(shù)據(jù),傳參中需要傳入第N項(xiàng)的ID時(shí),該場景多出現(xiàn)于論壇類應(yīng)用評論信息的請求。
  • PositionalDataSource<T>:適用于目標(biāo)數(shù)據(jù)總數(shù)固定,通過特定的位置加載數(shù)據(jù),這里Key是Integer類型的位置信息,T即Value。 比如從數(shù)據(jù)庫中的1200條開始加在20條數(shù)據(jù)。

PagedList:

它就像是一個(gè)蓄水池,DataSource抽的水放到PagedList中。它是List的子類,它包含著我們的數(shù)據(jù)并告訴數(shù)據(jù)源何時(shí)加載數(shù)據(jù)。我們也可以配置一次加載多少數(shù)據(jù),以及應(yīng)該預(yù)取多少數(shù)據(jù)。它提供適配器的更新作為頁面中加載的數(shù)據(jù)。
PagedList有五個(gè)重要的參數(shù):

  • mMainThreadExecutor: 主線程的Excutor, 用于將結(jié)果post到主線程。

  • mBackgroundThreadExecutor: 后臺線程的Excutor.

  • BoundaryCallback:加載Datasource中的數(shù)據(jù)加載到邊界時(shí)的回調(diào).

  • Config: 配置PagedList從Datasource加載數(shù)據(jù)的方式, 其中包含以下屬性:

    • pageSize:設(shè)置每頁加載的數(shù)量
    • prefetchDistance:預(yù)加載的數(shù)量
    • initialLoadSizeHint:初始化數(shù)據(jù)時(shí)加載的數(shù)量
    • enablePlaceholders:當(dāng)item為null是否使用PlaceHolder展示
    • PagedStorage<T>: 用于存儲加載到的數(shù)據(jù),它是真正的蓄水池所在,它包 含一個(gè)ArrayList<List<T>> 對象mPages,按頁存儲數(shù)據(jù)。

PagedListAdapter:

這個(gè)類是RecyclerView.adapter的實(shí)現(xiàn),它提供來自PagedList的數(shù)據(jù)并以DiffUtil作為參數(shù)來計(jì)算數(shù)據(jù)的差異并為你做所有的更新工作。

看十遍不如敲一遍,搞起,搞起~~(本篇全部用Kotlin語言,涉及到LiveData,ViewModel,Room,請大家系好安全帶)

功能:本地增加和刪除Item的列表

1、添加依賴

  def paging_version = "1.0.0"
    def lifecycle_version = "1.1.1"
    def room_version = "1.1.0"
//這是 Paging的依賴
  implementation "android.arch.paging:runtime:$paging_version"
    // alternatively - without Android dependencies for testing
    testImplementation "android.arch.paging:common:$paging_version"
    // optional - RxJava support, currently in release candidate
    implementation 'android.arch.paging:rxjava2:1.0.0-rc1'

  //這是  ViewModel and LiveData 的依賴
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"

  implementation "android.arch.persistence.room:runtime:$room_version"
//這是room的依賴
  implementation "android.arch.persistence.room:rxjava2:$room_version"
    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "android.arch.persistence.room:guava:$room_version"
    // Test helpers
    testImplementation "android.arch.persistence.room:testing:$room_version"

網(wǎng)上有很多文章都是如此添加依賴,但是我們用到了ROOM這個(gè)組件,對于Kotlin是有問題的。因?yàn)镵otlin需要Kotlin-kapt插件,用來引入注解處理庫,java的話可以用 annotationProcessor,我們需要apply plugin: 'kotlin-kapt',然后在上面的依賴中添加

//java  用這個(gè)
// annotationProcessor "android.arch.persistence.room:compiler:$room_version"
    //kotlin 用這個(gè)
    kapt 'android.arch.persistence.room:compiler:1.0.0'

不然就會(huì)有xx_Impl does not exist
at android.arch.persistence.room.Room.getGeneratedImplementation的錯(cuò)誤,這個(gè)坑讓我爬了一上午,很是狼狽。而且谷歌demo的項(xiàng)目代碼和我們創(chuàng)建的有區(qū)別,所以會(huì)有很多注意不到的坑。

2、加載單一數(shù)據(jù)源的數(shù)據(jù)

image.png

Student.kt

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

StudentDao.kt
對數(shù)據(jù)庫數(shù)據(jù)的操作接口(增刪改查等方法類)

@Dao
interface StudentDao {
    /**
     * Room knows how to return a LivePagedListProvider, from which we can get a LiveData and serve
     * it back to UI via ViewModel.
     */
    @Query("SELECT * FROM Student ORDER BY name COLLATE NOCASE ASC")
    fun allStudentByName(): DataSource.Factory<Int, Student>

    @Insert
    fun insert(student: List<Student>)

    @Insert
    fun insert(student: Student)

    @Delete
    fun delete(student: Student)
}

Executors.kt
實(shí)用工具方法,用于在專用后臺線程上運(yùn)行塊,用于io/數(shù)據(jù)庫工作,下面的類里會(huì)用到

private val IO_EXECUTOR = Executors.newSingleThreadExecutor()

/**
 * Utility method to run blocks on a dedicated background thread, used for io/database work.
 */
fun ioThread(f : () -> Unit) {
    IO_EXECUTOR.execute(f)
}

StudentDb.kt
數(shù)據(jù)庫的創(chuàng)建類

@Database(entities = arrayOf(Student::class), version = 1)
abstract class StudentDb : RoomDatabase() {
    abstract fun studentDao(): StudentDao
    override fun clearAllTables() {
     
    }
    companion object {
        private var instance: StudentDb? = null
        @Synchronized
        fun get(context: Context): StudentDb {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                        StudentDb::class.java, "student.db")
                        .addCallback(object : RoomDatabase.Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                fillInDb(context.applicationContext)
                            }
                        }).build()
            }
            return instance!!
        }

        /**
         * fill database with list of cheeses
         */
        private fun fillInDb(context: Context) {
            // 在Room中的插入是在當(dāng)前線程上執(zhí)行的,因此我們將插入到后臺線程中
            ioThread {
                get(context).studentDao().insert(
                        CHEESE_DATA.map { Student(id = 0, name = it) })
            }
        }

    }
}


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 Student",
        "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 Student", "Bath Student", "Bavarian Bergkase",
        "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Student", "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 Student", "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 Student", "Cottage Student (Australian)",
        "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Student",
        "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 Student",
        "Frying Student", "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 Student", "Harbourne Blue", "Havarti",
        "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin",
        "Plateau de Herve", "Plymouth Student", "Podhalanski", "Poivre d'Ane", "Polkolbin",
        "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre",
        "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone",
        "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 Student", "Tibet", "Tillamook Cheddar",
        "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano");

StudentAdapter.kt
繼承PagedListAdapter,構(gòu)造屬于我們的Adapter

class StudentAdapter : PagedListAdapter<Student, StudentViewHolder>(diffCallback) {
    override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

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

    companion object {
        /**
         * 這個(gè)diff回調(diào)通知PagedListAdapter在新列表到來的時(shí)候如何計(jì)算列表差異
         *
         * 當(dāng)您使用“add”按鈕添加一個(gè)Student的時(shí)候,PagedListAdapter使用diffCallback t去檢測到與以前的Item的不同,所以它只需要重畫和重新綁定一個(gè)視圖。
         *
         * @see android.support.v7.util.DiffUtil
         */
        private val diffCallback = object : DiffUtil.ItemCallback<Student>() {
            override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem.id == newItem.id

            /**
             * 注意 kotlin的== 等價(jià)于java的equas()方法 
             */
            override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
                    oldItem == newItem
        }
    }
}

StudentViewHolder.kt

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

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

    /**
     * Items might be null if they are not paged in yet. PagedListAdapter will re-bind the
     * ViewHolder when Item is loaded.
     */
    fun bindTo(student : Student?) {
        this.student = student
        nameView.text = student?.name
    }
}

StudentiewModel.kt
繼承AndroidViewModel類,這也是JetPack推薦的視圖數(shù)據(jù)關(guān)聯(lián)方式,避免了Acticity和Fragment的任務(wù)繁重。Pagelist數(shù)據(jù)用LiveData包裝,這樣可以避免生命周期對數(shù)據(jù)的影響,減少內(nèi)存泄漏。

class StudentiewModel(app: Application) : AndroidViewModel(app) {
    val dao = StudentDb.get(app).studentDao()

    companion object {
      
        private const val PAGE_SIZE = 30

     /**如果啟用了占位符,PagedList將報(bào)告完整的大小,但是有的Item在onBind方法中可能會(huì)為空(PagedListAdapter在加載數(shù)據(jù)時(shí)觸發(fā)重新綁定)
如果禁用了占位符,onBind將永遠(yuǎn)不會(huì)收到null。如果你禁用占位符那么你應(yīng)該禁用滾動(dòng)條,不然隨著頁面已加載的增多,滾動(dòng)條將隨著新頁面的加載而抖動(dòng)
*/
        private const val ENABLE_PLACEHOLDERS = true
    }
#Config可以設(shè)置頁面顯示的數(shù)量,是否啟動(dòng)占位符等等
    val students = LivePagedListBuilder(dao.allStudentByName(), PagedList.Config.Builder()
                    .setPageSize(PAGE_SIZE)
                    .setEnablePlaceholders(ENABLE_PLACEHOLDERS)
                    .build()).build()
//直接插入到數(shù)據(jù)庫
    fun insert(text: CharSequence) = ioThread {
        dao.insert(Student(id = 0, name = text.toString()))
    }
//從數(shù)據(jù)庫刪除
    fun remove(cheese: Student) = ioThread {
        dao.delete(cheese)
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val viewModel by lazy(LazyThreadSafetyMode.NONE) {
        ViewModelProviders.of(this@MainActivity).get(StudentiewModel::class.java)
    }

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

        val adapter = StudentAdapter()
        cheeseList.adapter = adapter

   // 將adapter添加訂閱到ViewModel,當(dāng)列表改變時(shí),Adapter中的item會(huì)被刷新

       viewModel.students.observe(this, Observer(adapter::submitList))

        initAddButtonListener()
        initSwipeToDelete()
    }

    private fun initSwipeToDelete() {
        ItemTouchHelper(object : ItemTouchHelper.Callback() {
            // //使Item能向左或向右滑動(dòng)
            override fun getMovementFlags(recyclerView: RecyclerView,
                                          viewHolder: RecyclerView.ViewHolder): Int =
                    makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)

            override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
                                target: RecyclerView.ViewHolder): Boolean = false

          //當(dāng)項(xiàng)被滑動(dòng)時(shí),通過ViewModel刪除該項(xiàng)。列表項(xiàng)將會(huì)自動(dòng)刪除,因?yàn)閍dapter正在觀察這個(gè)Live List。
            override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
                (viewHolder as? StudentViewHolder)?.student?.let {
                    viewModel.remove(it)
                }
            }
        }).attachToRecyclerView(cheeseList)
    }

    private fun addStudnet() {
        val newCheese = inputText.text.trim()
        if (newCheese.isNotEmpty()) {
            viewModel.insert(newCheese)
            inputText.setText("")
        }
    }

    private fun initAddButtonListener() {
        addButton.setOnClickListener {
            addStudnet()
        }

        // 當(dāng)用戶點(diǎn)擊屏幕鍵盤上的“完成”按鈕時(shí),保存item.
        inputText.setOnEditorActionListener({ _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_DONE) {
                addStudnet()
                return@setOnEditorActionListener true
            }
            false // action that isn't DONE occurred - ignore
        })
        // 當(dāng)用戶單擊按鈕或按enter時(shí),保存該 item.
        inputText.setOnKeyListener({ _, keyCode, event ->
            if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
                addStudnet()
                return@setOnKeyListener true
            }
            false // event that isn't DOWN or ENTER occurred - ignore
        })
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <EditText
            android:id="@+id/inputText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="@string/add_cheese"
            android:imeOptions="actionDone"
            android:inputType="text"
            android:maxLines="1"/>
        <Button
            android:id="@+id/addButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0"
            android:text="@string/add"/>
    </LinearLayout>
    <android.support.v7.widget.RecyclerView
        android:id="@+id/cheeseList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
</LinearLayout>

student_item.xml

<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
                                    xmlns:app="http://schemas.android.com/apk/res-auto"
                                    android:layout_width="match_parent"
                                    android:layout_height="wrap_content"
                                    android:orientation="vertical"
                                    app:cardUseCompatPadding="true">
    <TextView android:id="@+id/name"
              android:layout_width="match_parent" android:layout_height="wrap_content"
              android:layout_marginBottom="@dimen/card_vertical_margin"
              android:layout_marginTop="@dimen/card_vertical_margin"/>
</android.support.v7.widget.CardView>

ok,到此demo完工,大家可以試試效果,不過從代碼量來看kotlin比java簡潔太多了,不過這demo里面基本把JetPack里的LiveData,ViewModel,Paging,Room都用到了,很多細(xì)節(jié)和用法,需要大家連貫起來學(xué)習(xí)。從MainActivity的代碼來看很簡潔,意思也很明確,比java的閱讀性要高很多,不過如果沒有學(xué)JetPack的同學(xué)看到這些代碼我想內(nèi)心是MMP的。

WorkManager

WorkManager 可以輕松指定可延遲的異步任務(wù)以及何時(shí)運(yùn)行。這些API可讓我們創(chuàng)建任務(wù)并將其交給WorkManager,以便立即或在適當(dāng)?shù)臅r(shí)間運(yùn)行。例如,應(yīng)用程序可能需要不時(shí)從網(wǎng)絡(luò)下載新資源。使用這些類,可以設(shè)置一個(gè)任務(wù),選擇適合它運(yùn)行的環(huán)境(例如“僅在設(shè)備充電和聯(lián)網(wǎng)時(shí)”),并在符合條件時(shí)將其交給WorkManager運(yùn)行。即使您的應(yīng)用程序強(qiáng)制退出或設(shè)備重新啟動(dòng),該任務(wù)仍可保證運(yùn)行。

注意:WorkManager適用于需要保證即使應(yīng)用退出也能運(yùn)行系統(tǒng)的任務(wù),例如將應(yīng)用數(shù)據(jù)上傳到服務(wù)器。如果應(yīng)用程序進(jìn)程消失,它不適用于可以安全終止的進(jìn)程內(nèi)后臺工作; 對于這樣的情況,推薦使用ThreadPools。

以上是官方的介紹,那么我們就來白話一下

谷歌出這個(gè)到底是干嘛??? 不是有JobScheduler, AlarmManger,AsyncTask, ThreadPool, RxJava等等了嗎?怎么又來一套?
其實(shí)不是的,這回谷歌真的替我們做了很多我們平時(shí)比較頭疼的東西,什么呢?WorkManager的作用是在應(yīng)用退出或者某些原因終止了之后,任務(wù)還可以進(jìn)行,至于采取什么方法,這個(gè)我們不需要去管,WorkManager都替我們處理了。WorkManage會(huì)根據(jù)系統(tǒng)版本來選擇用JobScheduler, Firebase的JobDispatcher, 或是AlarmManager。
至于AsyncTask, ThreadPool, RxJava這三個(gè)和WorkManager是沒有沖突的,人家WorkManager是為了保證任務(wù)的可靠運(yùn)行,但是AsyncTask, ThreadPool, RxJava,這三兄弟app退出人家就不干活了,和WorkManager的職責(zé)有著本事區(qū)別。一個(gè)是風(fēng)雨無阻完成任務(wù),一個(gè)有點(diǎn)事就撂挑子不干活了。


調(diào)用流程

我們表揚(yáng)下WorkManager的好處
1、 易于調(diào)度

  • 后臺工作程序只能在特定條件下調(diào)度任務(wù)(例如只有設(shè)備處于充電狀態(tài),該任務(wù)才會(huì)運(yùn)行)
  • 一旦你調(diào)度了任務(wù),就可以忘記任務(wù)的存在,調(diào)度程序應(yīng)該提供在所需條件匹配的情況下保證任務(wù)運(yùn)行。
  • 每個(gè)任務(wù)可以與另外一個(gè)任務(wù)并行鏈接,以并行或順序運(yùn)行多個(gè)任務(wù)。

2、易于取消

  • 你必須擁有對任務(wù)的控制權(quán),調(diào)度程序應(yīng)該提供API以輕松取消計(jì)劃任務(wù)。

3、易于查詢

  • 你的應(yīng)用程序可能會(huì)需要顯示任務(wù)的狀態(tài)。
  • 假設(shè)你要上傳照片并且需要在界面上顯示上傳的百分比。
  • 調(diào)度程序必須提供API以輕松獲取任務(wù)的當(dāng)前狀態(tài),如果任務(wù)完成之后可以傳遞一些結(jié)果數(shù)據(jù),那就更棒了!

4、支持所有的Android版本

  • 調(diào)度程序API應(yīng)該在所有的Android版本中都一樣。

WorkManager由以下幾個(gè)部分組成

  • Worker:指定您需要執(zhí)行的任務(wù)。WorkManager API包含一個(gè)抽象Worker類。你擴(kuò)展這個(gè)類并且在這里執(zhí)行這個(gè)工作。

  • WorkRequest:代表一個(gè)單獨(dú)的任務(wù)。至少,WorkRequest對象指定應(yīng)該執(zhí)行任務(wù)的Worker類。但是,您也可以向WorkRequest對象添加細(xì)節(jié),指定任務(wù)應(yīng)該運(yùn)行的環(huán)境。每個(gè)工作請求都有一個(gè)自動(dòng)生成的唯一ID;您可以使用ID來執(zhí)行諸如取消排隊(duì)任務(wù)或獲取任務(wù)的狀態(tài)等操作。WorkRequest是一個(gè)抽象類;在您的代碼中,您將使用一個(gè)直接子類,一個(gè)timeworkrequest(一次性)或PeriodicWorkRequest(周期性)。

    • WorkRequest.Builder:創(chuàng)建工作請求對象的構(gòu)造類。同樣,您將使用一個(gè)子類,OneTimeWorkRequest。建筑商或PeriodicWorkRequest.Builder。
    • Constraints:指定任務(wù)運(yùn)行時(shí)間的限制(例如,“僅在連接到網(wǎng)絡(luò)時(shí)”)。
  • WorkManager:排隊(duì)和管理工作請求。你傳遞你的WorkRequest 對象WorkManager來排隊(duì)的任務(wù)。WorkManager調(diào)度任務(wù)的方式是分散系統(tǒng)資源的負(fù)載,同時(shí)遵守您指定的約束條件。

  • WorkStatus:包含有關(guān)特定任務(wù)的信息。包含關(guān)于特定任務(wù)的信息。WorkManager為每個(gè)WorkRequest對象提供一個(gè)LiveData。LiveData保存一個(gè)WorkStatus對象;通過觀察這個(gè)LiveData,您可以確定任務(wù)的當(dāng)前狀態(tài),并在任務(wù)完成后獲得任何返回值。

了解完WorkManager,該擼代碼了,前方高能依然是Kotlin。請抓好安全帶。

這個(gè)小demo的功能是執(zhí)行延時(shí)任務(wù),獲取廣告信息,然后通知UI顯示廣告,看看能不能做一些無賴的事情,比如一有廣告直接調(diào)起app顯示。
1、添加依賴

implementation "android.arch.work:work-runtime-ktx:1.0.0-alpha01"

AdWorker.kt

做了一個(gè)任務(wù)的開關(guān),inputData是輸入信息outputData是對外輸出信息,也就是根據(jù)inputData的信息去做不同的事情,然后把結(jié)果通過outputData送出來。
WorkerResult的狀態(tài) FAILURE,RETRY,SUCCESS;
RETRY:WorkManager可以再次重試該工作
FAILURE:發(fā)生了一個(gè)或多個(gè)錯(cuò)誤
SUCCESS:任務(wù)成功完成

class AdWorker : Worker() {
    override fun doWork(): WorkerResult {
        //輸入data
        val is_open = this.inputData.getBoolean("is_open_ad", false)
        if (is_open) {
        //模擬延時(shí)操作
            Thread.sleep(10000)
            val ad = getAd()
            outputData = Data.Builder().putString("key_ad", ad).build()

            Log.e("ad", "SUCCESS")
            return WorkerResult.SUCCESS
        } else {
            Log.e("ad", "FAILURE:")
            return WorkerResult.FAILURE
        }

    }

    private fun getAd(): String {
        return "我是廣告君,沒進(jìn)剛哥知識星球的趕緊加入了啊~~" + System.currentTimeMillis()
    }

}

AdEngine.kt

任務(wù)的調(diào)度類 我們在此用的是OneTimeWorkRequestBuilder一次性調(diào)用。
如果我們需要重復(fù)執(zhí)行一項(xiàng)任務(wù)的話使用PeriodicWorkRequest.Builder

不過這個(gè)有個(gè)坑在等著大家。使用PeriodicWorkRequest的時(shí)候outputdata的里面的值是空的,上網(wǎng)查了很多資料都沒有寫出這個(gè)問題,但是OneTimeWorkRequestBuilder確實(shí)沒有問題的,希望各位大神可以給解答一下這個(gè)問題。

這里可以往AdWorker進(jìn)行setInputData,數(shù)據(jù)輸入。然后加入任務(wù)隊(duì)列。我們在此保存好任務(wù)ID,根據(jù)這個(gè)ID才可以找到這個(gè)任務(wù)。

那么只有這一個(gè)方法才能找到任務(wù)嗎?答案是否定的。
我們可以標(biāo)記任務(wù) .addTag("tag_ad")
這個(gè)方法來獲取任務(wù) WorkManager.getInstance().getStatusesByTag("tag_id")

約束:
定義約束條件以告訴WorkManager合適安排任務(wù)執(zhí)行,如果沒有提供任何約束條件,那么該任務(wù)將立即運(yùn)行。
以下是僅在設(shè)備充電和設(shè)備是否為空閑才運(yùn)行任務(wù)的約束

   val myConstraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()
image.png
class AdEngine {
    fun schedulAd() {
        val adReauest = OneTimeWorkRequestBuilder<AdWorker>()
                .setInputData(
                        Data.Builder().putBoolean("is_open_ad", true)
                                .build()
                )
 .setConstraints(myConstraints)
 .addTag("tag_ad")
.build()
        WorkManager.getInstance().enqueue(adReauest)
        //保存任務(wù)ID
        val adRequestId = adReauest.id
        var arid by Preference("adRequestId", "")
        arid = adRequestId.toString()

    }
//這是約束條件
 @RequiresApi(Build.VERSION_CODES.M)
    val myConstraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()
}

Preference.kt

SharedPreferences在kotlin的工具類

class Preference<T>(val name: String, private val default: T) {
    private val prefs: SharedPreferences by lazy { App.instance.applicationContext.getSharedPreferences(name, Context.MODE_PRIVATE) }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        Log.i("info", "調(diào)用$this 的getValue()")
        return getSharePreferences(name, default)
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        Log.i("info", "調(diào)用$this 的setValue() value參數(shù)值為:$value")
        putSharePreferences(name, value)
    }

    @SuppressLint("CommitPrefEdits")
    private fun putSharePreferences(name: String, value: T) = with(prefs.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type of data cannot be saved!")
        }.apply()
    }

    @Suppress("UNCHECKED_CAST")
    private fun getSharePreferences(name: String, default: T): T = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type of data cannot be saved!")
        }
        return res as T
    }
}

app.kt

class App :Application(){
    companion object {// 伴生對象  java里的靜態(tài)屬性
    lateinit var instance: App
        private set
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

Main2Activity.kt

調(diào)用adEngine.schedulAd()方法,開啟任務(wù),然后通過 getStatusById返回LiveData<WorkStatus>,添加到LifecycleOwner(AppCompatActivity本身就是一個(gè)LifecycleOwner),我們就可以根據(jù)state的改變,改變TextView的內(nèi)容。顯示我們的廣告語。結(jié)果我們會(huì)發(fā)現(xiàn)并不會(huì)直接調(diào)起來APP,這是為什么呢?
因?yàn)槲覀冇玫氖荓iveData,當(dāng)Activity銷毀后,LiveData處于未激活的狀態(tài),不回去接受數(shù)據(jù)的改變,只有Activity從新獲得生命周期后,LiveData才會(huì)接受數(shù)據(jù)的變化,從而受到LiveData的通知,所以想要做壞事的同學(xué),這條路行不通的,谷歌只是為了保證一些任務(wù)的可靠性,而不是保證你的App的生命。

當(dāng)然任務(wù)可以執(zhí)行就可以取消
WorkManager.getInstance().cancelByWorkId(uuid);

鏈?zhǔn)秸{(diào)用

WorkManager.getInstance(). 
      beginWith(workA1,workA2,workA3)
      .then 
      (workB)
      .then(workC1,workC2).enqueue();

image.png

這樣就完了嗎?還有更復(fù)雜的鏈?zhǔn)秸{(diào)用WorkContinuation大家可以自行學(xué)習(xí)下。

class Main2Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        button.setOnClickListener(View.OnClickListener {
            val adEngine: AdEngine = AdEngine()
            adEngine.schedulAd()

            showAd(this, textad)
        })

        showAd(this, textad)
    }


}

fun showAd(lifeowner: LifecycleOwner, textad: TextView) {

    var arid by Preference("adRequestId", "")
    if (!arid.equals("")) {
        val uuid = UUID.fromString(arid)
//   public abstract LiveData<WorkStatus> getStatusById(@NonNull UUID id);
        WorkManager.getInstance().getStatusById(uuid)
                .observe(lifeowner, android.arch.lifecycle.Observer<WorkStatus> { state ->

                    if (state != null && state.state.isFinished) {
                        val adResult = state.outputData.getString("key_ad", "無")
                        textad.text = adResult

                    }
                })

    }

}

Slices

Slices 在國內(nèi)應(yīng)用的范圍不廣,重要是因?yàn)镾lices是 Google Assistant 的延伸,谷歌希望使用者能過快速到達(dá)App里面的某個(gè)特點(diǎn)功能,舉一例子就是,你對Google Assistant說你要回家,那么以前可能只會(huì)出現(xiàn)滴滴,Uber的選項(xiàng),但是引進(jìn)Slices之后會(huì)顯示更加詳細(xì)的數(shù)據(jù)列表,比如滴滴item下會(huì)出現(xiàn)到家多少距離,多少錢,是否立即打車等等。Google Assistant 在國內(nèi)不好用,但是谷歌有這個(gè)功能開源我們自己其實(shí)也可以去實(shí)現(xiàn),可能小米會(huì)把這個(gè)功能給小艾同學(xué)吧。

開始搭建我們的Slices吧。
注意注意:開發(fā)環(huán)境必須是Android Studio 3.2 以及以上,最低版本Android 4.4 (API level 19) ,我們可以從官網(wǎng)下載Android Studio 3.2 ,圖標(biāo)是黃色的,可以和我們之前的Android Studio 共存,相互沒有干擾,講實(shí)話Android Studio 3.2 真的處處是坑,特別和Kotlin配合,那真的是一言難盡,苦不堪言。

no.1

image.png

如果沒有這個(gè)選項(xiàng)的話

 <provider
            android:name=".MySliceProvider"
            android:authorities="com.simple.slicesapplication"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.app.slice.category.SLICE" />

                <data
                    android:host="simple.com"
                    android:pathPrefix="/ssy"
                    android:scheme="http" />
            </intent-filter>
        </provider>

值得一說的是 依賴的版本真的很坑,注意是否依賴了正確的版本

implementation "androidx.slice:slice-core:1.0.0-alpha1"
implementation "androidx.slice:slice-builders:1.0.0-alpha1"

no.2

class MySliceProvider : SliceProvider() {
    /**
     * Instantiate any required objects. Return true if the provider was successfully created,
     * false otherwise.
     */
    override fun onCreateSliceProvider(): Boolean {
        return true
    }

    override fun onMapIntentToUri(intent: Intent?): Uri {
        // Note: implementing this is only required if you plan on catching URL requests.
        // This is an example solution.
        var uriBuilder: Uri.Builder = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
        if (intent == null) return uriBuilder.build()
        val data = intent.data
        if (data != null && data.path != null) {
            val path = data.path.replace("/", "")
            uriBuilder = uriBuilder.path(path)
        }
        val context = context
        if (context != null) {
            uriBuilder = uriBuilder.authority(context.getPackageName())
        }
        return uriBuilder.build()
    }

   
    override fun onBindSlice(sliceUri: Uri): Slice? {
        val context = getContext() ?: return null
        return if (sliceUri.path == "/") {
            // Path recognized. Customize the Slice using the androidx.slice.builders API.
            // Note: ANR and StrictMode are enforced here so don't do any heavy operations. 
            // Only bind data that is currently available in memory.
            ListBuilder(context, sliceUri)
                    .addRow { it.setTitle("URI found.") }
                    .build()
        } else {
            // Error: Path not found.
            ListBuilder(context, sliceUri)
                    .addRow { it.setTitle("URI not found.") }
                    .build()
        }
    }

  
    override fun onSlicePinned(sliceUri: Uri?) {
    }

    override fun onSliceUnpinned(sliceUri: Uri?) {
        // Remove any observers if necessary to avoid memory leaks.
    }
}

綁定Slice

override fun onBindSlice(sliceUri: Uri): Slice? {
  val activityAction = createActivityAction()
    return if (sliceUri.path == "/ssy") {
        ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow { it.setTitle("URI found. 我是標(biāo)題")
                 it.setSubtitle("我是子標(biāo)題")
//設(shè)置Action
                 it.setPrimaryAction(activityAction)}
 }
                .build()
    } else {
        ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow { it.setTitle("URI not found.") }
                .build()
    }
}
//創(chuàng)建Action
 fun createActivityAction(): SliceAction {
        val intent = Intent(context, MainActivity::class.java)
        return SliceAction(PendingIntent.getActivity(context, 0, intent, 0),
                IconCompat.createWithResource(context, R.drawable.ic_launcher_background),
                "Open MainActivity."
        )
    }

將URL轉(zhuǎn)變成content URI


    override fun onMapIntentToUri(intent: Intent?): Uri {
       
        var uriBuilder: Uri.Builder = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
        if (intent == null) return uriBuilder.build()
        val data = intent.data
        if (data != null && data.path != null) {
            val path = data.path.replace("/ssy", "")
            uriBuilder = uriBuilder.path(path)
        }
        val context = context
        if (context != null) {
            uriBuilder = uriBuilder.authority(context.getPackageName())
        }
        return uriBuilder.build()
    }

這樣我們我們就完成了一個(gè)簡單的Slice,什么?怎么用?下載這個(gè)slice-viewer.apk充當(dāng)Google Assistant吧,然后我們需要做的是
adb shell am start -a android.intent.action.VIEW -d slice-content://com.simple.slicesapplication/ssy
藍(lán)色的是我們自己的Content Uri,這樣就會(huì)在slice-viewer.apk打開我們的slice了。

image.png

點(diǎn)擊會(huì)跳轉(zhuǎn)到我們的app。

什么?覺得不夠豐富?來來來,阿秀同志請坐下,有好東西給你

創(chuàng)建一個(gè)帶有Togglebutton和進(jìn)度條 的Slice和action,只需要替換我們上面的Slice和action即可

fun createBrightnessSlice(sliceUri: Uri): Slice {
    val toggleAction =
            SliceAction(createToggleIntent(), "Toggle adaptive brightness", true)
    return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
            .addRow {
                it.apply {
                    setTitle("Adaptive brightness")
                    setSubtitle("Optimizes brightness for available light")
//這是togglebutton
                    setPrimaryAction(toggleAction)
                }
            }.addInputRange {
                it.apply {
//這個(gè)是進(jìn)度條
                    setInputAction(brightnessPendingIntent)
                    setMax(100)
                    setValue(45)
                }
            }.build()
}

fun createToggleIntent(): PendingIntent {
    val intent = Intent(context, MyBroadcastReceiver::class.java)
    return PendingIntent.getBroadcast(context, 0, intent, 0)
}

class MyBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.hasExtra(Slice.EXTRA_TOGGLE_STATE)) {
            Toast.makeText(context, "Toggled:  " + intent.getBooleanExtra(
                    Slice.EXTRA_TOGGLE_STATE, false),
                    Toast.LENGTH_LONG).show()
        }
    }

    companion object {
        const val EXTRA_MESSAGE = "message"
    }

啥?就這些?你想點(diǎn)擊Slice后,在Slice上面動(dòng)態(tài)修改一些信息,這是你的應(yīng)用嗎?咋想這么多呢?好吧,怕你了,來來來(谷歌官網(wǎng)的代碼有坑,真的有坑)

  fun createDynamicSlice(sliceUri: Uri): Slice {
        return when (sliceUri.path) {
            "/ssy" -> {
                val toastAndIncrementAction = SliceAction(createToastAndIncrementIntent("Item clicked"),
                        IconCompat.createWithResource(context, R.drawable.no1), "Increment.")
                ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                        .addRow {
                            it.apply {
                                setPrimaryAction(toastAndIncrementAction)
                                setTitle("Count: ${MyBroadcastReceiver.receivedCount}")
                                setSubtitle("Click me")
                            }
                        }
                        .build()
            }
            else -> {
                ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                        .addRow { it.setTitle("URI not found.") }
                        .build()
            }
        }
    }

    fun createToastAndIncrementIntent(s: String): PendingIntent {
        return PendingIntent.getBroadcast(context, 0,
                Intent(context, MyBroadcastReceiver::class.java)
                        .putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, s), 0)
    }

class MyBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.hasExtra(Slice.EXTRA_TOGGLE_STATE)) {
            Toast.makeText(context, "Toggled:  " + intent.getBooleanExtra(
                    Slice.EXTRA_TOGGLE_STATE, false),
                    Toast.LENGTH_LONG).show()
            receivedCount++;
            context.contentResolver.notifyChange(sliceUri, null)
        }
    }


    companion object {
        var receivedCount = 0
        val sliceUri = Uri.parse("content://com.simple.slicesapplication/ssy")
        const val EXTRA_MESSAGE = "message"
    }
}

這回行了吧,什么布局能不能改?啥你想加廣告?真是,好吧好吧

Slice templates

這個(gè)大哥叫切片模板

定義你的切片模板

切片是通過使用ListBuilder構(gòu)造的 。ListBuilder允許您添加列表中顯示的不同類型的行。本節(jié)介紹每種行類型及其構(gòu)造方式。

SliceAction

Slice模板的最基本元素是 SliceAction。SliceAction包含一個(gè)標(biāo)簽以及一個(gè)PendingIntent, 并且是以下之一:

  • 圖標(biāo)按鈕
  • 默認(rèn)切換
  • 自定義切換(具有開/關(guān)狀態(tài)的可繪圖)

切片模板

SliceAction由本節(jié)其余部分描述的模板構(gòu)建器使用。SliceAction可以定義一個(gè)圖像模式,用于確定如何為該動(dòng)作呈現(xiàn)圖像:

  • ICON_IMAGE:尺寸小,可上色
  • SMALL_IMAGE:體積小,不可著色
  • LARGE_IMAGE:最大的尺寸和不可著色

HeaderBuilder

在大多數(shù)情況下,您應(yīng)該使用HeaderBuilder為您的模板設(shè)置標(biāo)題 。標(biāo)頭可以支持以下內(nèi)容:

  • 標(biāo)題
  • 字幕
  • 摘要字幕
  • 主要行動(dòng)

下面顯示了一些示例頭部配置。請注意,灰色框顯示潛在的圖標(biāo)和填充位置:


image.png
標(biāo)題在不同的表面上呈現(xiàn)

當(dāng)需要切片時(shí),顯示表面決定如何渲染切片。請注意,托管表面之間的渲染可能有所不同。

在較小的格式中,通常只顯示標(biāo)題(如果存在)。如果您為標(biāo)題指定了摘要,則會(huì)顯示摘要文本而不是字幕文本。

如果您沒有在模板中指定標(biāo)題,則通常會(huì)顯示添加到ListBuilder的第一行


image.png

那我們就試一下

fun createSliceWithHeader(sliceUri: Uri): Slice   =
            ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                    .setAccentColor(0xff0F9D) // Specify color for tinting icons
                    .setHeader {
                        it.apply {
                            setTitle("Get a ride")
                            setSubtitle("Ride in 4 min")
                            setSummary("Work in 1 hour 45 min | Home in 12 min")
                        }
                    }.addRow {
                        it.apply {
                            setTitle("Home")
                            setSubtitle("12 miles | 12 min | $9.00")
                            addEndItem(IconCompat.createWithResource(context, R.drawable.ic_launcher_background), SliceHints.ICON_IMAGE)
                        }
                    }
                    .build()
}
image.png

感覺有點(diǎn)意思了,不過還是沒有想象中該有的樣子。上下標(biāo)題差不多有了,右邊的icon可以多幾個(gè)嗎?

fun createSliceWithActionInHeader(sliceUri: Uri): Slice {
    // Construct our slice actions.
    val noteAction = SliceAction(takeNoteIntent,
            IconCompat.createWithResource(context, R.drawable.a),
            ICON_IMAGE, "Take note")

    val voiceNoteAction = SliceAction(voiceNoteIntent,
            IconCompat.createWithResource(context, R.drawable.b),
            ICON_IMAGE,
            "Take voice note")

    val cameraNoteAction = SliceAction(cameraNoteIntent,
            IconCompat.createWithResource(context, R.drawable.c),
            ICON_IMAGE,
            "Create photo note")


    // Construct the list.
    return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
            .setAccentColor(0xfff4b4) // Specify color for tinting icons
            .setHeader {
                it.apply {
                    setTitle("Create new note")
                    setSubtitle("Easily done with this note taking app")
                }
            }
            .addAction(noteAction)
            .addAction(voiceNoteAction)
            .addAction(cameraNoteAction)
            .build()
}

image.png

最后來個(gè)全家桶


image.png
fun createSliceWithGridRow(sliceUri: Uri): Slice {
        // Create the parent builder.
        val icon_a = IconCompat.createWithResource(context, R.mipmap.a)
        val icon_b = IconCompat.createWithResource(context, R.mipmap.b)
        val icon_c = IconCompat.createWithResource(context, R.mipmap.c)
        val icon_d = IconCompat.createWithResource(context, R.mipmap.d)
        val intent = Intent(context, MainActivity::class.java)
        val brightnessPendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
        return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .setHeader {
                    it.apply {
                        setTitle("玩具")
                        setPrimaryAction(SliceAction(brightnessPendingIntent, icon_c, "Famous restaurants"))
                    }
                }
                .addRow {
                    it.apply {

                        setSubtitle("12 miles | 12 min | $9.00")
                        addEndItem(IconCompat.createWithResource(context, R.mipmap.b), SliceHints.LARGE_IMAGE)
                    }
                }

                .addGridRow {
                    it.apply {
                        addCell {
                            it.apply {
                                addImage(icon_a, LARGE_IMAGE)
                                addTitleText("積木")
                                addText("¥100")
                                setContentIntent(brightnessPendingIntent)
                            }
                        }
                        addCell {
                            it.apply {
                                addImage(icon_b, LARGE_IMAGE)
                                addTitleText("搖桿")
                                addText("¥200")
                                setContentIntent(brightnessPendingIntent)
                            }
                        }
                        addCell {
                            it.apply {
                                addImage(icon_c, LARGE_IMAGE)
                                addTitleText("十合一卡")
                                addText("¥300")
                                setContentIntent(brightnessPendingIntent)
                            }
                        }
                        addCell {
                            it.apply {
                                addImage(icon_d, LARGE_IMAGE)
                                addTitleText("手柄t")
                                addText("¥200")
                                setContentIntent(brightnessPendingIntent)
                            }
                        }
                    }
                }
                .build()
    }

本篇把Paging、WorkManager、Slices的概念和簡單的應(yīng)用梳理了一遍,其中也發(fā)現(xiàn)了很多坑,網(wǎng)上好多文章,只是講原理不寫實(shí)例,或者有的人寫了實(shí)例但是自己沒有驗(yàn)證過,就是谷歌文檔也有很多不清楚的地方,特別是Kotlin的依賴的配置,所幸把大部分的問題解決 了,不過還有一些問題依然不清楚,查看了官方文檔,谷歌了眾多文章可是資料很少,希望有小伙伴可以給解惑。一起學(xué)習(xí)。

大家可以點(diǎn)個(gè)關(guān)注,告訴我大家想要深入探究哪些問題,希望看到哪方面的文章,我可以免費(fèi)給你寫專題文章。。哈哈。。。
希望大家多多支持。。你的一個(gè)關(guān)注,是我堅(jiān)持的最大動(dòng)力。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容