阻止视频学习暂停窗口弹出_暂停阻止

阻止视频学习暂停窗口弹出

This article aims to show how to use Kotlin Coroutines and remove Reaxtive eXtensions (Rx).

本文旨在展示如何使用Kotlin协程并删除Reaxtive eXtensions(Rx)

好处 (Benefits)

To start let's consider four benefits of Coroutines over Rx:

首先,让我们考虑协程相对于Rx的四个好处:

暂停阻止 (Suspending over Blocking)

To run non-blocking code using Rx you'd write something like this:

要使用Rx运行非阻塞代码,您需要编写如下代码:

Observable.interval(1, TimeUnit.SECONDS)
    .subscribe {
        textView.text = "$it seconds have passed"
    }

Which is effectively creating a new thread. Threads are heavy objects in terms of memory and performance.

这实际上是在创建新线程。 就内存和性能而言,线程是沉重的对象。

Both are critical in the mobile development world.

两者在移动开发领域都至关重要。

You can achieve the same behavior using the following snippet:

您可以使用以下代码段实现相同的行为:

launch {
    var i = 0
    while (true){
        textView.text = "${it++} seconds have passed"
        delay(1000)
    }
}

Essentially, Coroutines are light-weight threads but we don't create any real thread. Here we are using non-blocking delay() function, which is a special suspending function that does not block a thread but suspends the Coroutine.

本质上,协程是轻量级线程,但是我们不创建任何实际线程。 在这里,我们使用非阻塞delay()函数,这是一个特殊的挂起函数,它不会阻塞线程,而是挂起Coroutine。

自然背压处理超过手动 (Natural backpressure handling over manual)

Backpressure is when observables produce items more rapidly than their observers consume them. While using Rx you have to explicitly specify how you will deal with backpressure. There are 2 basic approaches:

背压是可观察物产生的物品比观察者消耗物品更快的速度。 使用Rx时,您必须明确指定如何处理背压。 有两种基本方法:

  • Use throttling, buffers or windows operators

    使用限制,缓冲区或Windows运算符
  • The reactive pull model

    React拉力模型

Whereas Coroutines can suspend they provide a natural answer to handling backpressure. Thus, no additional actions are required.

协程可以暂停,这为应对背压提供了自然答案。 因此,不需要其他动作。

通过异步同步代码样式 (Sync code style over async)

The basic nature of a mobile app is to react to user actions. That is why Reactive eXtensions would be a good choice.

移动应用程序的基本性质是对用户操作做出React。 这就是为什么React式扩展将是一个不错的选择。

However, you have to write a code in a functional style. If you used to write in imperative style it could be a bit hard.

但是,您必须以功能样式编写代码。 如果您过去习惯以命令式的风格编写,可能会有些困难。

Whereas Coroutines enable you to write async code as if it was usual sync functions. For example,

而协程使您可以像编写通常的同步功能一样编写异步代码。 例如,

suspend fun showTextFromRemote() {
    val text = remote.getText()
    textView.text = text
}

Even I am working with functional style for a long time it is still easier to read and debug an imperative code.

即使我长时间使用函数式样式,也仍然更容易阅读和调试命令性代码。

本地超过第三方库 (Native over 3rd party lib)

Coroutines are a native build-in feature of Kotlin.

协程是Kotlin的本地内置功能。

You don't have to add any additional dependencies. Currently, all the main libraries could deal with coroutines.

您不必添加任何其他依赖项。 当前,所有主要库都可以处理协程。

For example,

例如,

Retrofit

翻新

interface Api {

    @Get("users")
    suspend fun loadUsers() : List<User>
}

Room

房间

interface Dao {

   @Update
   suspend fun update(user: UserEntity)
}

So, you can build an app which is all the way suspending — starting UI layer, through domain and ending in the data layer.

因此,您可以构建一个始终挂起的应用程序-通过域开始UI层,直到数据层结束。

应用程式 (App)

Let's go down to business. We will create a classic master-detail app. The first page would contain an infinite list of deliveries. On item click, we will open a detail page. Also, we will support offline mode — all the data would be cached. Moreover, I will use MVVM architecture where the ViewModel role is played by Fragment instead of ViewModel from AAC. There are several reasons: Fragments are usually very bald — just bind viewModel to XML.

