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

本文由玉刚说写作平台提供写作赞助,版权归玉刚说微信公众号所有
原作者:Mr.s(猪_队友)
版权声明:未经玉刚说许可,不得以任何形式转载

今年谷歌I/O大会,谷歌发布了 Android Jetpack.这是新一代组件、工具和架构指导,用谷歌官方的话就是旨在加快开发者的 Android 应用开发速度。Android Jetpack 组件将现有的支持库与架构组件联系起来,并将它们分成四个类别:

结构图.png

Android Jetpack 组件以“未捆绑的”库形式提供,这些库不是基础 Android 平台的一部分。这就意味着,我们可以根据自己的需求采用每一个组件。在新的 Android Jetpack 功能发布后,我们可以将其添加到自己的应用中,将我们的应用部署到应用商店并向用户提供新功能,如果我们的行动足够快,所有这些可以在一天内完成!

那么谷歌发布JetPack的目的是什么呢?

三大优点.png
  • 加速开发
    组件可单独采用,但可以一起使用,同时利用Kotlin语言功能,提高工作效率。(ps:Kotlin可以大大减少代码量,据说可以减少到1/3,请一定要学习kotlin)
  • 减少并消除样板代码
    Android Jetpack管理诸如后台任务,导航和生命周期管理等繁琐的活动,因此我们可以专注提高应用品质等其他方面。
  • 构建高品质,强大的应用
    以现代设计实践为基础,Android Jetpack组件可降低崩溃次数并减少内存泄漏,并向后兼容。

除了这三点外,谷歌想给开发者定制一套标准,比如框架的标准,我们平时MVC,MVP,MVVM等等,现在谷歌自己搞了一套MVP-CLEAN
我们从JetPack的四大部分也可以看出,谷歌想要结束混乱的局面,给开发者一个规范,这个对我们开发者也是一件好事,跟着官方走总不会差的。更多参见App体系结构指南

那么我在这一篇给大家介绍一下Paging、WorkManager、Slices和他们的使用方式,篇幅较长,请大家酌情找尿点。

Paging(分页)

背景:
很多应用程序从包含大量项目的数据源中获取数据,但一次只显示一小部分数据。加载应用程序中显示的数据可能很大并且代价高昂,因此要避免一次下载,创建或呈现太多数据。为了可以更轻松地在我们的应用程序中逐渐加载数据谷歌方法提供了这个组件,可以很容易地加载和现在的大数据集与我们的RecyclerView快速,无限滚动。它可以从本地存储,网络或两者加载分页数据,并且可以让我们自定义如何加载内容。它可以与Room,LiveData和RxJava一起使用。
Paging Libray分为三部分:DataSource, PagedList, PagedAdapter

Paging Libraray Diagram

DataSource:

它就像是一个抽水泵,而不是真正的水源,它负责从数据源加载数据,可以看成是Paging Library与数据源之间的接口。
Datasource<Key, Value>是数据源相关的类,Key是加载数据的条件信息,Value是返回结果, 针对不同场景我们需要用不同的Datasource,Paging提供了三个子类来供我们选择。

  • PageKeyedDataSource<Key, Value> :适用于目标数据根据页信息请求数据的场景,即Key 字段是页相关的信息。比如请求的数据的参数中包含类似next/previous的信息。
  • ItemKeyedDataSource<Key, Value> :适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
  • PositionalDataSource<T>:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。

PagedList:

它就像是一个蓄水池,DataSource抽的水放到PagedList中。它是List的子类,它包含着我们的数据并告诉数据源何时加载数据。我们也可以配置一次加载多少数据,以及应该预取多少数据。它提供适配器的更新作为页面中加载的数据。
PagedList有五个重要的参数:

  • mMainThreadExecutor: 主线程的Excutor, 用于将结果post到主线程。

  • mBackgroundThreadExecutor: 后台线程的Excutor.

  • BoundaryCallback:加载Datasource中的数据加载到边界时的回调.

  • Config: 配置PagedList从Datasource加载数据的方式, 其中包含以下属性:

    • pageSize:设置每页加载的数量
    • prefetchDistance:预加载的数量
    • initialLoadSizeHint:初始化数据时加载的数量
    • enablePlaceholders:当item为null是否使用PlaceHolder展示
    • PagedStorage<T>: 用于存储加载到的数据,它是真正的蓄水池所在,它包 含一个ArrayList<List<T>> 对象mPages,按页存储数据。

PagedListAdapter:

这个类是RecyclerView.adapter的实现,它提供来自PagedList的数据并以DiffUtil作为参数来计算数据的差异并为你做所有的更新工作。

看十遍不如敲一遍,搞起,搞起~~(本篇全部用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"

网上有很多文章都是如此添加依赖,但是我们用到了ROOM这个组件,对于Kotlin是有问题的。因为Kotlin需要Kotlin-kapt插件,用来引入注解处理库,java的话可以用 annotationProcessor,我们需要apply plugin: 'kotlin-kapt',然后在上面的依赖中添加

//java  用这个
// annotationProcessor "android.arch.persistence.room:compiler:$room_version"
    //kotlin 用这个
    kapt 'android.arch.persistence.room:compiler:1.0.0'

不然就会有xx_Impl does not exist
at android.arch.persistence.room.Room.getGeneratedImplementation的错误,这个坑让我爬了一上午,很是狼狈。而且谷歌demo的项目代码和我们创建的有区别,所以会有很多注意不到的坑。

2、加载单一数据源的数据

image.png

Student.kt

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

StudentDao.kt
对数据库数据的操作接口(增删改查等方法类)

@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
实用工具方法,用于在专用后台线程上运行块,用于io/数据库工作,下面的类里会用到

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
数据库的创建类

@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中的插入是在当前线程上执行的,因此我们将插入到后台线程中
            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,构造属于我们的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 {
        /**
         * 这个diff回调通知PagedListAdapter在新列表到来的时候如何计算列表差异
         *
         * 当您使用“add”按钮添加一个Student的时候,PagedListAdapter使用diffCallback t去检测到与以前的Item的不同,所以它只需要重画和重新绑定一个视图。
         *
         * @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的== 等价于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推荐的视图数据关联方式,避免了Acticity和Fragment的任务繁重。Pagelist数据用LiveData包装,这样可以避免生命周期对数据的影响,减少内存泄漏。

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

    companion object {
      
        private const val PAGE_SIZE = 30

     /**如果启用了占位符,PagedList将报告完整的大小,但是有的Item在onBind方法中可能会为空(PagedListAdapter在加载数据时触发重新绑定)
如果禁用了占位符,onBind将永远不会收到null。如果你禁用占位符那么你应该禁用滚动条,不然随着页面已加载的增多,滚动条将随着新页面的加载而抖动
*/
        private const val ENABLE_PLACEHOLDERS = true
    }
#Config可以设置页面显示的数量,是否启动占位符等等
    val students = LivePagedListBuilder(dao.allStudentByName(), PagedList.Config.Builder()
                    .setPageSize(PAGE_SIZE)
                    .setEnablePlaceholders(ENABLE_PLACEHOLDERS)
                    .build()).build()
//直接插入到数据库
    fun insert(text: CharSequence) = ioThread {
        dao.insert(Student(id = 0, name = text.toString()))
    }
//从数据库删除
    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,当列表改变时,Adapter中的item会被刷新

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

        initAddButtonListener()
        initSwipeToDelete()
    }

    private fun initSwipeToDelete() {
        ItemTouchHelper(object : ItemTouchHelper.Callback() {
            // //使Item能向左或向右滑动
            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

          //当项被滑动时,通过ViewModel删除该项。列表项将会自动删除,因为adapter正在观察这个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()
        }

        // 当用户点击屏幕键盘上的“完成”按钮时,保存item.
        inputText.setOnEditorActionListener({ _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_DONE) {
                addStudnet()
                return@setOnEditorActionListener true
            }
            false // action that isn't DONE occurred - ignore
        })
        // 当用户单击按钮或按enter时,保存该 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都用到了,很多细节和用法,需要大家连贯起来学习。从MainActivity的代码来看很简洁,意思也很明确,比java的阅读性要高很多,不过如果没有学JetPack的同学看到这些代码我想内心是MMP的。

WorkManager

WorkManager 可以轻松指定可延迟的异步任务以及何时运行。这些API可让我们创建任务并将其交给WorkManager,以便立即或在适当的时间运行。例如,应用程序可能需要不时从网络下载新资源。使用这些类,可以设置一个任务,选择适合它运行的环境(例如“仅在设备充电和联网时”),并在符合条件时将其交给WorkManager运行。即使您的应用程序强制退出或设备重新启动,该任务仍可保证运行。

注意:WorkManager适用于需要保证即使应用退出也能运行系统的任务,例如将应用数据上传到服务器。如果应用程序进程消失,它不适用于可以安全终止的进程内后台工作; 对于这样的情况,推荐使用ThreadPools

以上是官方的介绍,那么我们就来白话一下

谷歌出这个到底是干嘛啊? 不是有JobScheduler, AlarmManger,AsyncTask, ThreadPool, RxJava等等了吗?怎么又来一套?
其实不是的,这回谷歌真的替我们做了很多我们平时比较头疼的东西,什么呢?WorkManager的作用是在应用退出或者某些原因终止了之后,任务还可以进行,至于采取什么方法,这个我们不需要去管,WorkManager都替我们处理了。WorkManage会根据系统版本来选择用JobScheduler, Firebase的JobDispatcher, 或是AlarmManager。
至于AsyncTask, ThreadPool, RxJava这三个和WorkManager是没有冲突的,人家WorkManager是为了保证任务的可靠运行,但是AsyncTask, ThreadPool, RxJava,这三兄弟app退出人家就不干活了,和WorkManager的职责有着本事区别。一个是风雨无阻完成任务,一个有点事就撂挑子不干活了。


调用流程

我们表扬下WorkManager的好处
1、 易于调度

  • 后台工作程序只能在特定条件下调度任务(例如只有设备处于充电状态,该任务才会运行)
  • 一旦你调度了任务,就可以忘记任务的存在,调度程序应该提供在所需条件匹配的情况下保证任务运行。
  • 每个任务可以与另外一个任务并行链接,以并行或顺序运行多个任务。

2、易于取消

  • 你必须拥有对任务的控制权,调度程序应该提供API以轻松取消计划任务。

3、易于查询

  • 你的应用程序可能会需要显示任务的状态。
  • 假设你要上传照片并且需要在界面上显示上传的百分比。
  • 调度程序必须提供API以轻松获取任务的当前状态,如果任务完成之后可以传递一些结果数据,那就更棒了!

4、支持所有的Android版本

  • 调度程序API应该在所有的Android版本中都一样。

WorkManager由以下几个部分组成

  • Worker:指定您需要执行的任务。WorkManager API包含一个抽象Worker类。你扩展这个类并且在这里执行这个工作。

  • WorkRequest:代表一个单独的任务。至少,WorkRequest对象指定应该执行任务的Worker类。但是,您也可以向WorkRequest对象添加细节,指定任务应该运行的环境。每个工作请求都有一个自动生成的唯一ID;您可以使用ID来执行诸如取消排队任务或获取任务的状态等操作。WorkRequest是一个抽象类;在您的代码中,您将使用一个直接子类,一个timeworkrequest(一次性)或PeriodicWorkRequest(周期性)。

    • WorkRequest.Builder:创建工作请求对象的构造类。同样,您将使用一个子类,OneTimeWorkRequest。建筑商或PeriodicWorkRequest.Builder。
    • Constraints:指定任务运行时间的限制(例如,“仅在连接到网络时”)。
  • WorkManager:排队和管理工作请求。你传递你的WorkRequest 对象WorkManager来排队的任务。WorkManager调度任务的方式是分散系统资源的负载,同时遵守您指定的约束条件。

  • WorkStatus:包含有关特定任务的信息。包含关于特定任务的信息。WorkManager为每个WorkRequest对象提供一个LiveData。LiveData保存一个WorkStatus对象;通过观察这个LiveData,您可以确定任务的当前状态,并在任务完成后获得任何返回值。

了解完WorkManager,该撸代码了,前方高能依然是Kotlin。请抓好安全带。

这个小demo的功能是执行延时任务,获取广告信息,然后通知UI显示广告,看看能不能做一些无赖的事情,比如一有广告直接调起app显示。
1、添加依赖

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

AdWorker.kt

做了一个任务的开关,inputData是输入信息outputData是对外输出信息,也就是根据inputData的信息去做不同的事情,然后把结果通过outputData送出来。
WorkerResult的状态 FAILURE,RETRY,SUCCESS;
RETRY:WorkManager可以再次重试该工作
FAILURE:发生了一个或多个错误
SUCCESS:任务成功完成

class AdWorker : Worker() {
    override fun doWork(): WorkerResult {
        //输入data
        val is_open = this.inputData.getBoolean("is_open_ad", false)
        if (is_open) {
        //模拟延时操作
            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 "我是广告君,没进刚哥知识星球的赶紧加入了啊~~" + System.currentTimeMillis()
    }

}

AdEngine.kt

任务的调度类 我们在此用的是OneTimeWorkRequestBuilder一次性调用。
如果我们需要重复执行一项任务的话使用PeriodicWorkRequest.Builder

不过这个有个坑在等着大家。使用PeriodicWorkRequest的时候outputdata的里面的值是空的,上网查了很多资料都没有写出这个问题,但是OneTimeWorkRequestBuilder确实没有问题的,希望各位大神可以给解答一下这个问题。

这里可以往AdWorker进行setInputData,数据输入。然后加入任务队列。我们在此保存好任务ID,根据这个ID才可以找到这个任务。

那么只有这一个方法才能找到任务吗?答案是否定的。
我们可以标记任务 .addTag("tag_ad")
这个方法来获取任务 WorkManager.getInstance().getStatusesByTag("tag_id")

约束:
定义约束条件以告诉WorkManager合适安排任务执行,如果没有提供任何约束条件,那么该任务将立即运行。
以下是仅在设备充电和设备是否为空闲才运行任务的约束

   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)
        //保存任务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", "调用$this 的getValue()")
        return getSharePreferences(name, default)
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        Log.i("info", "调用$this 的setValue() value参数值为:$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里的静态属性
    lateinit var instance: App
        private set
    }

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

Main2Activity.kt

调用adEngine.schedulAd()方法,开启任务,然后通过 getStatusById返回LiveData<WorkStatus>,添加到LifecycleOwner(AppCompatActivity本身就是一个LifecycleOwner),我们就可以根据state的改变,改变TextView的内容。显示我们的广告语。结果我们会发现并不会直接调起来APP,这是为什么呢?
因为我们用的是LiveData,当Activity销毁后,LiveData处于未激活的状态,不回去接受数据的改变,只有Activity从新获得生命周期后,LiveData才会接受数据的变化,从而受到LiveData的通知,所以想要做坏事的同学,这条路行不通的,谷歌只是为了保证一些任务的可靠性,而不是保证你的App的生命。

当然任务可以执行就可以取消
WorkManager.getInstance().cancelByWorkId(uuid);

链式调用

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

image.png

这样就完了吗?还有更复杂的链式调用WorkContinuation大家可以自行学习下。

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 在国内应用的范围不广,重要是因为Slices是 Google Assistant 的延伸,谷歌希望使用者能过快速到达App里面的某个特点功能,举一例子就是,你对Google Assistant说你要回家,那么以前可能只会出现滴滴,Uber的选项,但是引进Slices之后会显示更加详细的数据列表,比如滴滴item下会出现到家多少距离,多少钱,是否立即打车等等。Google Assistant 在国内不好用,但是谷歌有这个功能开源我们自己其实也可以去实现,可能小米会把这个功能给小艾同学吧。

开始搭建我们的Slices吧。
注意注意:开发环境必须是Android Studio 3.2 以及以上,最低版本Android 4.4 (API level 19) ,我们可以从官网下载Android Studio 3.2 ,图标是黄色的,可以和我们之前的Android Studio 共存,相互没有干扰,讲实话Android Studio 3.2 真的处处是坑,特别和Kotlin配合,那真的是一言难尽,苦不堪言。

no.1

image.png

如果没有这个选项的话

 <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. 我是标题")
                 it.setSubtitle("我是子标题")
//设置Action
                 it.setPrimaryAction(activityAction)}
 }
                .build()
    } else {
        ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow { it.setTitle("URI not found.") }
                .build()
    }
}
//创建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转变成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()
    }

这样我们我们就完成了一个简单的Slice,什么?怎么用?下载这个slice-viewer.apk充当Google Assistant吧,然后我们需要做的是
adb shell am start -a android.intent.action.VIEW -d slice-content://com.simple.slicesapplication/ssy
蓝色的是我们自己的Content Uri,这样就会在slice-viewer.apk打开我们的slice了。

image.png

点击会跳转到我们的app。

什么?觉得不够丰富?来来来,阿秀同志请坐下,有好东西给你

创建一个带有Togglebutton和进度条 的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 {
//这个是进度条
                    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"
    }

啥?就这些?你想点击Slice后,在Slice上面动态修改一些信息,这是你的应用吗?咋想这么多呢?好吧,怕你了,来来来(谷歌官网的代码有坑,真的有坑)

  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

这个大哥叫切片模板

定义你的切片模板

切片是通过使用ListBuilder构造的 。ListBuilder允许您添加列表中显示的不同类型的行。本节介绍每种行类型及其构造方式。

SliceAction

Slice模板的最基本元素是 SliceAction。SliceAction包含一个标签以及一个PendingIntent, 并且是以下之一:

  • 图标按钮
  • 默认切换
  • 自定义切换(具有开/关状态的可绘图)

切片模板

SliceAction由本节其余部分描述的模板构建器使用。SliceAction可以定义一个图像模式,用于确定如何为该动作呈现图像:

  • ICON_IMAGE:尺寸小,可上色
  • SMALL_IMAGE:体积小,不可着色
  • LARGE_IMAGE:最大的尺寸和不可着色

