Android 中的协程常用姿势

925 阅读8分钟

GlobalScope

GlobalScope是一个特殊的CoroutineScope,它在整个应用程序生命周期内存在,它不会因为任何协程的完成或取消而终止。它的作用是提供一个顶级作用域,以在不需要显式CoroutineScope的情况下启动协程。如果无脑使用GlobalScope,我们需要管理好所有的协程,否则容易遗漏,造成内存泄漏,如下:

mJob1 = GlobalScope.launch(Dispatchers.Main){
        //do something
    }
mJob
2 = GlobalScope.launch(Dispatchers.Main){
        //do something
    }
mJob3 = GlobalScope.launch(Dispatchers.Main){
        //do something
    }
 
override fun onDestroy() {
    super.onDestroy()
    mJob1?.cancel()
    mJob2?.cancel()
    mJob3?.cancel()
}

使用GlobalScope在Android中不是一个最佳实践,因为它不能明确的控制生命周期,如果协程不正确管理,它可能导致内存泄漏。在Android中,通常使用一个显式的CoroutineScope,如一个特定的Activity或Fragment的生命周期,来启动协程,以便在生命周期结束时正确取消。

MainScope

MainScope是CoroutineScope的一种,是一个与主线程关联的全局协程作用域。与主线程关联意味着,在MainScope中的协程,如果主线程结束,这些协程也会结束。

最佳实践:使用MainScope在主线程中启动协程,进行简单的后台任务,例如访问网络并更新UI,然后在onDestory里取消。

例如:

class MainActivity : AppCompatActivity() {

    privateval mainScope by lazy {MainScope()}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mainScope.launch {
            val data = getDataFromNetwork()
            updateUI(data)
        }
    }

    suspend fun getDataFromNetwork(): String {
        // 假设这是访问网络的代码
        return "Data from network"
    }

    fun updateUI(data: String) {
        // 假设这是更新UI的代码
    }

    override fun onDestroy() {
        super.onDestroy()
        mMainScope.cancel()
    }
}

ViewModelScope

ViewModelScope 是一种可以在 Android 应用的 ViewModel 中使用的协程作用域。它可以让您在 ViewModel 内部启动协程,而不用担心在配置更改时它们的生命周期。如果 ViewModel 销毁,则所有正在运行的协程将自动取消。

下面是一个简单的实践例子:

class MyViewModel : ViewModel() {
    fun performLongRunningTask() {
        viewModelScope.launch {
            // Perform the long-running task here
        }
    }
}

在这个例子中,我们定义了一个 MyViewModel 类,并在其内部调用了 viewModelScope.launch 方法来启动一个新的协程。当 ViewModel 销毁时,所有正在运行的协程都将自动取消。这样可以确保在 ViewModel 生命周期结束时不会有不必要的资源消耗。

LifecycleScope

LifecycleScope 是在 Android 的 Jetpack 框架中提供的用于管理生命周期的协程作用域。它可以结合 Android 的生命周期组件,比如 Activity 和 Fragment,来管理协程的生命周期。使用 LifecycleScope,您可以确保当生命周期处于销毁状态时,所有协程都会被取消。

下面是使用 LifecycleScope 的一个实践例子:

class MyFragment : Fragment() {
 
    private val viewModel: MyViewModel by viewModels()
 
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // ...
        return binding.root
    }
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            // 在这里发起网络请求
            val data = withContext(Dispatchers.IO) {
                // 在 IO 线程中执行网络请求
                // ...
            }
            // 在主线程更新 UI
            binding.data = data
        }
    }
}

在这个例子中,我们使用 lifecycleScope.launch 在生命周期内启动了一个协程。当 Fragment 销毁时,所有协程都会被取消,因此不会有任何内存泄漏。

LiveDataScope

LiveDataScope是一种限制LiveData对象生命周期的工具。它在超出生命周期范围后,LiveData对象将被自动释放。

以下是一个使用LiveDataScope的最佳实践示例:

1.在某个LifecycleOwner(例如Fragment或Activity)内创建LiveDataScope:

val liveDataScope = viewLifecycleOwner.lifecycleScope.launchWhenStarted {
   // some code here
}

2.在该作用域内创建并使用LiveData:

val liveData = liveData(liveDataScope.coroutineContext) {
   // emit values here
}