让我们开始做生意。 我们将创建一个经典的主从应用程序。 第一页将包含无限的交货清单。 单击项目后,我们将打开一个详细信息页面。 另外,我们将支持离线模式-所有数据都将被缓存。 此外,我将使用MVVM体系结构,其中Fragment扮演ViewModel角色,而不是AAC扮演ViewModel角色。 原因有几个:片段通常非常秃顶-只需将viewModel绑定到XML。

Features like setting status bar color couldn't be done in AAC ViewModel — you have to trigger fragment's method. Using fragment as ViewModel would allow us to store all the related functionality (managing one given screen) in one class.

在AAC ViewModel中无法完成设置状态栏颜色之类的功能-您必须触发片段的方法。 使用片段作为ViewModel可以使我们将所有相关功能(管理一个给定的屏幕)存储在一个类中。

First, let's create BaseViewModel:

首先,让我们创建BaseViewModel:

abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope  by CoroutineScope(Dispatchers.IO){

    protected abstract val layoutId: Int

    protected abstract val bindings: B

    protected lateinit var viewBinding: V

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        viewBinding = DataBindingUtil.inflate(inflater, layoutId, container, false)

        return viewBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewBinding.lifecycleOwner = viewLifecycleOwner

        viewBinding.setVariable(BR.bindings, bindings)
    }

    override fun onDestroy() {
        cancel()

        super.onDestroy()
    }
}

We mark our ViewModel as CoroutineScope so that we can start coroutines inside view models and any launched coroutines would be limited to the lifecycle of a fragment.

我们将ViewModel标记为CoroutineScope,以便我们可以在视图模型内启动协程,并且任何启动的协程都将限于片段的生命周期。

We have to explicitly specify the end of scope's lifecycle calling cancel() method to cancel all the running requests to avoid memory leaks.

我们必须显式指定作用域生命周期的结尾,调用cancel()方法来取消所有正在运行的请求,以避免内存泄漏。

We set retainInstance = true so that in configuration changes fragment would not be recreated so that we can complete all long-running requests.

我们设置retainInstance = true以便在配置更改中不重新创建片段,以便我们可以完成所有长时间运行的请求。

Also, we have to set lifecycleOwner to binding to turn on two-way data binding.

另外,我们必须将lifecycleOwner设置为binding才能打开双向数据绑定

异常处理 (Exception handling)

According to Coroutines documentation:

根据协程的文档

Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). The former treat exceptions as unhandled, similar to Java's Thread.uncaughtExceptionHandler

Since we are using launch builder in the most cases we have to specify CoroutineExceptionHandler CoroutineExceptionHandler is CoroutineContext.Element which could be used to build a coroutine context using plus operator. I will declare static handler as follows:

由于在大多数情况下我们使用启动生成器,因此我们必须指定CoroutineExceptionHandler CoroutineExceptionHandler是CoroutineContext.Element ,可以使用加号运算符来构建协程上下文。 我将声明静态处理程序,如下所示:

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Timber.e(throwable)
}

And change BaseViewModel:

并更改BaseViewModel:

abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler)

From here on any exception occurred in launched coroutine inside ViewModel's scope would be delivered to given handler. Next, I need to declare my API and DAO:

从这里开始,ViewModel范围内启动的协程中发生的任何异常都将传递给给定的处理程序。 接下来,我需要声明我的API和DAO:

interface DeliveriesApi {

    @GET("deliveries")
    suspend fun getDeliveries(@Query("offset") offset: Int, @Query("limit") limit: Int): List<DeliveryResponse>
}

@Dao
interface DeliveryDao {

    @Query("SELECT * FROM ${DeliveryEntity.TABLE_NAME}")
    fun getAll(): DataSource.Factory<Int, DeliveryEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(delivery: DeliveryEntity)
}

As you can see I marked methods as suspended so that we can just declare expected response objects. Moreover, cancellation of parent coroutine will cancel network call as well. The same for DAO. The only difference is that we want to provide an ability to observe the database. The easiest way is to use built-in live data support. But if we would mark getAll() as suspended it would cause a compilation error error:

