Kotlin协程最佳实践

编程入门 行业动态 更新时间:2024-10-23 06:22:49

<a href=https://www.elefans.com/category/jswz/34/1766123.html style=Kotlin协程最佳实践"/>

Kotlin协程最佳实践

文章目录

    • 转自官方文档
    • 一 注入调度程序
    • 二 挂起函数应该能够安全地从主线程调用
    • 三 ViewModel 应创建协程
    • 四 不要公开可变类型
    • 五 数据层和业务层应公开挂起函数和数据流
      • 在业务层和数据层中创建协程
    • 六 在测试中注入 TestDispatcher
    • 七 避免使用 GlobalScope
    • 八 将协程设为可取消
    • 九 留意异常

转自官方文档

=zh-cn

一 注入调度程序

在创建新协程或调用 withContext 时,请勿对 Dispatchers 进行硬编码。

// DO inject Dispatchers
class NewsRepository(private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}// DO NOT hardcode Dispatchers
class NewsRepository {// DO NOT use Dispatchers.Default directly, inject it insteadsuspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

二 挂起函数应该能够安全地从主线程调用

挂起函数应该是主线程安全的,这意味着,您可以安全地从主线程调用挂起函数。如果某个类在协程中执行长期运行的阻塞操作,那么该类负责使用 withContext 将执行操作移出主线程。这适用于应用中的所有类,无论其属于架构的哪个部分都不例外。

class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {// As this operation is manually retrieving the news from the server// using a blocking HttpURLConnection, it needs to move the execution// to an IO dispatcher to make it main-safesuspend fun fetchLatestNews(): List<Article> {withContext(ioDispatcher) { /* ... implementation ... */ }}
}// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(private val newsRepository: NewsRepository,private val authorsRepository: AuthorsRepository
) {// This method doesn't need to worry about moving the execution of the// coroutine to a different thread as newsRepository is main-safe.// The work done in the coroutine is lightweight as it only creates// a list and add elements to itsuspend operator fun invoke(): List<ArticleWithAuthor> {val news = newsRepository.fetchLatestNews()val response: List<ArticleWithAuthor> = mutableEmptyList()for (article in news) {val author = authorsRepository.getAuthor(article.author)response.add(ArticleWithAuthor(article, author))}return Result.Success(response)}
}

三 ViewModel 应创建协程

ViewModel 类应首选创建协程,而不是公开挂起函数来执行业务逻辑

// DO create coroutines in the ViewModel
class LatestNewsViewModel(private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)val uiState: StateFlow<LatestNewsUiState> = _uiStatefun loadNews() {viewModelScope.launch {val latestNewsWithAuthors = getLatestNewsWithAuthors()_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)}}
}// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {// DO NOT do this. News would probably need to be refreshed as well.// Instead of exposing a single value with a suspend function, news should// be exposed using a stream of data as in the code snippet above.suspend fun loadNews() = getLatestNewsWithAuthors()
}

视图不应直接触发任何协程来执行业务逻辑,而应将这项工作委托给 ViewModel

四 不要公开可变类型

最好向其他类公开不可变类型。这样一来,对可变类型的所有更改都会集中在一个类中,便于在出现问题时进行调试。

// DO expose immutable types
class LatestNewsViewModel : ViewModel() {private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)val uiState: StateFlow<LatestNewsUiState> = _uiState/* ... */
}class LatestNewsViewModel : ViewModel() {// DO NOT expose mutable typesval uiState = MutableStateFlow(LatestNewsUiState.Loading)/* ... */
}

五 数据层和业务层应公开挂起函数和数据流

数据层和业务层中的类通常会公开函数以执行一次性调用,或接收数据随时间变化的通知。这些层中的类应该针对一次性调用公开挂起函数,并公开数据流以接收关于数据更改的通知。

// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {suspend fun makeNetworkRequest() { /* ... */ }fun getExamples(): Flow<Example> { /* ... */ }
}

在业务层和数据层中创建协程

仅当用户查看当前屏幕时,要在这些协程中完成的工作才具有相关性,则应遵循调用方的生命周期。在大多数情况下,调用方是 ViewModel,当用户离开屏幕并且 ViewModel 被清除时,调用将被取消。在这种情况下,应使用 coroutineScope 或 supervisorScope。