在这个例子中,当LiveDataScope的生命周期结束时,该作用域内的LiveData对象也将被自动释放。

Room

在Room中使用协程需要遵循以下步骤:

1.创建Dao接口并声明数据库操作:

@Dao
interface UserDao {
   @Query("SELECT * FROM user")
   suspend fun getUsers(): List<User>
 
   @Insert
   suspend fun insertUser(user: User)
 
   // ... other operations
}

2.在Repository中使用协程调用Dao接口:

classUserRepository(private val userDao: UserDao) {
   suspend fun getUsers(): List<User> {
      return userDao.getUsers()
   }
 
   suspend fun insertUser(user: User) {
      userDao.insertUser(user)
   }
 
   // ... other operations
}

3.在ViewModel或其他地方调用Repository:

classUserViewModel(private val userRepository: UserRepository) : ViewModel() {
   val users = liveData {
      emit(userRepository.getUsers())
   }
 
   fun insertUser(user: User) {
      viewModelScope.launch {
         userRepository.insertUser(user)
      }
   }
 
   // ... other operations
}

在这个例子中,Repository通过调用Dao接口来执行数据库操作,ViewModel通过调用Repository来获取数据,并通过LiveData返回给UI层。

使用 suspend 关键字对 Dao接口方法进行声明,执行此操作后,Room 会让您的查询具有主线程安全性,并自动在后台线程上执行此查询。不过,这也意味着您只能从协程内调用此查询。

以上就是在 Room 中使用协程所需执行的全部操作。

注意:在数据库操作时,不要在主线程中执行阻塞操作,否则会导致界面卡顿。使用协程可以避免这个问题,并且使代码更简洁易读。

WorkManager

在WorkManager中使用协程需要遵循以下步骤:

1.定义一个Worker类,该类继承自CoroutineWorker:

class MyWorker(
   context: Context,
   params: WorkerParameters
) : CoroutineWorker(context, params) {
 
   override suspend fun doWork(): Result {
      // your work here
      return Result.success()
   }
}

2.在该Worker类内使用协程执行任务:

override suspend fun doWork(): Result {
   withContext(Dispatchers.IO) {
      // perform long-running task here
   }
   return Result.success()
}

3.在应用程序代码中调用Worker:

val work = OneTimeWorkRequestBuilder<MyWorker>()
   .build()
WorkManager.getInstance(this).enqueue(work)

在这个例子中,我们定义了一个MyWorker类,该类继承自CoroutineWorker,可以使用协程在后台执行长时间运行的任务。在应用程序代码中,我们使用WorkManager调用该Worker类,从而实现在后台执行任务的功能。

注意:CoroutineWorker.doWork() 是一个挂起函数。与更简单的 Worker 类不同,此代码不会在您的 WorkManager 配置所指定的执行器上运行,而是使用 coroutineContext 成员的调度程序(默认为 Dispatchers.Default)。在Worker类内执行的任务不要在主线程中执行阻塞操作,否则会导致界面卡顿。使用协程可以避免这个问题,并且使代码更简洁易读。

Retrofit

在Retrofit中使用协程的步骤如下:

1.创建Retrofit接口:

interface MyAPI {
   @GET("/todos/{id}")
   suspend fun getTodo(@Path("id") id: Int): Todo
}

2.创建Retrofit实例:

val retrofit = Retrofit.Builder()
   .baseUrl("https://jsonplaceholder.typicode.com")
   .addConverterFactory(GsonConverterFactory.create())
   .build()
val myAPI = retrofit.create(MyAPI::class.java)

3.在协程中调用API:

