cashapp / redwood

Multiplatform reactive UI for Android, iOS, and web using Kotlin and Jetpack Compose
https://cashapp.github.io/redwood/0.x/docs/
Apache License 2.0
1.64k stars 72 forks source link

Revamp the widget-side codegen to better facilitate the protocol and testing #1041

Open JakeWharton opened 1 year ago

JakeWharton commented 1 year ago

This is going to be somewhat of a brain dump of a bunch of things bouncing around in my head. There are or were individual issues for some of the points in here, but this will basically track the whole revamp which will need many PRs to complete.

High-level goals is to make the protocol usage for Treehouse and the tester have better UX as well as correctness. The need for Treehouse to have a WidgetSystem type and the current design of the tester API (even after #806 was fixed) are failures of good codegen design.

I think the new shape is going to be

public object EmojiSearchWidgets {
  fun <W : Any> provider(
    EmojiSearch: EmojiSearchWidgetFactory<W>,
    RedwoodLayout: RedwoodLayoutWidgetFactory<W>,
    RedwoodTreehouseLazyLayout: RedwoodTreehouseLazyLayoutWidgetFactory<W>,
  ) : EmojiSearchWidgetFactoryProvider<W> {
    return EmojiSearchWidgetFactories(EmojiSearch, RedwoodLayout, RedwoodTreehouseLazyLayout)
  }
}

private class EmojiSearchWidgetFactories<W : Any>(
  public override val EmojiSearch: EmojiSearchWidgetFactory<W>,
  public override val RedwoodLayout: RedwoodLayoutWidgetFactory<W>,
  public override val RedwoodTreehouseLazyLayout: RedwoodTreehouseLazyLayoutWidgetFactory<W>,
) : EmojiSearchWidgetFactoryProvider<W>

which is similar to what we have today, except we have a object off which to hang other niceties.

Such as: testing!

suspend fun EmojiSearchWidgets.test(body: TestScope.() -> Unit) {
  val provider = provider(
    EmojiSearch = MutableEmojiSearchWidgetFactory(),
    RedwoodLayout = MutableRedwoodLayoutWidgetFactory(),
    RedwoodTreehouseLazyLayout = MutableRedwoodTreehouseLazyLayoutWidgetFactory(),
  )

  blah blah..
}

This should also come with the continued revamp to the tester API. We don't need the MutableWidget type as we can use Widget<WidgetValue> maybe we rename that to Widget<Snapshot> or something.

And probably the protocol becomes an extension on EmojiSearchWidgetFactoryProvider like

fun EmojiSearchWidgetFactoryProvider<*>.protcolSomethingSomething(
  json: Json = Json.Default,
  mismatchHandler: ProtocolMismatchHandler = ...
) {
  ...
}

Then we basically do the ProtocolBridge / ProtocolState types which obviates WidgetSystem and should allow setup for Treehouse to be very lightweight and provides symmetry with the Compose-side of the protocol.

Now just have to figure out the order...

JakeWharton commented 6 months ago

Okay I just spiked this again for unrelated reasons and came up with nearly the same design.

First, the widget codegen gets the foundational object named directly based on the schema. It is on this where we will hang functions both directly and through extensions.

public object EmojiSearch {
  fun <W : Any> widgetSystem(
    EmojiSearch: EmojiSearchWidgetFactory<W>,
    RedwoodLayout: RedwoodLayoutWidgetFactory<W>,
    RedwoodLazyLayout: RedwoodLazyLayoutWidgetFactory<W>,
  ) : WidgetSystem<W> {
    return EmojiSearchWidgetSystem(EmojiSearch, RedwoodLayout, RedwoodLazyLayout)
  }
}

private class EmojiSearchWidgetSystem(..) {..}

In the time since this issue we have some better types and better names. The grouping of widget factories creates a widget system.

The tester:

public suspend fun <R> EmojiSearch.test(
  onBackPressedDispatcher: OnBackPressedDispatcher = NoOpOnBackPressedDispatcher,
  savedState: TestSavedState? = null,
  uiConfiguration: UiConfiguration = UiConfiguration(),
  body: suspend TestRedwoodComposition<List<WidgetValue>>.() -> R,
) {..}

Now for the fun stuff, the protocol. Guest-side:

public fun EmojiSearch.guestProtocol(): RedwoodGuestProtocol {
  return EmojiSearchGuestProtocol
}

private object EmojiSearchGuestProtocol {..}

The guest protocol allows the unification of the ProtocolBridge and ProtocolState types whose separation was solely a generated code vs. user code split.

Host-side:

public fun EmojiSearch.hostProtocol(): RedwoodHostProtocol {
  return EmojiSearchHostProtocol
}

private object EmojiSearchHostProtocol {..}

The RedwoodHostProtocol type completely obviates Treehouse's WidgetSystem. It also removes poorly-named types from the public API.

Both the RedwoodGuestProtocol and RedwoodHostProtocol types will be factories for a GuestProtocolBridge and HostProtocolBridge, respectively, but user code shouldn't interact with them directly.