Open langsmith opened 9 months ago
Hey there. Regarding https://github.com/airbnb/mavericks/issues/692, we don't use Hilt or the navigation component at Airbnb, so I don't really have an opinion or expertise to share. Maybe other people in the community have feedback for you on it though.
Or is Mavericks intentionally designed to just throw a pretty generic Fail with the error code and nothing else? Does the backend error need to be structured differently for Mavericks' code to correctly interpret it and provide more info in the Fail?
Check out the source code for the execute function
try {
val result = invoke()
setState { reducer(Success(result)) }
} catch (e: CancellationException) {
@Suppress("RethrowCaughtException")
throw e
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
setState { reducer(Fail(e, value = retainValue?.get(this)?.invoke())) }
}
Mavericks just catches any exception thrown by your suspend function and saves it in the Fail class. So if you want specific data out of the exception, you need to throw an exception containing that failure data. Mavericks has nothing to do with parsing your json or interpreting your exception.
If you do have a custom exception class with your data, you would have to cast it from the Fail instance, which isn't ideal for type safety, but also not terrible if you limit where you need to do so. I suppose the Fail class could have a generic type for the exception, but it's a bit late to introduce that at this point. For us, we have standardized that the errorMessage of the Throwable from network requests is the user facing message to show, so we can easily extract that to present in the UI. For example, it sounds like you need to change your networking layer to throw an exception with your reason
field as the error message.
@gpeal @elihart @rossbacher, I made what I thought was a pretty specific and thorough thread at https://github.com/airbnb/mavericks/issues/692 but never heard back, which is totally fine. I get that life happens and open-source work is a beast of its own. Really hoping for a response on this ticket's topics 👇🏽 though
tldr; Is there a way to combine the latest version of Mavericks with "custom" network response and error handling? A way to combine Mavericks with Retrofit API
Response
/callback/adapters?This whole ticket is born out two high-level end goals, which are to:
Loading
so that progress indicators and other UI can tap into a network call'sLoading
state.Here are examples of the JSON body that the backend sends the app when there's a error:
or
I've already got the JSON object modeled as POJO 👇🏽
Ideally, the Mavericks Async
Fail
would end up with themessage
in it.When looking at Mavericks'
data class Fail
, it seems that users aren't given much flexibility.Ideally, I wouldn't have to create my own custom sealed class setup with
Uninitialized
,Error
,Success
,Loading
, and so on. I like Mavericks'Async
class, especially because it hasLoading
built in and I've got various progress indicator UI tied toLoading
.App context
Single activity, multiple fragment Android app. Using Kotlin, Retrofit, Coroutines, Moshi, View binding, Navigation Component, and Hilt. Nothing crazy.
App architecture:
Fragment
<->ViewModel
<->Repository
<–>RetrofitService
. Nothing crazy.Mavericks and Retrofit Gradle versions
Log in as an example scenario of what I'm talking about:
LoginState
hasval loginToken: Async<LoginToken> = Uninitialized
and 👇🏽 in the ViewModelViewModel's log in method 👇🏽
Repository method 👇🏽
Retrofit service endpoint is
Right now, this setup works correctly and shows a circular progress indicator when
loginToken
isLoading
, thanks to theState
's derived propertyval showProgressInButton = loginToken is Loading
and theFragment
'sinvalidate()
After submitting an incorrect password, the backend sends a
401
error with a JSONThe Timber line above prints
LoginState loginToken = retrofit2.HttpException: HTTP 401 , cause = null, message = HTTP 401
cause
andmessage
arenull
. Is there a recommended way to (simply) go from receiving the JSON body to the MavericksFail
having some sort of content from the JSON body? A hacky way? Casting somehow?Or is Mavericks intentionally designed to just throw a pretty generic
Fail
with the error code and nothing else? Does the backend error need to be structured differently for Mavericks' code to correctly interpret it and provide more info in theFail
?Because I don't see a way to get access to the JSON info, I'm looking into the often suggested route of creating and using a custom sealed class with networking states. From what it looks like, it's essentially my own version of Mavericks'
Async.kt
class 👇🏽Custom
Result.kt
situationI'm sure there are issues with it, but I've got 👇🏽 for now.
So, with the
Result.kt
class above, I can doval loginToken: Result<LoginToken?> = Result.Uninitialized
in theLoginState
.Doing
Result<LoginToken>
instead ofAsync<LoginToken>
now means:.execute
extension function no longer worksLoading
aspect of the networking callLoading
to have the circular progress indictor simply tied toResult.Loading
.If I MUST have a custom
Result.kt
class to create custom errors, any thoughts on a way to somehow combine it with Mavericks? I have👇🏽 working welland
Capturing exceptions
I've created a
handleResponse()
method, which takes in a Retrofit2Response
object and checks the response object. I'm hoping to use it for all networking response object values that are wrapped withResult
in any of myViewModel
s (e.g.val loginToken: Result<LoginToken?> = Result.Uninitialized
)Do notice that it returns some type of
Result
from the customResult.kt
class I made and described above.val metadataErrorResponse = convertErrorBody(moshi, response.errorBody())
above is what parses the error's JSON body.This
handleResponse()
method allows me to do 👇🏽 in the repositoryRetrofit endpoint is now wrapped with
Response
Running
setState { copy(loginToken = Loading()) }
at the beginning of theViewModel
method works and correctly leads to showing the circular progress indicator in the fragment. Ideally, I wouldn't have to (remember to) add thissetState { copy(loginToken = Loading()) }
code before all calls to any repository in all of myViewModel
s.I tried my hand at a custom implemention of
.execute
with the.customExecute()
method below, but it didn't work.👇🏽 didn't work 😕 with the method above
Any ideas on what a correct custom
.execute
might look like so thatLoading
is always first set? Is the.customExecute()
method above, close to being correct?Some other way to handle the response instead of 👇🏽?
Regarding error handling, I've seen:
They're all in my ticket's territory but not really answering what I'm wondering. So yea, @gpeal @elihart @rossbacher, I'd really appreciate any and all thoughts on how to approach this. It seems like a fairly standard use case for Mavericks-loving users. I hope this comment, and hopefully discussion, becomes a resource for anyone else trying to figure out similar things with Mavericks.