Android-kotlin协程应用

1. 添加依赖

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

具体版本号去官网搜索即可

2. 协程作用

协程可以用来解决回调地狱问题,作为一门新的技术,我们总会要去跟之前的代码作比对,看看使用新的技术能够带来哪方面的提升。
首先我们已网络获取数据为例,看下之前的实现方式:

@UiThread
fun makeNetworkRequest() {
    slowFetch { result ->
        show(result)
    }
}

然后在看下协程实现的方式:

@UiThread
suspend fun makeNetworkRequest() {
    val result = slowFetch()
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

suspend 关键词 可能跟其他语言中的async 类似,都是不阻塞当前线程,异步等待数据返回。
通过比对我们可以发现,使用协程有如下优势:

  1. 让代码更容易理解,而且让代码更加精简
  2. 不会阻塞线程
  3. 可以链式操作

上面示例中因为就只有一个从网络获取数据的方法,可能还看不出其威力,假设我们有如下需求,两次从服务端获取数据,然后在把数据写入到数据库,让我们来看看用协程的方式实现:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

3.协程关键点

CoroutineScope

协程作用域,通过Job来管理作用域范围内的所有协程,当job cancel 时会取消作用域范围内的所有协程。
应用场景: 当从Activity 或者 Fragment退出时,可以调用job cancel 用来取消其所有协程

Dispatcher

使用协程时可以通过Dispatcher很方便的切换线程,指定线程技巧:
1.开销较小的可以直接在主线程中操作而不用担心阻塞线程,这样可以节省线程间切换的资源消耗
2.开销较大的如操作数据库,解析大的json文件等需要指定后台线程
像Room、Retrofit等这种本身就不会占用主线程时间的可以直接在主线程中应用即可,从而简化代码。

viewModelScope

viewModelScope是ViewModel的扩展方法,这个作用域是绑定在Dispatchers.Main上的,在ViewModel onCleared的时候自动cancel,当然要使用这个方法需要添加依赖:

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

在ViewModel中的使用示例:
使用前代码:

private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

使用viewModelScope后代码:

fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

4.协程替代callback实现方式

看下使用前的代码:

 fun refreshTitle() {
     // TODO: Convert refreshTitle to use coroutines
     _spinner.value = true
     repository.refreshTitleWithCallbacks(object : TitleRefreshCallback {
         override fun onCompleted() {
             _spinner.postValue(false)
         }

         override fun onError(cause: Throwable) {
             _snackBar.postValue(cause.message)
             _spinner.postValue(false)
         }
     })
 }

在以前,我们一般用回调来处理异步耗时数据
tips: object: TitleRefreshCallback 这个是在kotlin中构建匿名类的一种方式,它创建了一个新的实现TitleRefreshCallback接口实例对象

然后在看下我们使用协程之后的方式:

fun refreshTitle() {
  viewModelScope.launch {
     try {
          _spinner.value = true
          repository.refreshTitle()
      } catch (error: TitleRefreshError) {
          _snackBar.value = error.message
      }finally {
          _spinner.value = false
      }
    }
}

refreshTitle 模仿网络请求实现如下:

suspend fun refreshTitle() {
    delay(500)
}

上面代码通过异常来处理错误场景,从而避免使用接口来自定义错误处理

示例关键点说明

协程启动:
1.新建一个协程,我们需要使用launch来启动协程
2.已经有一个协程,从协程内部启动协程,有两种方式:

  1. 当不需要返回值时,使用launch启动
  2. 当需要返回值时,使用async 启动

Exception异常:
在协程中使用uncaught exceptions和在正常的方法中使用基本是类似的,不过需要注意的是,默认情况下,他们会取消Job,
即会通知父协程取消作用域内的所有协程,如果这些异常没有去handle,则最终会传给协程作用域CoroutineScope

5.协程替代耗时操作

回调实现方案:

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
    // This request will be run on a background thread by retrofit
    BACKGROUND.submit {
        try {
            // Make network request using a blocking call
            val result = network.fetchNextTitle().execute()
            if (result.isSuccessful) {
                // Save it to database
                titleDao.insertTitle(Title(result.body()!!))
                // Inform the caller the refresh is completed
                titleRefreshCallback.onCompleted()
            } else {
                // If it's not successful, inform the callback of the error
                titleRefreshCallback.onError(
                        TitleRefreshError("Unable to refresh title", null))
            }
        } catch (cause: Throwable) {
            // If anything throws an exception, inform the caller
            titleRefreshCallback.onError(
                    TitleRefreshError("Unable to refresh title", cause))
        }
    }
}

使用协程后方案:

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

技能点:
1.协程使用withContext来切换不同的dispatcher
2.协程默认提供三种Dispatchers
Main:主线程
IO:针对IO操作,如从网络或者磁盘中读取数据等
Default: 主要针对CPU密集型任务

使用room和retrofit自带协程接口

在上面小结中,我们使用withContext在自己管理线程切换,因为上面只是从网络中获取数据和插入数据到数据库,这两步如果使用room框架和retrofit框架自带协程支持可以更加进一步的简化代码,首先我们改造下插入数据库代码和从网络中获取代码:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

然后将上一步中手动管理线程代码改造如下:

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

这样代码比之前精简了非常多
tips: Room和Retrofit 是自己内部自定义的线程,而不是使用的Dispatchers.IO

高级函数传递suspend方法

在写代码时,我们需要把重复代码抽离出来,下面示例中就是通过高阶函数抽离等待加载代码,首先先看下之前的实现代码:

fun refreshTitle() {
    viewModelScope.launch {
        try {
            _spinner.value = true
            repository.refreshTitle()
        } catch (error: TitleRefreshError) {
            _snackBar.value = error.message
        }finally {
            _spinner.value = false
        }
    }
}

直接上改造之后的代码:

fun refreshTitle() {
    launchDataLoading{
        repository.refreshTitle()
    }
}

private fun launchDataLoading(block: suspend () ->Unit) : Job {
    return viewModelScope.launch {
        try {
            _spinner.value = true
            block()
        } catch (error: TitleRefreshError) {
            _snackBar.value = error.message
        }finally {
            _spinner.value = false
        }
    }
}

上述实现中,将block: suspend () ->Unit suspend lamda表达式当做参数传递

发布了159 篇原创文章 · 获赞 22 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/ytuglt/article/details/105585408