Skip to content
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

Proposal: Consolidate Gutenberg initialProps handling #12444

Conversation

mchowning
Copy link
Contributor

@mchowning mchowning commented Jul 14, 2020

The purpose of this PR is to simplify the handling of passing initial props to the Gutenberg editor. Because this is a bit of an architecture change, I think it would be helpful to get feedback from more than one reviewer if possible. 🙇

Related PRs:

Current State

Currently, the process when you need to pass a new initialProp is you figure out where you have access to that prop, and then you manually pass it up through the relevant Activity and Fragments until you get it to the WPAndroidGlueCode where you insert the value into the initial props (i.e., EditPostActivityGutenbergEditorFragmentGutenbergContainerFragmentWPAndroidGlueCode). As a result, (1) adding a new initial prop means we have to pass that prop through all of those different classes, which results in a lot of boilerplate (see specifics below⤵); and (2) in order to track down whether a specific object is being passed for inclusion in the initial props we have to trace it's path through all those classes until we get to WPAndroidGlueCode where it is inserted into the initial props.

Steps Involved in Adding an Initial Prop Under Current State

To make this more concrete, adding an object for inclusion as a new initial prop under the current approach requires the following:

  1. Frequently, the prop is available in EditPostActivity so we add a new parameter to the GutenbergEditorFragment::newInstance so that we can pass the value to the GutenbergEditorFragment. This method currently has 17 parameters, so this would increase it to 18! 🙃

  2. Inside GutenbergEditorFragment::newInstance we have to create a String key for the new parameter and store that parameter in the fragment's argument bundle.

  3. In GutenbergEditorFragment::onCreate we pull the new object out of the fragment's argument bundle, create a new parameter in theGutenbergContainerFragment::newInstance method for that object and pass it.

  4. Then we create another String key for the new parameter in GutengergContainerFragment and put the parameter into that Fragment's argument bundle.

  5. Now we pull the object out of GutenbergContainerFragment's argument bundle, add a new parameter to WPAndroidGlueCode for that object, and pass it to WPAndroidGlueCode::onCreateView, which currently accepts 17 parameters.

  6. In WPAndroidGlueCode::onCreateView, we finally take the object and insert into the initialProps object that is passed to React Native.

Proposed Change