如您所见,我将方法标记为已暂停,以便我们可以声明预期的响应对象。 此外,取消父协程也会同时取消网络通话。 对于DAO也是一样。 唯一的区别是我们要提供观察数据库的功能。 最简单的方法是使用内置的实时数据支持。 但是,如果我们将getAll()标记为已暂停,则会导致编译错误错误:

Not sure how to convert a Cursor to this method's return type ...

Here we don't need suspending because:

在这里我们不需要暂停,因为:

  • Db requests are performed in the background by default

    默认情况下,db请求在后台执行
  • Resulting LiveData is lifecycle aware so that we don't need to cancel it manually

    结果LiveData具有生命周期意识,因此我们无需手动取消它

We have to somehow combine remote and local data sources. It is worthy to remember — there is should be an only single point of truth. According to offline-first design, it would be local storage. So, we would observe the database state. When there is nothing to retrieve we would ask data from remote and insert it to the database. We will introduce the Listing class

我们必须以某种方式组合远程和本地数据源。 值得记住的是-应该只有一个真理点。 根据离线优先设计 ,它将是本地存储。 因此,我们将观察数据库状态。 当没有什么可检索的时,我们将从远程请求数据并将其插入数据库。 我们将介绍Listing类

data class Listing<T>(
    val pagedList: LiveData<PagedList<T>>,
    val dataState: LiveData<DataState>,
    val refreshState: LiveData<DataState>,
    val refresh: () -> Unit,
    val retry: () -> Unit
)

Let's go val by val:

让我们逐个循环:

  • pagedList — the main data which is constructed as PagedList to enable infinite scrolling and wrapped with LiveData to enable data observing

    pagedList —主要数据,它被构造为PagedList以实现无限滚动,并被LiveData包装以实现数据观察
  • dataState — one of three states in which our data could be: Success, Running, Error. Also wrapped to LiveData to observe changes

    dataState-我们的数据可能处于以下三种状态之一:成功,运行,错误。 还包装到LiveData中以观察更改
  • refreshState — when we trigger data refreshing through swipe-to-refresh we need some tool by which we would distinguish between refresh request feedback and next page request feedback. For the former one, we want to show an error at the end of the list but for refresh error, we want to show a toast message and hide a loader.

    refreshState —当我们通过刷卡刷新触发数据刷新时,我们需要一些工具来区分刷新请求反馈和下一页请求反馈。 对于前一个,我们希望在列表的末尾显示一个错误,但是对于刷新错误,我们希望显示一个吐司消息并隐藏一个加载程序。
  • refresh() — callback to trigger on swipe-to-refresh

    refresh()—在刷卡刷新时触发的回调
  • retry() — callback to trigger on pagedList loading error Next, list view model:

    retry()—在pagedList加载错误时触发的回调接下来,列表视图模型:

    class DeliveryListViewModel : BaseViewModel<DeliveryListBindings, DeliveryListBinding>(), DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings {
    
    override val layoutId: Int = R.layout.delivery_list
    
    override val bindings: DeliveryListBindings = this
    
    private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) }
    
    private val listing = deliveryGateway.getDeliveries()
    
    override val dataState = listing.dataState
    
    override val isRefreshing = Transformations.switchMap(listing.refreshState) {
        MutableLiveData(it == DataState.Loading)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    
        setupList()
    
        setupRefresh()
    }
    
    private fun setupList() {
        val adapter = DeliveriesAdapter(this, this)
    
        viewBinding.deliveries.adapter = adapter
        viewBinding.deliveries.setHasFixedSize(true)
    
        listing.pagedList.observe(viewLifecycleOwner, Observer {
            adapter.submitList(it)
        })
    
        listing.dataState.observe(viewLifecycleOwner, Observer {
            adapter.updateDataState(it)
        })
    }
    
    private fun setupRefresh() {
        listing.refreshState.observe(viewLifecycleOwner, Observer {
            if (it is DataState.Error) {
                Toast.makeText(context, it.message, LENGTH_SHORT).show()
            }
        })
    }
    
    override fun refresh() {
        listing.refresh()
    }
    
    override fun onDeliveryClicked(delivery: Delivery) {
        view?.findNavController()?.navigate(DeliveryListViewModelDirections.toDetails(delivery))
    }
    
    override fun onRetryClicked() {
        listing.retry()
    }
    }

    Let's start from class declaration.

    让我们从类声明开始。

