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 一起使用,您必须执行以下两项操作:
- 为函数添加挂起修饰符;
- 从返回值类型中移除 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…