In this PR, I have created a GutenbergProps data class in `react-native-gutenberg-bridge. This class's purpose is to (1) define the information that is required for Gutenberg's initial props, and (2) construct that initial props object.

Using this class by itself would not significantly simplify things on the WPAndroid side, however, because we get the data needed for the initial props from different locations (sometimes EditPostActivity, sometimes GutenbergEditorFragment, and sometimes GutenbergContainerFragment). That means that the only time we have all the data we need to construct this object is at the end of that chain. It would be much better if we could instead construct this single object in EditPostActivity and pass it down through the various fragments, adding the needed information along the way (instead of passing all the information separately, as we do now).

The editor module defines a GutenbergPropsBuilder object to facilitate exactly that. I first construct an instance of GutenbergPropsBuilder in EditPostActivity and then pass that single object through all the steps discussed above, adding information along the way (for example). Then we just call GutenbergPropsBuilder.build() to create a fully-realized GutenbergProps object that we can pass to WPAndroidGlueCode.

This required adding 1 new method parameter to pass the new GutenbergPropsBuilder instance to GutenbergEditorFragment::newInstance, GutenbergContainerFragment::newInstance and WPAndroidGlueCode::onCreateView. This enables us to remove a total of 24 parameters from those same method calls though 😃 because those parameters are now stored in the GutenbergPropsBuilder instance that is being passed.

Steps Involved in Adding an Initial Prop Under Proposed Change

Have updated these steps to reflect improvements made after the discussion in this PR. Old text was left, but struckthrough. Discussion in the comments on this PR will provide additional context.

  1. Add the new initial prop to the GutenbergProps object, and take the new information as a construction parameter.

  2. This will create a compile error in the GutenbergPropsBuilder's construction of the GutenbergProps object, so we need to fix that, which will probably involve enabling the GutenbergPropsBuilder to accept additional information needed to construct the GutenbergProps instance (either via a construction parameter or through a setter method) involve adding the additional information as a parameter to either GutenbergPropsBuilder's constructor or it's build(...) method. The decision of which method should be changed should depend on where it is easier to provide the required information.

  3. At the point where the new information is available (regardless of whether that is in EditPostActivity, GutenbergEditorFragment, or the GutenbergContainerFragment) pass the information to the GutenbergPropsBuilder instance. That will create a new compile error at the point where the constructor or build method is called, so we will need to pass the required information in that method.

Possible Issues With Proposed Change

Adding or Removing a Prop Requires Updating the WPAndroid and Gutenberg repos

One possible objection to this change is that the GutenbergProps object is defined in the @wordpress/react-native-bridge module, which is in the Gutenberg repo, which means that when you want to, for example, add a new initial prop, you have to (1) update the GutenbergProps object in Gutenberg, (2) update the gb-mobile submodule ref, and (3) update WPAndroid to provide the newly required prop.

It seems easier if we just define the GutenbergProps object inside the editor module in WPAndroid and have it construct and pass an initial props Bundle to Gutenberg's WPAndroidGlueCode class. That way we would only have to update code in the WPANdroid repo to add or remove initial props on the native side (i.e., we wouldn't have to change any native code in Gutenberg). Although this would definitely be a nice benefit, I am inclined against it for two reasons:

  1. Doing it this way means that the WPAndroid repo is defining what initial props need to be passed to the WPAndroidGlueCode. I think that it makes far more sense for WPAndroidGlueCode to have the knowledge about what initial props React Native needs and to for it to "tell" WPAndroid what is required. For example, if another app ended up using gutenberg-mobile, I don't think they should have to look at either javascript or WPAndroid to see what props are expected from native; the Android code in react-native-gutenberg-bridge should have that information in the type system (in this PR the GutenbergProps class serves that purpose).

  2. I think that it will actually be pretty rare that we will ever add or remove an initial prop without also needing to make JS change to Gutenberg, so I doubt that we would be able to take advantage of "only" having to update WPAndroid very often in practice.

We Have Separate GutenbergProps and GutenbergPropsBuilder classes

A similar change we could make would be to not make the GutenbergProps object immutable, which would mean we wouldn't need the (mutable) GutenbergPropsBuilder object to accumulate the relevant information as it is passed up from EditPostActivity. Similar to what I said above⤴️, I like having the immutable GutenbergProps object as a clear representation of what Gutenberg needs for its initial props (if you can construct the GutenergProps object, then you know you have all the information that React Native needs). The fact that within WPAndroid all the information needed for the initial props is not conveniently available from a single location is an implementation detail of WPAndroid and (imo) the react-native-gutenberg-bridge module should not "know" about that implementation detail or attempt to work around it.

Feedback Please!

Although I feel pretty good about what I'm proposing here, I still consider this a proposal, so please share any feedback, suggestions, or better alternatives you might have. 🙇

To Test

Smoke test the Gutenberg editor to ensure that the initial props are getting properly set and it is functioning normally. This PR should not result in any changes in how the app behaves.

PR submission checklist:

  • I have considered adding unit tests where possible.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@mchowning mchowning added this to the 15.5 milestone Jul 14, 2020
@mchowning mchowning self-assigned this Jul 14, 2020
@peril-wordpress-mobile
Copy link

peril-wordpress-mobile bot commented Jul 14, 2020

You can trigger optional UI/connected tests for these changes by visiting CircleCI here.

@peril-wordpress-mobile
Copy link

peril-wordpress-mobile bot commented Jul 14, 2020

You can test the changes on this Pull Request by downloading the APK here.

@mkevins
Copy link
Contributor

mkevins commented Jul 15, 2020

Thanks @mchowning for the detailed proposal! I've had similar thoughts about muxing to reduce the arity of these very interfaces 😄 , and I think you've outlined the essential requirements very well in your description.

adding a new initial prop means we have to pass that prop through all of those different classes, which results in a lot of boilerplate

I welcome a reduction here 😃, and I think the greatest value from this is that it can reduce the cognitive load while reasoning about the collaborating classes:

in order to track down whether a specific object is being passed for inclusion in the initial props we have to trace it's path through all those classes until we get to WPAndroidGlueCode where it is inserted into the initial props

i.e. making these interfaces more readable will also improve the experience of maintaining other nearby areas of the code base.

I especially agree with these points:

For example, if another app ended up using gutenberg-mobile, I don't think they should have to look at either javascript or WPAndroid to see what props are expected;

and

it will actually be pretty rare that we will ever add or remove an initial prop without also needing to make JS change to Gutenberg, so I doubt that we would be able to take advantage of "only" having to update WPAndroid very often in practice

I wonder whether we could reduce the boiler plate even more, reducing repetition in the builder class.. though I'm not sure that would be possible without losing some compile-time guarantees 🤔

@mchowning
Copy link
Contributor Author

mchowning commented Jul 16, 2020

Thanks for giving this some thought @mkevins !

I wonder whether we could reduce the boiler plate even more, reducing repetition in the builder class. though I'm not sure that would be possible without losing some compile-time guarantees 🤔

We actually may not lose as many compile-time guarantees as you suspect because even using the builder object, I'm losing some compile-time guarantees because I'm using requireNotNull when constructing the final GutenbergProps instance. I think that's still an improvement because the failure will be fast and obvious instead of getting some weird behavior in Gutenberg because an expected initial prop is missing.

We could drop the GutenbergPropsBuilder class; that would just mean that we would have to make the GutenbergProps object itself mutable so that it could be easily updated from Java without us passing all the required objects around like we have until now. I think there are pluses to both approaches (two classes vs mutable GutenbergProps). My main reason for making it immutable is that from the react-native-gutenberg-bridge perspective there is no reason GutenbergProps needs to be mutable; it is only because of how WPAndroid is implemented that it is helpful to make it mutable, so it made sense to me to create a separate, mutable, builder version of GutenbergProps within WPAndroid.

A second reason I forgot to mention for why I used the GutenbergPropsBuilder class is that currently react-native-gutenberg-bridge is an implementation dependency of the WordPressEditor module. Because EditPostActiviity is in the main WordPress module (which depends on the WordPressEditor module), it does not have access to classes defined in WordPressEditor's implementation dependencies--such as react-native-gutenberg-bridge. So EditPostActivity actually cannot construct a GutenbergProps object under the current project structure. The GutenbergPropsBuilder class, however, is defined in the WordPressEditor module, so EditPostActivity can construct it.

We could switch react-native-gutenberg-bridge to being an api dependency, which would expose it's internals (including GutenbergProps) all the way down to the root WordPress module, but I was trying to avoid doing that unless it was really necessary. I'm guessing this wasn't done initially out of a desire to shield the rest of WPAndroid from the React Native dependencies as much as possible, but @hypest probably has a better idea about any motivations behind that decision. If we are fine turning react-native-gutenberg-bridge into an api dependency of the WordPressEditor module, I could push another branch to show what this would look like if GutenbergProps were mutable if you would like.

@mkevins
Copy link
Contributor

mkevins commented Jul 16, 2020

Thanks for further elaborating.

I'm losing some compile-time guarantees because I'm using requireNotNull when constructing the final GutenbergProps instance

I wonder about this.. are we actually losing any guarantees? I still think (in the current state of your proposal) we get all the same guarantees we had before, and this runtime non-null assertion is "extra". I mean, at least, in this proposal, I guess a compilation failure will still clearly signal the bridge incompatibility when a member / parameter / prop is added in one place but not the other. Is there a scenario where we currently get compile errors, in which the proposed implementation will instead have runtime errors?

Regarding the mutable / immutable, I like the current approach (immutable), but don't have strong feelings either way.

@mchowning
Copy link
Contributor Author

Is there a scenario where we currently get compile errors, in which the proposed implementation will instead have runtime errors?

I do think we lose a small bit of compile-time safety around the requireNotNull(someRequredValue) variables. Under the current approach (before this PR), because each value required for the initial props is a separate parameter in the WPAndroidGlueCode::onCreateView function, we will fail to compile if we don't pass that value in. With the GutenbergPropsBuilder class, if we fail to "set" one of the values that are not initialized in the constructor, the app will compile and crash at runtime when it hits the requireNotNull check during the construction of the GutenbergProps instance.

Personally, I think this tradeoff is more than worth it with what we gain in reduced boilerplate and the clarity the GutenbergProps class provides around what is required for the initial props. In addition, the failure to set the value is a really clear coding mistake that will immediately start crashing with an obvious exception. So while it is not quite as good as a compile-time check, it's pretty close imo.

@mkevins
Copy link
Contributor

mkevins commented Jul 17, 2020

if we fail to "set" one of the values that are not initialized in the constructor, the app will compile and crash at runtime when it hits the requireNotNull check during the construction of the GutenbergProps instance

Ah, right. There is no signal to add the necessary mutations prior to invoking build(). I wonder if we could go "full immutable", and add the subsequently required values to the signature of a "build-like" method, which would make that a compile-time check again. E.g. instead of having translations, isDarkMode, htmlModeEnabled mutated, they could be part of the interface to the method that returns the final GutenbergProps value. I'm still thinking on what to name such a method, since it would deviate from a typical builder pattern, but I guess it could be something descriptive / semantic enough to illustrate the need for the deferred "finalization" of the GutenbergProps.

@mchowning
Copy link
Contributor Author

mchowning commented Jul 17, 2020

instead of having translations, isDarkMode, htmlModeEnabled mutated, they could be part of the interface to the method that returns the final GutenbergProps value.

Good point, I was able to move translations and isDarkMode out of the GutenbergContainerFragment and instead made them static methods that could be called from within GutenbergPropsBuilder. I then just passed what was needed for those method calls into the build method. GutenbergPropsBuilder now has much better type safety (and it's shorter and easier to understand imo now too). 🙂 Thanks for the suggestion @mkevins . 👍

Copy link
Contributor

@cameronvoell cameronvoell left a comment

Choose a reason for hiding this comment

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

These changes are a huge improvement, well done! The thread with @mkevins was enlightening as well. I agree with the conclusions that it is worth it to have the immutable GutenbergProps defined in the react-native-bridge and a separate GutenbergPropsBuilder class in WPAndroid. Improvements here made me think about how we may write up steps for integrating GB-Mobile in third party apps in the future - changes here will definitely be helpful for that future work.

I left a comment on the Gutenberg PR about updating the demo app, but otherwise I think these changes are in pretty good shape. Sanity tests running WPAndroid all looked good.

@cameronvoell cameronvoell self-requested a review July 20, 2020 20:50
Copy link
Contributor

@cameronvoell cameronvoell left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for the improvement here!

@mchowning mchowning changed the base branch from develop to gutenberg/after_release_1.33.0 July 20, 2020 20:56
@mchowning mchowning force-pushed the put_mentions_behind_feature_flag_updated branch from c57a73e to 8467702 Compare July 20, 2020 21:14
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.

3 participants