class GetAllBooksAndAuthorsUseCase(private val booksRepository: BooksRepository,private val authorsRepository: AuthorsRepository,
) {suspend fun getBookAndAuthors(): BookAndAuthors {// In parallel, fetch books and authors and return when both requests// complete and the data is readyreturn coroutineScope {val books = async { booksRepository.getAllBooks() }val authors = async { authorsRepository.getAllAuthors() }BookAndAuthors(books.await(), authors.await())}}
}

如果只要应用处于打开状态,要完成的工作就具有相关性,并且此工作不限于特定屏幕,那么此工作的存在时间应该比调用方的生命周期更长。对于这种情况,您应使用外部 CoroutineScope(如“不应取消的工作的协程和模式”这篇博文中所述)

class ArticlesRepository(private val articlesDataSource: ArticlesDataSource,private val externalScope: CoroutineScope,
) {// As we want to complete bookmarking the article even if the user moves// away from the screen, the work is done creating a new coroutine// from an external scopesuspend fun bookmarkArticle(article: Article) {externalScope.launch { articlesDataSource.bookmarkArticle(article) }.join() // Wait for the coroutine to complete}
}

六 在测试中注入 TestDispatcher

  • StandardTestDispatcher:使用调度器将已在其上启动的协程加入队列,并在测试线程不繁忙时执行这些协程。您可以使用 advanceUntilIdle 等方法挂起测试线程,以允许其他加入队列的协程运行。

  • UnconfinedTestDispatcher:以阻塞方式即刻运行新协程。这样做通常可以更轻松地编写测试,但会使您无法更好地控制测试期间协程的执行方式。

class ArticlesRepositoryTest {@Testfun testBookmarkArticle() = runTest {// Pass the testScheduler provided by runTest's coroutine scope to// the test dispatcherval testDispatcher = UnconfinedTestDispatcher(testScheduler)val articlesDataSource = FakeArticlesDataSource()val repository = ArticlesRepository(articlesDataSource,testDispatcher)val article = Article()repository.bookmarkArticle(article)assertThat(articlesDataSource.isBookmarked(article)).isTrue()}
}

七 避免使用 GlobalScope

通过使用 GlobalScope,您将对类使用的 CoroutineScope 进行硬编码,而这会带来一些问题:

  • 提高硬编码值。如果您对 GlobalScope 进行硬编码,则可能同时对 Dispatchers 进行硬编码。

  • 这会让测试变得非常困难,因为您的代码是在非受控的作用域内执行的,您将无法控制其执行。

  • 无法设置一个通用的 CoroutineContext 来对内置于作用域本身的所有协程执行。

可以考虑针对存在时间需要比当前作用域更长的工作注入一个 CoroutineScope

// DO inject an external scope instead of using GlobalScope.
// GlobalScope can be used indirectly. Here as a default parameter makes sense.
class ArticlesRepository(private val articlesDataSource: ArticlesDataSource,private val externalScope: CoroutineScope = GlobalScope,private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {// As we want to complete bookmarking the article even if the user moves// away from the screen, the work is done creating a new coroutine// from an external scopesuspend fun bookmarkArticle(article: Article) {externalScope.launch(defaultDispatcher) {articlesDataSource.bookmarkArticle(article)}.join() // Wait for the coroutine to complete}
}// DO NOT use GlobalScope directly
class ArticlesRepository(private val articlesDataSource: ArticlesDataSource,
) {// As we want to complete bookmarking the article even if the user moves away// from the screen, the work is done creating a new coroutine with GlobalScopesuspend fun bookmarkArticle(article: Article) {GlobalScope.launch {articlesDataSource.bookmarkArticle(article)}.join() // Wait for the coroutine to complete}
}

八 将协程设为可取消

协程取消属于协作操作,也就是说,在协程的 Job 被取消后,相应协程在挂起或检查是否存在取消操作之前不会被取消。如果您在协程中执行阻塞操作,请确保相应协程是可取消的。

someScope.launch {for(file in files) {ensureActive() // Check for cancellationreadFile(file)}
}

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

九 留意异常

未处理协程中抛出的异常可能会导致应用崩溃。如果可能会发生异常,请在使用 viewModelScope 或 lifecycleScope 创建的任何协程主体中捕获相应异常。

class LoginViewModel(private val loginRepository: LoginRepository
) : ViewModel() {fun login(username: String, token: String) {viewModelScope.launch {try {loginRepository.login(username, token)// Notify view user logged in successfully} catch (exception: IOException) {// Notify view login attempt failed}}}
}

更多推荐

Kotlin协程最佳实践

本文发布于:2023-12-03 20:52:28,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1657527.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:Kotlin

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!