Skip to content

Parcelable object #625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 19, 2017
Merged

Conversation

natario1
Copy link
Contributor

@natario1 natario1 commented Apr 4, 2017

This is a working stub that implements Parcelable interface into ParseObject ( #122 ).

  • Implements Parcelable into ParseACL
  • Implements Parcelable into ParseRelation
  • Implements Parcelable into ParseObject. It parcels both server data and dirty data. Internally this is done by parceling the ParseObject.State for server data, and the ParseOperationSets attached to the object. These sets are squashed into a single ParseOperationSet, as outlined by grantland in ParseObject implements Parcelable #122 , and the merged set is saved to parcel.
  • Creates ParseParcelEncoder and ParseParcelDecoder, utility classes that help parceling when we host untyped lists (e.g. Map<String, Object>). They work by saving a type string into the parcel and checking that when unparceling. Eventually these classes will be extended to work for ParseQuery as well.
  • Introduces (untested) smooth parceling for ParseObject subclasses. They do not need to provide a CREATOR static field. Instead, we have two new protected methods in ParseObject which should be familiar to any developer, onSaveInstanceState(Bundle) and onRestoreInstanceState(Bundle). For example, they are used in ParseUser to keep the isCurrentUser boolean. We pass a Bundle because that’s much safer than a Parcel.
  • Should work out of the box for internal subclasses: ParsePin, ParseInstallation, ParseSession, ParseRole, EventuallyPin and finally ParseUser
  • Special treatment for ParseUser since it has an internal State class and own fields (isNew, isCurrentUser).
  • Warns the developer (with Log.w) if the object about to be parceled has ongoing save tasks / scheduled saveEventually tasks, to make him aware of the implications outlined by grantland in ParseObject implements Parcelable #122 .
  • Warns the developer (with Log.w) if the object has ongoing delete tasks / scheduled deleteEventually tasks, as above

Approach

My approach was to never give Parcelable to inner classes (e.g. ParseObject.State, ParseACL.Permissions) even if they need to be parceled. That is because unparceling requires the CREATOR field to be publicly accessible, and we don’t want to make ParseObject.State public.

Instead, simple methods like writeToParcel or constructor(Parcel) are implemented to inner classes, and are called from the outer public class. Internally this ensures also inheritance when needed (e.g. ParseUser.State that extends ParseObject.State). We use Parcelable only when the developer would benefit from it.

Consistency

I built this trying to keep consistency with JSON encoding / decoding that is already built in into classes. For example

  • ParseParcelDecoder / Encoder works just like ParseDecoder / Encoder for JSON
  • ParseOperationSet has toRest(ParseEncoder) and fromRest(JSONObject, ParseDecoder). Added toParcel(Parcel, ParseParcelEncoder) and fromParcel(Parcel, ParseParcelDecoder)
  • ParseFieldOperation interface has encode(ParseEncoder) and now encode(Parcel, ParseParcelEncoder)
  • Same for ParseFieldOperationFactory for decoding

Recursiveness

This now avoids StackOverflowErrors caused by nested ParseObject in the object tree. It’s pretty common for a ParseObject to be self referenced by one of its keys. This can be explicit (e.g. object.put(“itself”, object) or more obscure (e.g. ParseRelation is parceled when the parent object is parceled, but the relation itself holds a reference to the parent...).

This is solved by using stateful encoders / decoders called ParseObjectParcelEncoder and ParseObjectParcelDecoder. When the encoder traverses the objects / keys tree,

  • it holds a reference to parceled objects
  • when asked to parcel an object that was already parceled, it is parceled as a pointer (just class name and id)

Later, when the decoder parses the parcel,

  • it holds a reference to unparceled objects
  • when it finds a pointer, and an object with that id was already unparceled, it uses the same instance

This is pretty much what KnownParseObjectDecoder is already doing for JSON decoding, so there is still consistency with JSON stuff.
The required encoder / decoder instance is passed to every class so it can safely keep trace of actions.

Ongoing tasks and LDS

By default this follows what was told in #122, that is, if there are ongoing save / save eventually tasks and we parcel, the tasks might succeed or fail but the unparceled object will act as if they failed.
The operation set queue is squashed to a single ParseOperationSet, that is parceled and restored, and contains the dirty changes from the last successful save at the moment of parceling.

If local datastore is enabled and we manage to get the object from LDS memory when unparceling, we can have better behavior because the same instance is restored. In this case the original operation set queue is kept safe, and the unparceled object will be internally updated when save / delete tasks end.

What do you think of this approach?
I think there’s not much missing anymore.

@natario1
Copy link
Contributor Author

natario1 commented Apr 4, 2017

An issue I can see (off the top of my head) is that in special occasions we might stack overflow, for example object.put(“itself”, object) when parceled will go on forever, right..?

However if this is an issue, it is already addressed by the SDK, because this will be true also with JSON encoding / decoding that the SDK already does for caching. So we should just see how it’s addressed in that case, and emulate the behavior. I think it’s about subclassing ParseParcelableEncoder to not encode ParseObject in some occasions... Or to encode as a pointer... Anyway, this can be addressed once all the TODOs are fixed.

@@ -94,6 +98,14 @@ private static ParseObjectSubclassingController getSubclassingController() {
return new Builder(className);
}

/* package */ static State createFromParcel(Parcel source) {
String className = source.readString();
if ("_User".equals(className)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have this constant defined somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s left as _User everywhere, e.g. a few lines above.

protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
synchronized (mutex) {
outState.putBoolean("_isCurrentUser", isCurrentUser);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, should be a constant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’ll fix this

@rogerhu
Copy link
Contributor

rogerhu commented Apr 4, 2017

Are you talking about some type of recursive put? Yeah there should be a check to make sure you're not putting an object against yourself. I don't think Parcelable changes that fact (or might exacerbate it?)

In general I think the challenge with supporting this functionality is going to be all the inflight operations that occur. I think the relative mechanics of implementing a parcelable interface is a lot of plumbing work. We just have to make sure we have in the docs about best practices with this stuff.

@natario1
Copy link
Contributor Author

natario1 commented Apr 4, 2017

Well not a recursive put, but recursive parceling because we parcel all dirty data for all objects, even child objects. The recursion might also be more hidden, e.g.

user.setLastPost(post);
post.put(“author”, user);
// Then parcel post...

I wrote a similar test and it throws StackOverflowError when parceling. Any of you know how this is managed when JSON encoding ParseObjects?

@facebook-github-bot
Copy link

@natario1 updated the pull request - view changes

@facebook-github-bot
Copy link

@natario1 updated the pull request - view changes

@facebook-github-bot
Copy link

@natario1 updated the pull request - view changes

@codecov
Copy link

codecov bot commented Apr 5, 2017

Codecov Report

Merging #625 into master will increase coverage by 1.35%.
The diff coverage is 72.96%.

Impacted file tree graph

@@             Coverage Diff             @@
##             master    #625      +/-   ##
===========================================
+ Coverage     51.05%   52.4%   +1.35%     
- Complexity     1524    1647     +123     
===========================================
  Files           127     131       +4     
  Lines          9699   10096     +397     
  Branches       1310    1404      +94     
===========================================
+ Hits           4952    5291     +339     
- Misses         4345    4364      +19     
- Partials        402     441      +39
Impacted Files Coverage Δ Complexity Δ
...rse/src/main/java/com/parse/ParseAddOperation.java 0% <0%> (ø) 0 <0> (ø) ⬇️
.../src/main/java/com/parse/ParseRemoveOperation.java 11.53% <0%> (-1.51%) 3 <0> (ø)
.../src/main/java/com/parse/ParseDeleteOperation.java 54.54% <0%> (-12.13%) 5 <0> (ø)
...c/main/java/com/parse/ParseAddUniqueOperation.java 0% <0%> (ø) 0 <0> (ø) ⬇️
Parse/src/main/java/com/parse/ParseUser.java 77.09% <100%> (+0.88%) 107 <2> (+3) ⬆️
.../main/java/com/parse/ParseObjectParcelEncoder.java 100% <100%> (ø) 5 <5> (?)
...rse/src/main/java/com/parse/ParseSetOperation.java 100% <100%> (+14.28%) 6 <1> (+2) ⬆️
...c/main/java/com/parse/ParseIncrementOperation.java 60.71% <100%> (+24.71%) 7 <1> (+3) ⬆️
...rc/main/java/com/parse/ParseRelationOperation.java 46.26% <30%> (+0.11%) 22 <2> (+3) ⬆️
...e/src/main/java/com/parse/ParseFieldOperation.java 33.33% <41.3%> (-3.88%) 0 <0> (ø)
... and 19 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 53b05e9...e06280a. Read the comment docs.

boolean saving = hasOutstandingOperations();
boolean deleting = isDeleting || isDeletingEventually > 0;
if (saving) {
Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be easy to trigger these issues if people are navigating between activities or you have a lot of rotation-type changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess depends on developer habits, but generally this should be rare (a configuration change while the object is in the middle of a save).

I agree the message is too long, but I could not make it shorter... Maybe if we merge this and update docs, we can replace with About to parcel a ParseObject with ongoing tasks. This might be dangerous. Read <link to docs>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that later. Would rather have a really big warning msg when this thing is first being used rather than a one-liner so we get some feedback about whether it happens a lot or not a ton.

@natario1
Copy link
Contributor Author

natario1 commented Apr 7, 2017

Now that this has been refined, it would be great to get the opinion of one of the original authors, beyond what was said in #122 .. Maybe we can summon @inlined ?

@rogerhu
Copy link
Contributor

rogerhu commented Apr 12, 2017

@wangmengyan95 @grantland any thoughts?

@grantland
Copy link
Contributor

I haven't had time to go into the code in-depth, but the description seems like this has been well thought out. I haven't touched Parse code in about a year now, but the only thing that jumped out to me originally was the single instance use case for LDS which seems to have been covered.

@natario1
Copy link
Contributor Author

Glad to hear that @grantland , thank you for your time.

@rogerhu
Copy link
Contributor

rogerhu commented Apr 18, 2017

rebase =)

@natario1 natario1 force-pushed the parcelable-object branch from 94945f0 to b0d07c2 Compare April 18, 2017 09:59
@natario1
Copy link
Contributor Author

@rogerhu what do you think? I think we can merge this first, then I will just add a couple lines to #619 and #624

@natario1 natario1 force-pushed the parcelable-object branch 2 times, most recently from 7ab3f11 to a16971a Compare April 19, 2017 19:49
@rogerhu
Copy link
Contributor

rogerhu commented Apr 19, 2017

rebase again

@natario1 natario1 force-pushed the parcelable-object branch from a16971a to e06280a Compare April 19, 2017 20:51
@natario1
Copy link
Contributor Author

@rogerhu done now

@rogerhu rogerhu merged commit aaf740a into parse-community:master Apr 19, 2017
@natario1 natario1 deleted the parcelable-object branch May 1, 2017 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants