Kotlin KMM停止Coroutine流动,无限环路正确

发布于 2025-02-05 11:27:43 字数 3145 浏览 2 评论 0原文


I'm building a KMM app for retrieving news. My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?

我设计了如下:

  • ViewModel(android和iOS)
  • USECASE(共享)
  • 存储库(共享)
  • 数据源(共享)
  • Android JetPack组成单个活动
  • ios Swiftui,

Android ViewModel:(iOS use ObservableObject, but logic is the same)
@HiltViewModel
class NewsViewModel @Inject constructor(
   private val startFetchingNews: GetNewsUseCase,
   private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
   private val _mutableNewsUiState = MutableStateFlow(NewsState())
   val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()

   fun onTriggerEvent(action: MapEvents) {
       when (action) {
           is NewsEvent.GetNews -> {
            getNews()
          }
           is MapEvents.StopNews -> {
            //????
           }
           else -> {

           }
       }
   }

   private fun getNews()() {
       startFetchingNews().collectCommon(viewModelScope) { result ->
           when {
               result.error -> {
                //update state
               }
               result.succeeded -> {
                //update state
               }
           }
       }
   }
}

UseCase:
class GetNewsUseCase(
   private val newsRepo: NewsRepoInterface) {
   companion object {
       private val UPDATE_INTERVAL = 30.seconds
   }

   operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
       while (true) {
           emit(Result.loading())
           val result = newsRepo.getNews()

           if (result.succeeded) {
               // emit result
           } else {
               //emit error
           }

           delay(UPDATE_INTERVAL)
       }
   }.asCommonFlow()
}

Repository:
class NewsRepository(
   private val sourceNews: SourceNews,
   private val cacheNews: CacheNews) : NewsRepoInterface {

   override suspend fun getNews(): Result<List<News>> {
    
       val news = sourceNews.fetchNews()

          //.....

       cacheNews.insert(news) //could be a lot of news

       return Result.data(cacheNews.selectAll())
   }
}


Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)

class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
    coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
    callback: (T) -> Unit, // callback on each emission
) {
    onEach {
        callback(it)
    }.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}

我尝试移动存储库,所以也许我可以用单顿存储库打破循环,但是然后我必须更改 getNews 方法以在getNewSecase内部进行流动和收集(因此在另一个流程中流动)。


感谢您的帮助!

I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?

I designed the app like follows:

  • ViewModel (separate for Android and iOS)
  • UseCase (shared)
  • Repository (shared)
  • Data source (shared)
  • Android Jetpack compose single activity
  • iOS SwiftUI

Android ViewModel:(iOS use ObservableObject, but logic is the same)

@HiltViewModel
class NewsViewModel @Inject constructor(
   private val startFetchingNews: GetNewsUseCase,
   private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
   private val _mutableNewsUiState = MutableStateFlow(NewsState())
   val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()

   fun onTriggerEvent(action: MapEvents) {
       when (action) {
           is NewsEvent.GetNews -> {
            getNews()
          }
           is MapEvents.StopNews -> {
            //????
           }
           else -> {

           }
       }
   }

   private fun getNews()() {
       startFetchingNews().collectCommon(viewModelScope) { result ->
           when {
               result.error -> {
                //update state
               }
               result.succeeded -> {
                //update state
               }
           }
       }
   }
}

UseCase:

class GetNewsUseCase(
   private val newsRepo: NewsRepoInterface) {
   companion object {
       private val UPDATE_INTERVAL = 30.seconds
   }

   operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
       while (true) {
           emit(Result.loading())
           val result = newsRepo.getNews()

           if (result.succeeded) {
               // emit result
           } else {
               //emit error
           }

           delay(UPDATE_INTERVAL)
       }
   }.asCommonFlow()
}

Repository:

class NewsRepository(
   private val sourceNews: SourceNews,
   private val cacheNews: CacheNews) : NewsRepoInterface {

   override suspend fun getNews(): Result<List<News>> {
    
       val news = sourceNews.fetchNews()

          //.....

       cacheNews.insert(news) //could be a lot of news

       return Result.data(cacheNews.selectAll())
   }
}

Flow extension functions:

fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)

class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
    coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
    callback: (T) -> Unit, // callback on each emission
) {
    onEach {
        callback(it)
    }.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}

I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

苏别ゝ 2025-02-12 11:27:43

当您在流程中调用启动时,它将返回作业。请在属性中提到对此作业的引用,当您要停止收集它时,您可以在其上调用CANCEL()

我看不到CommonFlow类的点。您可以直接将collectCommon直接编写为flow的扩展功能。

fun <T> Flow<T>.collectCommon(
    coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
    callback: (T) -> Unit, // callback on each emission
): Job {
    return onEach {
        callback(it)
    }.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}

// ...

   private var fetchNewsJob: Job? = null

   private fun getNews()() {
       fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
           when {
               result.error -> {
                //update state
               }
               result.succeeded -> {
                //update state
               }
           }
       }
   }

我认为,collectCommon应该完全消除,因为它所做的一切只是使您的代码稍微混淆。它仅节省了一行代码,以牺牲清晰度为代价。创建一个coroutinesscope是一种反图案,其参考您不保留,因此您可以管理IT中的coroutines运行 - 以及使用GlobalScope,而是使用GlobalsCope,以确保您不打算管理范围范围的生命周期,以便清楚您会变得清楚您必须手动取消工作,而不仅仅是新闻源更改,而且当UI与范围相关联时,也是如此。

When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.

I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.

fun <T> Flow<T>.collectCommon(
    coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
    callback: (T) -> Unit, // callback on each emission
): Job {
    return onEach {
        callback(it)
    }.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}

// ...

   private var fetchNewsJob: Job? = null

   private fun getNews()() {
       fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
           when {
               result.error -> {
                //update state
               }
               result.succeeded -> {
                //update state
               }
           }
       }
   }

In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文