kotlinkotlin-flowsuspend

When to use suspend function and Flow together or seperate in kotlin?


While reviewing some code written in kotlin ,something caught my attention. I was looking about domain layer in some projects and in some of the projects, I saw that the suspend function and Flow were used together, and in some of the projects, I saw that only Flow was used.

for example suspend and Flow together :

class FetchMovieDetailFlowUseCase @Inject constructor(
    private val repository: MovieRepository
) : FlowUseCase<FetchMovieDetailFlowUseCase.Params, State<MovieDetailUiModel>>() {

    data class Params(val id: Long)

    override suspend fun execute(params: Params): Flow<State<MovieDetailUiModel>> =
        repository.fetchMovieDetailFlow(params.id)
}

just Flow

class GetCoinUseCase @Inject constructor(
    private val repository: CoinRepository
){
 
    operator fun invoke(coinId:String): Flow<Resource<CoinDetail>> = flow {

        try {
            emit(Resource.Loading())
            emit(Resource.Success(coin))

        }catch (e:HttpException){
            emit(Resource.Error(e.localizedMessage ?: "An unexpected error occured"))
        }catch (e:IOException){
            emit(Resource.Error("Couldn't reach server. Check your internet connection."))
        }
    }
}

just suspend

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend operator fun invoke(): List<ArticleWithAuthor> =
        withContext(defaultDispatcher) {
            val news = newsRepository.fetchLatestNews()
            val result: MutableList<ArticleWithAuthor> = mutableListOf()
            // This is not parallelized, the use case is linearly slow.
            for (article in news) {
                // The repository exposes suspend functions
                val author = authorsRepository.getAuthor(article.authorId)
                result.add(ArticleWithAuthor(article, author))
            }
            result
        }
}

All three are different projects, don't get stuck with the codes, these are just the projects I've come across, I'm sharing to show examples, but what I want to draw your attention to here is that sometimes only the suspend function is used, sometimes only Flow is used, and sometimes both are used. What is the reason of this ? can you explain in detail ? I'm trying to make this into my logic


Solution

  • suspend functions make sense for retrieving a single item in a suspending, synchronous way.

    Flows return multiple items over time, when you decide to start collecting it. They are often used to monitor something that changes over time to retrieve the most up-to-date value of something. For example, a flow from a database can emit a new value each time something in the database changes.

    It rarely makes sense to combine both, using a suspend function that retrieves a Flow. This is because the Flow itself is a lightweight object that typically doesn't require any up-front, time-consuming work to retrieve and create.

    The reason to avoid making a function suspend when returning a flow is that it makes it more cumbersome to use. There might be cases where you want to pass a flow along without collecting it yet, and you don't want to have to launch a coroutine just to get the flow. Or, you might just want to use this nice, concise syntax for creating a dedicated coroutine for collection:

    someUseCase.retrieveSomeFlow()
        .onEach { ... }
        .launchIn(someScope)
    

    instead of:

    someScope.launch {
        someUseCase.retrieveSomeFlow().collect {
            ...
        }
    }
    

    An exception might be if you have to retrieve something to use as a key that is a basis for the Flow. For example, retrieving a user ID from an API and then returning a Flow that uses that user ID to fetch items for the flow. But even this is often unnecessary, since you can put that ID retrieval into the Flow. For example:

    fun getUserItems(): Flow<UserItem> = flow {
        val userId = someAPI.retrieveUserId() // calling a suspend function
        emitAll(someAPI.getUserItemsFlow(userId))
    }
    

    The only downside with this is that if you collect the flow multiple times, it would have to retrieve the user ID again each time collection begins.