Sam King
04/15/2024, 12:06 PM5.1.0-alpha02
and was wondering if anyone could help?
I followed [this](https://www.droidcon.com/2023/08/01/intercepting-ktor-network-responses-in-kotlin-multiplatform/) advise of adding a safe get method which returns a result type with either data or an error type in order to handle the situation when the device does not have an internet connection causing a <http://java.net|java.net>.UnknownHostException
.
This is based on the following from the documentation:
When an error happens, Store does not throw an exception, instead, it wraps it in a StoreResponse.Error type which allows Flow to continue so that it can still receive updates that might be triggered by either changes in your data source or subsequent fetch operations.So if I throw an exception here like this:
kotlin
override fun provide(): Store<Int, List<Pokemon>> {
return StoreBuilder
.from(
fetcher = Fetcher.of { _: Int ->
when (val pokemon =
pokemonService.getPokemon(offset = 0, limit = PokemonService.pageSize)) {
is ApiResponse.Success -> {
pokemon.body.results.mapIndexed { index, pokemon ->
Pokemon(
index,
pokemon.name
)
}
}
is ApiResponse.Error.HttpError -> throw Throwable("${pokemon.errorMessage}")
is ApiResponse.Error.GenericError -> throw Throwable("${pokemon.errorMessage}")
is ApiResponse.Error.SerializationError -> throw Throwable("${pokemon.errorMessage}")
}
},
sourceOfTruth = SourceOfTruth.of(
reader = { pokemonDao.selectAll() },
writer = { page, pokemon -> pokemonDao.insertAll(pokemon, page) },
delete = { page -> pokemonDao.delete(page) },
deleteAll = { pokemonDao.deleteAll() },
)
)
.build()
}
...I should be able to parse the StoreReadResponse.Error
when I collect the results and this will not interrupt the flow:
kotlin
pokemonStore.stream(StoreReadRequest.cached(key = page, refresh = false))
.collect { response ->
pokemonListUiState.value = when (response) {
... //handling other cases here omitted for brevity
is StoreReadResponse.Error -> {
//update UI with read error
}
...
}
}
Currently when I get an error and throw it there it will throw the exception there and it is not propagated and this causes the app to crash with the exception thrown.
FATAL EXCEPTION: main
Process: com.king.pokestore, PID: 3172
java.lang.Throwable: Unable to resolve host "<http://pokeapi.co|pokeapi.co>": No address associated with hostname
at com.king.pokestore.feature.pokemonlist.PokemonListStoreProvider$provide$1.invokeSuspend(PokemonListStoreProvider.kt:32) //this is the line where I throw the GenericError type
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@8e19663, Dispatchers.Main.immediate]
Am I doing something wrong to prevent the error from propagating? Some help would be greatly appreciated! :)Matthew Ramotar
04/17/2024, 4:41 PMFetcherResult
, which can hold data or an error. In your fetcher binding, convert the caught error to a FetcherResult.Error
. Here's an example: https://github.com/MobileNativeFoundation/Store/discussions/631#discussioncomment-8814038Sam King
04/18/2024, 6:51 PMwriter = { page, pokemon ->
when(pokemon){
is FetcherResult.Data -> pokemonDao.insertAll(pokemon.value, page)
else -> {
Timber.e("Error retrieving data")
}
} },
I've implemented this and while it does handle the error I've noticed the error still doesn't propagate the error to my ViewModel so I end up getting to the no new data stage without handling the error.Matthew Ramotar
04/22/2024, 9:10 PMMatthew Ramotar
04/23/2024, 5:25 PMFetcherResult
. Any thrown error should be caught and propagated by Store.
Below is a working sample. Take a look and let me know if you notice any setup differences between mine and yours. Also happy to review a full implementation if you can share it
@Inject
class UserStoreFactory(
private val networkingClient: NetworkingClient
) {
fun create(): UserStore {
val storeBuilder = StoreBuilder.from<GetUserQuery, GetUserQuery.User, GetUserQuery.User>(
fetcher = Fetcher.of { query: GetUserQuery ->
throw Throwable("TEST")
},
sourceOfTruth = SourceOfTruth.of<GetUserQuery, GetUserQuery.User, GetUserQuery.User>(
reader = {
flowOf(GetUserQuery.User("", null, "", GetUserQuery.Repositories(emptyList())))
},
writer = { _, __ ->
}
)
)
return storeBuilder.build()
}
}
// Sample Usage (do not recommend using Store in a launched effect in real code)
LaunchedEffect(Unit) {
try {
coreComponent.userStore.stream(StoreReadRequest.fresh(GetUserQuery("matt-ramotar"))).collect {
println("STORE RESPONSE = $it")
}
} catch (error: Throwable) {
println("ERROR NOT PROPAGATED BY STORE = $error")
}
}
// Output
STORE RESPONSE = Loading(origin=Fetcher(name=null))
STORE RESPONSE = Exception(error=java.lang.Throwable: TEST, origin=Fetcher(name=null))
Sam King
05/07/2024, 4:36 PMclass PokemonListStoreProvider(
private val pokemonService: PokemonService,
private val pokemonDao: PokemonDao,
) : StoreProvider<Int, List<Pokemon>> {
override fun provide(): Store<Int, List<Pokemon>> {
return StoreBuilder
.from(
fetcher = Fetcher.of { _: Int ->
when (val pokemon =
pokemonService.getPokemon(offset = 0, limit = PokemonService.pageSize)) {
is ApiResponse.Success -> {
val results = pokemon.body.results.mapIndexed { index, pokemonDto ->
Pokemon(
index,
pokemonDto.name
)
}
FetcherResult.Data(results)
}
is ApiResponse.Error.HttpError -> throw Throwable("${pokemon.errorMessage}")
is ApiResponse.Error.GenericError -> throw Throwable("${pokemon.errorMessage}") // this is the line where my exception is thrown.
is ApiResponse.Error.SerializationError -> throw Throwable("${pokemon.errorMessage}")
}
},
sourceOfTruth = SourceOfTruth.of(
reader = { pokemonDao.selectAll() },
writer = { page, pokemon ->
pokemonDao.insertAll(pokemon.value, page)
},
delete = { page -> pokemonDao.delete(page) },
deleteAll = { pokemonDao.deleteAll() },
)
)
.build()
}
}
class PokemonListViewModel(
private val pokemonStore: Store<Int, List<Pokemon>>,
) : ViewModel() {
var pokemonListUiState: MutableStateFlow<PokemonListUIState> =
MutableStateFlow(PokemonListUIState())
private set
init {
viewModelScope.launch {
val page = pokemonListUiState.value.page
Timber.i("Loading Pokemon list for page: $page")
try {
pokemonStore.stream(StoreReadRequest.fresh(key = page))
.collect { response ->
pokemonListUiState.value = when (response) {
is StoreReadResponse.Loading -> {
Timber.i("[State] ${UiState.Loading}")
pokemonListUiState.value.copy(
state = UiState.Loading,
error = "",
)
}
is StoreReadResponse.Data -> {
Timber.i("[State] ${UiState.NoNewData}")
pokemonListUiState.value.copy(
state = UiState.NoNewData,
pokemon = response.value,
endReached = response.value.isEmpty(),
error = "",
)
}
is StoreReadResponse.NoNewData -> {
Timber.i("[State] ${UiState.NoNewData}")
pokemonListUiState.value.copy(
state = UiState.NoNewData,
endReached = true,
error = "",
)
}
is StoreReadResponse.Error -> {
Timber.i("[State] ${UiState.Error}")
pokemonListUiState.value.copy(
state = UiState.Error,
error = response.errorMessageOrNull() ?: "Unknown error",
)
}
StoreReadResponse.Initial -> {
Timber.i("[State] ${UiState.Idle}")
pokemonListUiState.value.copy(
state = UiState.Loading,
error = "",
)
}
}
}
} catch (e: Exception) { //this doesn't catch the exception
e.message?.let {
pokemonListUiState.value.copy(
state = UiState.Error,
error = it
)
}
}
}
}
fun loadMore() {
...
}
fun refresh() {
...
}
}
Sam King
05/07/2024, 4:37 PM17:37:00.430 E FATAL EXCEPTION: main
Process: com.king.pokestore, PID: 5387
java.lang.Throwable: Unable to resolve host "<http://pokeapi.co|pokeapi.co>": No address associated with hostname
at com.king.pokestore.feature.pokemonlist.PokemonListStoreProvider$provide$1.invokeSuspend(PokemonListStoreProvider.kt:34)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@10c559b, Dispatchers.Main.immediate]
blakelee
10/31/2024, 7:22 PM.catching { ... }
in the fetcher but for some reason it's not catching it. I was digging through coroutine code all night to figure it out.Matthew Ramotar
10/31/2024, 7:31 PMMatthew Ramotar
10/31/2024, 7:32 PMblakelee
10/31/2024, 7:35 PMThrowable
and it still wasn’t catching it and was crashing it instead. It’s pretty simple fetcher code
Fetcher.of(
fetch = { key: EventKey ->
require(key is EventKey.Read)
when (key) {
is EventKey.Read.ByEventId -> api.getEventById(key.eventId)
EventKey.Read.All -> api.getEvents().run {
NetworkEvent(EventData.Collection(this), null)
}
}
}
)
Or with the catch my test code looked like this but it was still crashing the application
Fetcher.of(
fetch = { key: EventKey ->
require(key is EventKey.Read)
when (key) {
is EventKey.Read.ByEventId -> api.getEventById(key.eventId)
EventKey.Read.All -> runCatching {
api.getEvents().run {
NetworkEvent(EventData.Collection(this), null)
}
}.recoverCatching { it: Throwable ->
throw Throwable()
}.getOrThrow()
}
}
)
I’m beginning to think it might be a Kotlin thing rather than a store thingblakelee
10/31/2024, 7:37 PMresult = someCoroutine()
but this would
val response = someCoroutine()
result = response
Matthew Ramotar
10/31/2024, 7:42 PMblakelee
10/31/2024, 7:48 PMblakelee
10/31/2024, 7:50 PMMatthew Ramotar
10/31/2024, 7:53 PMblakelee
10/31/2024, 8:01 PMMutableStoreBuilder.from(
fetcher = fetcher,
sourceOfTruth = sourceOfTruth,
converter = converter
).build(updater, bookkeeper)
3. The read request is a Ktor HttpClient
backed by OkHttp for Android. No specific dispatcher is set and I’m not using withContext(…) { … }
for the request. Just a straight KtorClient.get
Matthew Ramotar
10/31/2024, 8:04 PMblakelee
10/31/2024, 8:06 PMMatthew Ramotar
11/01/2024, 12:15 AMMatthew Ramotar
11/01/2024, 12:15 AMblakelee
11/01/2024, 12:16 AMblakelee
11/01/2024, 12:42 AMstore.fresh
It filters out New and NoNewData, gets the first element, then expects a result. In my case I wasn’t catching the exception so it would crash. It would never point to that call sight as the point of the issue which made it hard to find. The person above seems to be using stream so it might very well be a different issueMatthew Ramotar
11/01/2024, 1:28 AMblakelee
11/01/2024, 6:03 AMMatthew Ramotar
11/01/2024, 12:30 PMblakelee
11/04/2024, 6:55 PM