Android进阶之路Android开发经验谈程序员

Android JetPack:Paging、WorkManag

2019-09-25  本文已影响0人  cff70524f5cf

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

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

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

三大优点

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

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

Paging(分页)

背景:

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

Paging Libray分为三部分:DataSource, PagedList, PagedAdapter

DataSource

它就像是一个抽水泵,而不是真正的水源,它负责从数据源加载数据,可以看成是Paging Library与数据源之间的接口。

Datasource是数据源相关的类,Key是加载数据的条件信息,Value是返回结果, 针对不同场景我们需要用不同的Datasource,Paging提供了三个子类来供我们选择。

PagedList

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

PagedList有五个重要的参数:

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的项目代码和我们创建的有区别,所以会有很多注意不到的坑。

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
        }
    }
}

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
        })
    }
}

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、易于取消

3、易于查询

4、支持所有的Android版本

WorkManager由以下几个部分组成

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

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

添加依赖

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

AdWorker.kt

做了一个任务的开关,inputData是输入信息outputData是对外输出信息,也就是根据inputData的信息去做不同的事情,然后把结果通过outputData送出来。

WorkerResult的状态:

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才可以找到这个任务。

约束:
定义约束条件以告诉WorkManager合适安排任务执行,如果没有提供任何约束条件,那么该任务将立即运行。

以下是仅在设备充电和设备是否为空闲才运行任务的约束

   val myConstraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()
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
    }
}

链式调用

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

这样就完了吗?还有更复杂的链式调用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)
        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

如果没有这个选项的话

 <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了。

点击会跳转到我们的app。

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

最后感谢谷歌官方文档的支持,感谢网上的各位大神文章的支持。我只是一个搬运工。

好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

转发+点赞+关注,第一时间获取最新知识点

Android架构师之路很漫长,一起共勉吧!

以下墙裂推荐阅读!!!

上一篇下一篇

猜你喜欢

热点阅读