First of all DeliveryListBindings and DeliveryListBinding. First is our declared interface to glue view model with XML view. Second is the autogenerated class based on XML. We need the second one to set our bindings interface and lifecycle to XML.

首先是DeliveryListBindings和DeliveryListBinding。 首先是我们声明的接口,用于将视图模型与XML视图粘合在一起。 其次是基于XML的自动生成的类。 我们需要第二个将绑定接口和生命周期设置为XML。

Moreover, it is good practice to reference views using this autogenerated binding rather than using kotlin's synthetic.

此外,优良作法是使用这种自动生成的绑定而不是使用Kotlin的合成来引用视图。

There is could be the case when referenced through synthetic view doesn't exist in the current view. With data binding, you will fail fast even on compilation stage.

在当前视图中可能存在通过综合视图引用的情况。 使用数据绑定,即使在编译阶段,您也会快速失败。

Next, three interfaces: DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings.

接下来,三个接口:DeliveryListBindings,DeliveryListItemBindings,DeliveryListErrorBindings。

  1. DeliveryListBindings — bindings for the screen itself. For example, it contains refresh() method which is called on vertical swipe.

    DeliveryListBindings-屏幕本身的绑定。 例如,它包含在垂直滑动时调用的refresh()方法。

  2. DeliveryListItemBindings — bindings for an item in the list. For example, onClicked()

    DeliveryListItemBindings —列表中项目的绑定。 例如,onClicked()

  3. DeliveryListErrorBindings — bindings for error view which is also the list item shown on error state. For example, it contains retry() method

    DeliveryListErrorBindings —错误视图的绑定,该视图也是错误状态上显示的列表项。 例如,它包含retry()方法

Thus, we are handling everything in the single view model since it is a single screen but also following Interface Segregation principle

因此,我们正在处理单一视图模型中的所有内容,因为它是单个屏幕,但也遵循接口隔离原则

Let's turn special attention to this line:

让我们特别注意这一行:

private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) }

DeliveryGateway needs to perform requests out of the main thread. So, it needs either to declare methods as suspended or CoroutineScope to launch new coroutines on this scope. We would choose the second approach since we need our LiveData from the beginning and then we would just wait for updates from it. It is very similar to subscribing to liveData instance when we are passing lifecycleOwner(which often refers to 'this'). Here are in the same way we are passing 'this' as CoroutineScope

DeliveryGateway需要在主线程之外执行请求。 因此,它需要将方法声明为suspended或CoroutineScope才能在此范围内启动新的协程。 我们将选择第二种方法,因为从一开始就需要LiveData,然后我们将等待它的更新。 当我们传递lifecycleOwner(通常指“ this”)时,它与订阅liveData实例非常相似。 这与我们通过CoroutineScope传递“ this”的方式相同

CoroutineScope interface consists of a sole field — CoroutineContext. In essence, a scope and a context are the same things. The difference between a context and a scope is in their intended purpose.

CoroutineScope接口包含一个唯一字段— CoroutineContext。 本质上,范围和上下文是相同的东西。 上下文和范围之间的区别在于它们的预期目的。

To learn more about this I would recommend an article by Roman Elizarov. So, providing scope to DeliveryGateway will also result in using the same context. Specifically thread, job and exception handler. Now let's take a look at DeliveryGateway itself:

要了解更多信息,我将推荐Roman Elizarov的文章 。 因此,向DeliveryGateway提供范围也将导致使用相同的上下文。 特别是线程,作业和异常处理程序。 现在让我们看一下DeliveryGateway本身:

