Suppose you have a list of users downloaded from a remote data source in your Android application, and for some reason you do not have a local DB. This list of users is then used throughout your entire application in multiple ViewModel
s to make other network requests, so you would surely like to have it cached for as long as the app lives and re-fetch it only on demand. This necessarily means you want to cache it inside the Data Layer, which is a Repository
in my case, to then get it from your ViewModel
s.
It is easy to do in a state holder like a ViewModel
- just make a StateFlow
or whatever. But what if we want a Flow
of List<User>
(that is cached in RAM after every API request) available inside a repository to then collect from it from the UI Layer? What is the most testable, stable and right way of achieving this?
My initial idea led to this:
class UsersRepository @Inject constructor(
private val usersApi: UsersApi,
private val handler: ResponseHandler
) {
private val _usersFlow = MutableStateFlow<Resource<List<UserResponse>>>(Resource.Empty)
val usersFlow = _usersFlow.asStateFlow()
suspend fun fetchUserList() = withContext(Dispatchers.IO) {
_usersFlow.emit(Resource.Loading)
_usersFlow.emit(
handler {
usersApi.getUsers()
}
)
}
}
Where ResponseHandler
is:
class ResponseHandler {
suspend operator fun <T> invoke(block: suspend () -> T) = try {
Resource.Success(block())
} catch (e: Exception) {
Log.e(javaClass.name, e.toString())
val errorCode = when (e) {
is HttpException -> e.code()
is SocketTimeoutException -> ErrorCodes.SocketTimeOut.code
is UnknownHostException -> ErrorCodes.UnknownHost.code
else -> Int.MAX_VALUE
}
Resource.Error(getErrorMessage(errorCode))
}
}
But while researching I found random guy on the internet telling that it is wrong:
Currently StateFlow is hot in nature so it’s not recommended to use in repository. For cold and reactive stream, you can use flow, channelFlow or callbackFlow in repository.
Is he right? If he is, how exactly do cold flows help in this situation, and how do we properly manage them?
If it helps, my UI Layer is written solely with Jetpack Compose
In the official "Guide to app architecture" from Google for Android:
About the source of true: ✅ The repository can contain an in-memory-cache.
The source of truth can be a data source—for example, the database—or even an in-memory cache that the repository might contain. Repositories combine different data sources and solve any potential conflicts between the data sources to update the single source of truth regularly or due to a user input event.
About the lifecycle: ✅ You can scope an instance of your repository to the Application class (but take care).
If a class contains in-memory data—for example, a cache—you might want to reuse the same instance of that class for a specific period of time. This is also referred to as the lifecycle of the class instance.
If the class's responsibility is crucial for the whole application, you can scope an instance of that class to the Application class. This makes it so the instance follows the application's lifecycle.
About the implementation: I recommend you to check the link directly.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews()
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
You should read the following page, I think it will answer a lot of your questions : https://developer.android.com/topic/architecture/data-layer