本文旨在展示如何使用Kotlin协程并删除Reaxtive eXtensions(Rx) 。
好处
首先,让我们考虑协程相对于Rx的四个好处:
暂停阻止
要使用Rx运行非阻塞代码,您需要编写如下代码:
Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text = "$it seconds have passed" }
这实际上是在创建新线程。 就内存和性能而言,线程是沉重的对象。
两者在移动开发领域都至关重要。
您可以使用以下代码段实现相同的行为:
launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } }
本质上,协程是轻量级线程,但我们不创建任何实际线程。
这里我们使用的是非阻塞delay()函数,这是一个特殊的挂起函数,它不会阻塞线程,而是挂起Coroutine。
自然背压处理超过手动
背压是可观察物产生的物品比观察者消耗物品更快的速度。
使用Rx时,必须明确指定如何处理背压。
有两种基本方法:
- 使用限制,缓冲区或Windows运算符
- 反应拉力模型
协程可以暂停,这为应对背压提供了自然答案。
因此,不需要其他动作。
通过异步同步代码样式
移动应用程序的基本性质是对用户操作做出反应。 这就是为什么反应式扩展将是一个不错的选择。
但是,您必须以功能样式编写代码。 如果您过去习惯以命令式的风格编写,可能会有些困难。
而协程使您可以像编写通常的同步功能一样编写异步代码。 例如
suspend fun showTextFromRemote() { val text = remote.getText() textView.text = text }
即使我使用函数式样式已经很长时间了,阅读和调试命令性代码仍然更加容易。
本地超过第三方库
协程是Kotlin的本地内置功能。
您不必添加任何其他依赖项。 当前,所有主要库都可以处理协程。
例如
翻新
interface Api { @Get("users") suspend fun loadUsers() : List<User> }
房间
interface Dao { @Update suspend fun update(user: UserEntity) }
因此,您可以构建一个完全暂停的应用程序-通过域开始UI层,并在数据层结束。
该应用程序
让我们开始做生意。 我们将创建一个经典的主从应用程序。
第一页将包含无限的交货清单。
单击项目时,我们将打开一个详细信息页面。
另外,我们将支持离线模式-所有数据都将被缓存。
此外,我将使用MVVM体系结构,其中Fragment扮演ViewModel角色,而不是AAC扮演ViewModel角色。 有以下几个原因:
片段通常非常秃顶-只需将viewModel绑定到XML。
诸如设置状态栏颜色之类的功能无法在AAC ViewModel中完成-您必须触发片段的方法。 将片段用作ViewModel将使我们能够将所有相关功能(管理一个给定的屏幕)存储在一个类中。
首先,让我们创建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() } }
我们将ViewModel标记为CoroutineScope,以便我们可以在视图模型中启动协程,并且任何启动的协程都将限于片段的生命周期。
我们必须显式指定作用域生命周期的结尾,调用cancel()
方法来取消所有正在运行的请求,以避免内存泄漏。
我们设置retainInstance = true
以便在配置更改中不重新创建片段,以便我们可以完成所有长时间运行的请求。
另外,我们必须将lifecycleOwner设置为binding才能打开双向数据绑定 。
异常处理
根据协程的文档 :
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
由于大多数情况下我们都使用启动生成器,因此我们必须指定CoroutineExceptionHandler
CoroutineExceptionHandler是CoroutineContext.Element ,可以使用加号运算符来构建一个协程上下文。
我将声明静态处理程序,如下所示:
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> Timber.e(throwable) }
并更改BaseViewModel:
abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler)
从这里开始,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) }
如您所见,我将方法标记为已暂停,以便我们可以声明预期的响应对象。 此外,取消父协程也会同时取消网络通话。
对于DAO也是一样。
唯一的区别是我们要提供观察数据库的功能。
最简单的方法是使用内置的实时数据支持。 但是,如果我们将getAll()标记为暂停,则会导致编译错误
错误:
Not sure how to convert a Cursor to this method's return type ...
在这里我们不需要暂停,因为:
- 默认情况下,db请求在后台执行
- 结果LiveData具有生命周期意识,因此我们无需手动取消它
我们必须以某种方式组合远程和本地数据源。
值得记住的是-应该只有一个事实真相。
根据离线优先设计 ,它将是本地存储。 因此,我们将观察数据库状态。 当没有什么可检索的时,我们会从远程询问数据并将其插入数据库。
我们将介绍Listing类
data class Listing<T>( val pagedList: LiveData<PagedList<T>>, val dataState: LiveData<DataState>, val refreshState: LiveData<DataState>, val refresh: () -> Unit, val retry: () -> Unit )
让我们逐个循环:
- pagedList-构建为PagedList的主数据,以实现无限滚动,并与LiveData打包在一起以实现数据观察
- dataState-我们的数据可能处于以下三种状态之一:成功,运行,错误。 还包装到LiveData中以观察更改
- refreshState-当我们通过刷卡刷新触发数据刷新时,我们需要一些工具来区分刷新请求反馈和下一页请求反馈。 对于前一个,我们希望在列表的末尾显示一个错误,但是对于刷新错误,我们希望显示一个吐司消息并隐藏一个加载器。
- refresh()-回调以在刷卡刷新时触发
重试()-回调以触发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() } }
让我们从类声明开始。
首先是DeliveryListBindings和DeliveryListBinding。 首先是我们声明的接口,用于将视图模型与XML视图粘合在一起。 其次是基于XML的自动生成的类。 我们需要第二个将绑定接口和生命周期设置为XML。
此外,优良作法是使用这种自动生成的绑定而不是使用Kotlin的合成来引用视图。
在当前视图中可能不存在通过综合视图引用的情况。 使用数据绑定,即使在编译阶段,您也会快速失败。
接下来,三个接口:DeliveryListBindings,DeliveryListItemBindings,DeliveryListErrorBindings。
- DeliveryListBindings-屏幕本身的绑定。 例如,它包含在垂直滑动时调用的refresh()方法。
- DeliveryListItemBindings-列表中项目的绑定。 例如onClicked()
- DeliveryListErrorBindings-错误视图的绑定,它也是错误状态上显示的列表项。 例如,它包含retry()方法
因此,我们在单视图模型中处理所有内容,因为它是单个屏幕,但也遵循接口隔离原则
让我们特别注意这一行:
private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) }
DeliveryGateway需要在主线程之外执行请求。 因此,它需要将方法声明为suspended或CoroutineScope才能在此范围内启动新的协程。 我们将选择第二种方法,因为从一开始就需要LiveData,然后我们将等待它的更新。 当我们传递lifecycleOwner(通常指“ this”)时,它与订阅liveData实例非常相似。 这与我们通过CoroutineScope传递“ this”的方式相同
CoroutineScope接口包含一个唯一字段-CoroutineContext。 本质上,范围和上下文是相同的东西。 上下文和范围之间的区别在于它们的预期目的。
要了解有关此内容的更多信息,我将推荐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 ) } @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 } }
在这里,我们从头开始构建LiveData结构,然后使用协程加载数据并将其发布到LiveData。 另外,我们使用PagedList.BoundaryCallback()的实现来粘合本地数据库和远程API。 当我们到达页面列表的末尾时,会触发borderCallback并加载下一个数据块。
如您所见,我们正在使用coroutineScope启动新的协程。
由于此作用域等于片段的生命周期-所有待处理的请求都将在片段的onDestroy()
回调中被取消。
交付详细信息页面非常简单-我们只需使用导航组件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 } }
这是github源代码的链接 。
欢迎您发表评论和提出问题。