HeaderBuilder

在大多数情况下,您应该使用HeaderBuilder为您的模板设置标题 。标头可以支持以下内容:

  • 标题
  • 字幕
  • 摘要字幕
  • 主要行动

下面显示了一些示例头部配置。请注意,灰色框显示潜在的图标和填充位置:


image.png
标题在不同的表面上呈现

当需要切片时,显示表面决定如何渲染切片。请注意,托管表面之间的渲染可能有所不同。

在较小的格式中,通常只显示标题(如果存在)。如果您为标题指定了摘要,则会显示摘要文本而不是字幕文本。

如果您没有在模板中指定标题,则通常会显示添加到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

感觉有点意思了,不过还是没有想象中该有的样子。上下标题差不多有了,右边的icon可以多几个吗?

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

最后来个全家桶


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的概念和简单的应用梳理了一遍,其中也发现了很多坑,网上好多文章,只是讲原理不写实例,或者有的人写了实例但是自己没有验证过,就是谷歌文档也有很多不清楚的地方,特别是Kotlin的依赖的配置,所幸把大部分的问题解决 了,不过还有一些问题依然不清楚,查看了官方文档,谷歌了众多文章可是资料很少,希望有小伙伴可以给解惑。一起学习。

大家可以点个关注,告诉我大家想要深入探究哪些问题,希望看到哪方面的文章,我可以免费给你写专题文章。。哈哈。。。
希望大家多多支持。。你的一个关注,是我坚持的最大动力。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容