Mohammed Akram Hussain
11/29/2024, 12:11 PM@HiltViewModel
class HomeViewModel @Inject constructor(
repository: Repository
) : ViewModel() {
val uiState = repository.data
.mapLatest { result ->
when (result) {
is Result.Success -> UiState.Success(result)
is Result.Error -> UiState.Error(result.exception.message ?: "Unknown error")
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = UiState.Loading
)
sealed class UiState {
data object Loading : UiState()
data class Success(val data: Data) : UiState()
data class Error(val message: String) : UiState()
}
}
class Repository @Inject constructor(
private val api: Api
) {
private val _data = MutableStateFlow<Result<Response>?>(null)
val data = _data.asStateFlow().filterNotNull()
suspend fun getData() {
try {
val response = api.getDataFromNetwork()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
_data.emit(Result.Success(body))
}
} else {
val exception = Exception("Server Error: ${response.code()}")
_data.emit(Result.Error(exception))
}
} catch (e: Exception) {
_data.emit(Result.Error(e))
}
}
}
The above is the approach suggest in the architecture samples HERE but I don't understand how it'll work for network call where caching or offline support is not required. The two main problems I am facing with this approach for the above are:
1. How should I trigger the initial data load when the screen opens?
2. What's the best way to implement a refresh mechanism (like pull-to-refresh)?
Is this Flow-based approach overkill for simple network calls without caching? Should I consider a different approach? Would appreciate any guidance or best practices!Ahmed
11/29/2024, 12:24 PMmapLatest
.
Let's start by rewriting the code to adjust and solidify our understanding. We do not want any kind of state variable in the repository, so we eliminate
getData()
function. We change it to cold flow by using the flow
builder (cold).
fun getData() = flow {
…
}
This will return us a cold flow.
Now within ViewModel
we will add relevant operators.
val uiState = repository.getData()
.mapLatest {
// Transform data here to ui state
}.stateIn (
…
)
> How should I trigger the initial data load when the screen opens?
We finally used stateIn
to convert cold flow to hot flow. Now, when a subscribe appears on uiState, the data will start 'flowing'.
> What's the best way to implement a refresh mechanism
Use a trigger mechanism. If using compose, you can use snapshotflow
Also, next time when there is a long message, consider prefacing the question and then adding 🧵 .
You can then reply yourself with all the relevant details.Mohammed Akram Hussain
11/29/2024, 2:20 PMWe do not want any kind of state variable in the repository, so we eliminateDo you mean we don't want the state variables only in this case? but is acceptable in other cases as in the sample.
Mohammed Akram Hussain
11/29/2024, 2:24 PMUse a trigger mechanism. If using compose, you can useAlso, can you please provide a sample for this if possible like above? I'm using composesnapshotflow
Ahmed
11/29/2024, 2:25 PMClasses in the data layer generally expose functions to perform one-shot Create, Read, Update and Delete (CRUD) calls or to be notified of data changes over time.It will answer a lot of your questions.
Mohammed Akram Hussain
11/29/2024, 7:41 PMgetData()
function to be a cold flow and observe it in ViewModel using stateIn
do we not need to specify coroutine dispatcher for it? like withContext(<http://Dispatchers.IO|Dispatchers.IO>)
as a network call should be main safe?Ahmed
11/30/2024, 9:04 AMAhmed
11/30/2024, 9:07 AMThreading
.
Note that most data sources already provide main-safe APIs like the suspend method calls provided by Room, Retrofit or Ktor. Your repository can take advantage of these APIs when they are available.So it depends on the client you are using. But you can still use
.flowOn(ioDispatcher)
for your flow.