apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.75k stars 651 forks source link

Provide an api for serializing objects for internal communication #714

Closed lincolnq closed 6 years ago

lincolnq commented 6 years ago

I, and probably others, would benefit from a feature which allows manually marshalling/unmarshaling objects to some serializable format -- it could be JSON or something else. The main thing this would help with is cross-Activity communication, where Intent extras require Parcelable objects -- I often want to load something from the server on one screen and then pass it to another screen once it's loaded. Other use cases would be storing response objects in a database or somewhere. The most ergonomic solution would be to make the whole class hierarchy Parcelable, but any way to provide this functionality like apolloClient.serialize(object) would suffice.

Currently the code does have ways of doing this kind of thing internally, because the RealApolloStore has code to save response objects to its cache. But constructing a RealApolloStore for in-memory use is a challenge because it has many package-private dependencies, and it requires a key-value store anyway, which doesn't fit very well with my goal. The best (& only?) way I can see to do it with the current api is to manually implement JsonWriter's methods. The InputFieldJsonWriter would work except that it is (surprisingly) not a JsonWriter -- it's an InputFieldWriter -- so cannot be used to marshal anything but input field objects.

digitalbuddha commented 6 years ago

I'm not sure if this is something that Apollo should be helping with. In general its an anti pattern to be passing anything more than IDs from one activity to another, particularly when you are using a caching data layer like Apollo-Android has. Any reason you cannot leverage the Apollo Store or the HTTP cache to re-request the same data from your second activity? Both the HTTP Cache and the normalized Apollo-Store would return the same data to you. If you do want to go the non recommended direction of passing large objects between activities what is preventing you from using Gson or Jackson to convert the apollo data models to/from json?

lincolnq commented 6 years ago

Thanks for your reply. I just tried it with Jackson and that seems like a pretty decent solution to this. The object I want to pass is the result of a Mutation so not cacheable by default. Jackson/GSON is an extra dependency, so it's not ideal but certainly solves the problem well enough to proceed, so if not many other people have a similar need then i'm ok closing this!

EDIT: Jackson doesn't work with most default generated Apollo classes, as others below have noted; my use case "got lucky". There should be a better solution.

sav007 commented 6 years ago

Mutation result is cacheable by default, means that normalized cache will be updated with the response returned after mutation execution. If you want to not cache mutation response then you have to add cache header ApolloCacheHeaders.DO_NOT_STORE.

So technically mutation updates are cacheable. Could you please provide concrete example of use case you are trying to achieve?

At the same time what we can do, because all our generated models have serialization / deserialization code in place, we can try to provide JsonResponseReader / JsonResponseWriter that can be passed to com.apollographql.apollo.api.ResponseFieldMapper#map / com.apollographql.apollo.api.ResponseFieldMarshaller#marshal

But what I would recommend is having another layer of abstraction ( view models for example) that should be very light weight pojo objects and can be passed between activities.

alissongodoi commented 6 years ago

I am also struggling to do some seriailizing/unserializing with the results from GraphQL. It would just be easier from my app perspective to just serialize/unserialize the response in the SharedPrefs in Android instead of using Android GraphQL cache for data persistence purposes. I run a query everyday from my devices and everyday the query request changes (for example, fetchResultsForDay(date:2017-11-08) returns a result) . If the query parameters changes, it won't hit the cache (for example, fetchResultsForDay(date:2017-11-09). However, if the device is offline, I would expect my app to use the data from yesterday's query (2017-11-08) to process critical information (besides it is 1 day ago).

That's why in my scenario just saving the query results in SharedPrefs would do it, since I could load it if the next day the device is offline.

However due to the way the classes in Apollo are built (non-default constructor for Jackson/Gson/Moshi and don't extends Serializable), I can't serialize/de-serialize the data from my local filestore easily. In summary, a simple Save to JSON file and Read from JSON local file would do it in my current context instead of handling complex Cache database operations.

My query is also not hitting the cache since I believe the parameter in the request changed.

Few things that I struggled when trying to use Jackson (which provides the most comprehensive set of Data Mapping for my types - like Joda Time):

I tried Moshi and Gson, however so many mapping issues to get fixed around LocaDate and LocalDateTime that I was not able to properly finalize the testing.

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class XYZ]: can not instantiate from JSON object (missing default constructor

sav007 commented 6 years ago

We don't recommend to serialize operation data into json and pass between activities. But there might be other cases where it's needed (like dump operation data as json to the disk), so please check opened PR.

lincolnq commented 6 years ago

@sav007 thanks again for your response. My use case is a mutation which looks something like this:

mutation LogInMutation($user: String, $pass: String) {
   logIn(user: $user, pass: $pass) {
        sessionId
        user { id, name, mobile, complicatedObject }
   }
}

and then, I want to pass the user info thus retrieved to another activity (let's call it HomeScreenActivity) to show your information while you're logged-in. That activity wants to have the latest user info as soon as it opens; it can get it by doing this query:

query UserQuery($id: ID) {
  user(id: $id) { id, name, mobile, complicatedObject }
}

but it wants to initially show the user info obtained during the LogInMutation before the server round-trip happens, to avoid a "flicker" of missing info before the HomeScreenActivity's first refresh completes.

There seem to be two ways of going about it:

Regarding PR #717, this OperationJsonWriter seems like it solves half the problem (that of writing out the user object returned from the session) but I don't see how it helps with loading such an object back into memory.

lincolnq commented 6 years ago

I just added PR #727 as a strawman which enables Idea 2 above to work the way I expect. Comments welcome.

jcihocki commented 6 years ago

I'm curious if there are reasons not to decorate the generated code types with Serializable? Are there non serializable types used under the covers that I'm unaware of? Is it just for future proofing?

EngMahmoudMagdy commented 5 years ago

So guys, is still the same issue only solved using JSON converting the object between the Activities?