GlobalScope.launch(Dispatchers.Main) {   val todo = withContext(Dispatchers.IO) {      myAPI.getTodo(1)   }   // update UI with todo data}

在这个例子中,我们首先定义了一个名为MyAPI的接口,该接口使用了Retrofit注解,以声明需要调用的API。接着,我们创建了一个Retrofit实例,该实例根据MyAPI接口自动生成了一个实现。最后,我们在协程中调用了API,并在协程内部使用了withContext,将耗时操作放在了后台线程中执行,从而避免了主线程阻塞。

要将挂起函数与 Retrofit 一起使用,您必须执行以下两项操作:

  1. 为函数添加挂起修饰符;
  2. 从返回值类型中移除 Call 封装容器。这里可以返回 String,也可以返回 json 支持的复杂类型。如果您仍希望提供对 Retrofit 的完整 Result 的访问权限,您可以从挂起函数返回 Result 而不是 String。

Retrofit 将自动使挂起函数具有主线程安全性,以便您可以直接从 Dispatchers.Main 调用它们。

注意:在Retrofit中使用协程需要使用支持协程的网络请求库,例如Kotlinx.coroutines中的Retrofit库。使用这些库可以更简单、高效地在Retrofit中使用协程。

Kotlin协程+JetPack(ViewModel+LiveData)+Retrofit封装网络框架最佳实践例子

1.创建Retrofit接口:

interface TodoApi {
    @GET("todos")
    suspend fun getTodos(): Response<List<Todo>>
}

2.创建Retrofit实例:

private val retrofit = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
 
private val todoApi = retrofit.create(TodoApi::class.java)

3.创建协程以请求数据:

private suspend fun loadTodos(): Result<List<Todo>> = withContext(Dispatchers.IO) {
    try {
        val response = todoApi.getTodos()
        if (response.isSuccessful) {
            Result.success(response.body()!!)
        } else {
            Result.failure(Exception("Failed to fetch data: ${response.message()}"))
        }
    } catch (e: Exception) {
        Result.failure(e)
    }
}

4.创建ViewModel:

class TodoViewModel : ViewModel() {
    private val _todos = MutableLiveData<List<Todo>>()
    val todos: LiveData<List<Todo>> = _todos
 
    fun loadTodos() {
        viewModelScope.launch {
            val result = loadTodos()
            if (result is Result.Success) {
                _todos.value = result.data
            }
        }
    }
}

5.在MainActivity中使用ViewModel:

class MainActivity : AppCompatActivity() {
    private lateinit var todoViewModel: TodoViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        todoViewModel = ViewModelProvider(this).get(TodoViewModel::class.java)
        todoViewModel.todos.observe(this, Observer { todos ->
            // update UI
        })
 
        todoViewModel.loadTodos()
    }
}

在高阶函数中使用协程

例如,我们有代码如下:

class MainViewModel(private val repository: TitleRepository) : ViewModel() {
    companion object {
        val FACTORY = singleArgViewModelFactory(::MainViewModel)
    }
    private val _snackBar = MutableLiveData<String?>()
    val snackbar: LiveData<String?>
        get() = _snackBar
    private val _spinner = MutableLiveData<Boolean>(false)
    val spinner: LiveData<Boolean>
        get() = _spinner
    fun onMainViewClicked() {
        refreshTitle()
    }
    fun refreshTitle() {
       viewModelScope.launch {
           try {
               _spinner.value = true
               // this is the only part that changes between sources
               repository.refreshTitle()
           } catch (error: TitleRefreshError) {
               _snackBar.value = error.message
           } finally {
               _spinner.value = false
           }
       }
    }
}

在这个例子的 refreshTitle()方法,可以发现,除 repository.refreshTitle() 之外的每行代码都是显示旋转图标和错误的样板代码,我们可以新增下面这个方法:

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

现在重构 refreshTitle() 以使用此高阶函数:

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

通过抽象化用于显示加载旋转图标和显示错误的逻辑,我们简化了加载数据所需的实际代码。显示旋转图标或显示错误是易于泛化到任何数据加载的内容,而实际数据源和目标则需要每次都指定。

为了构建此抽象,launchDataLoad 接受一个属于挂起 lambda 的参数 block。挂起 lambda 可让您调用挂起函数。

将协程设为可取消

协程取消属于协作操作,也就是说,在协程的 Job 被取消后,相应协程在挂起或检查是否存在取消操作之前不会被取消。如果您在协程中执行阻塞操作,请确保相应协程是可取消的。
例如,如果您要从磁盘读取多个文件,请先检查协程是否已取消,然后再开始读取每个文件。若要检查是否存在取消操作,有一种方法是调用 ensureActive 函数。

kotlinx.coroutines 中的所有挂起函数(例如 withContext 和 delay)都是可取消的。如果您的协程调用这些函数,您无需执行任何其他操作。

Reference

developer.android.com/topic/libra…

developer.android.com/kotlin/coro…

developer.android.com/codelabs/ko…