class DeliveryBoundGateway(
    private val db: DataBase,
    private val api: DeliveriesApi,
    private val deliveryDao: DeliveryDao,
    private val coroutineScope: CoroutineScope
) : DeliveryGateway {

    private val boundaryCallback = DeliveriesBoundaryCallback(
        api = api,
        coroutineScope = coroutineScope,
        handleResponse = { insertIntoDatabase(it) }
    )

    @MainThread
    override fun getDeliveries(): Listing<Delivery> {
        val refreshTrigger = MutableLiveData<Unit>()
        val refreshState = Transformations.switchMap(refreshTrigger) { refresh() }

        val pagingConfig = Config(
            initialLoadSizeHint = PAGE_SIZE,
            pageSize = PAGE_SIZE,
            prefetchDistance = PAGE_SIZE
        )

        val deliveries = deliveryDao.getAll()
            .toLiveData(
                config = pagingConfig,
                boundaryCallback = boundaryCallback
            )

        return Listing(
            pagedList = deliveries,
            dataState = boundaryCallback.dataState,
            retry = { boundaryCallback.helper.retryAllFailed() },
            refresh = { refreshTrigger.value = null },
            refreshState = refreshState
        )
    }

    /**
     * When refresh is called, we simply run a fresh network request and when it arrives, clear
     * the database table and insert all new items in a transaction.
     * <p>
     * Since the PagedList already uses a database bound data source, it will automatically be
     * updated after the database transaction is finished.
     */
    @MainThread
    private fun refresh(): LiveData<DataState> {
        boundaryCallback.refresh()

        val dataState = MutableLiveData<DataState>()
        dataState.value = DataState.Loading

        coroutineScope.launch {
            try {
                val deliveries = api.getDeliveries(0, PAGE_SIZE)

                db.withTransaction {
                    deliveryDao.clear()
                    insertIntoDatabase(deliveries)
                }

                dataState.postValue(DataState.Loaded)
            } catch (throwable: Throwable) {
                Timber.w(throwable)
                dataState.postValue(DataState.Error(throwable.message))
            }
        }

        return dataState
    }

    private suspend fun insertIntoDatabase(deliveries: List<DeliveryResponse>) {
        deliveries.forEach { delivery ->
            val entity = deliveryConverter.fromNetwork(delivery)
            deliveryDao.insert(entity)
        }
    }

    companion object {
        const val PAGE_SIZE = 20
    }
}

Here we are building LiveData structure from the beginning and then using coroutines load data and post it to the LiveData. Also, we are using the implementation of PagedList.BoundaryCallback() to glue local database and remote API. When we reach the end of the paged list boundaryCallback is triggered and loads next chunk of data.

在这里,我们从头开始构建LiveData结构,然后使用协程加载数据并将其发布到LiveData。 另外,我们使用PagedList.BoundaryCallback()的实现来粘合本地数据库和远程API。 当我们到达分页列表的末尾boundaryCallback被触发并加载数据的下一块。

As you can see we are using coroutineScope to launch new coroutines.

如您所见,我们正在使用coroutineScope启动新的协程。

Since this scope equals to the fragment's lifecycle — all pending requests would be canceled on fragment's onDestroy() callback.

由于此作用域等于片段的生命周期-所有待处理的请求都将在片段的onDestroy()回调中被取消。

The delivery detail page is quite straightforward — we just pass a Delivery object as Parcelable from the master screen using navigation component save args plugin. On details screen simply bind given an object to an XML.

交付详细信息页面非常简单-我们只需使用导航组件save args插件从主屏幕传递一个Parcelable交付对象。 在详细信息屏幕上,只需将给定的对象绑定到XML。

class DeliveryViewModel : BaseViewModel<DeliveryBindings, DeliveryBinding>(), DeliveryBindings {

    override val layoutId: Int = R.layout.delivery

    override val bindings: DeliveryBindings = this

    private val args: DeliveryViewModelArgs by navArgs()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewBinding.delivery = args.delivery

        viewBinding.image.clipToOutline = true
    }
}

联络我 (Contact me)

Here is the link to the github source code.

这是github源代码的链接

You are welcome to leave comments and open issues.

欢迎您发表评论和提出问题。

翻译自: https://habr.com/en/post/465529/

阻止视频学习暂停窗口弹出

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值