redwood icon indicating copy to clipboard operation
redwood copied to clipboard

Ability to decorate Widgets with functionality

Open chrisbanes opened this issue 1 year ago • 10 comments

There's a few use cases where we need to 'decorate' widgets with functionality, specific to the host platform. The API I keep coming back to is something similar to Compose UI's Modifiers, but with implementations bound for each platform.

Some examples:

Pull to Refresh

Due to how the host platforms differ, we've had to bake 'pull to refresh' into LazyColumn and friends (#1082). That's not great as we're coupling two things that really shouldn't be coupled. The API is also gross. Ideally we'd have some way to decorate a LazyColumn instead:

LazyColumn(
    modifier = Modifier.pullToRefresh(refreshing, onRefresh = { ... })
) {
    items(foo)
}

Animations

This one is more hazy, as it's mostly just stuff in the back of my head. We need a way to run animations on widgets, and it's looking likely that our API structure will be similar to React Native's, with a specific 'animated widget'. Something like:

var animations by remember { mutableStateListOf() }

AnimatedWidget(
    animations = animations,
    onAnimatedEnd = { animations.remove(it) }
) {
    Text(
        text = "Foo",
        onClick = { animations += Animation(transformX = -100f) }
    )
}

Now we don't necessarily want AnimatedWidget to produce a host-side UI node. It will likely just be there to decorate the child.

chrisbanes avatar May 16 '23 14:05 chrisbanes

Can the animated one be a modifier, too? A modifier designation (either another annotation or an interface implemented on the schema type) that changes it to participate in the view tree and probably in the factory with a callback to attach to the native view would be something that would be pretty straightforward.

I don't think we should do both forms if we can avoid it. Nodes in the Compose tree which don't show up in the native UI toolkit tree are possible, but likely more difficult.

JakeWharton avatar May 18 '23 14:05 JakeWharton

Can the animated one be a modifier, too?

Yeah, technically there's nothing wrong here, but the API may look a little weird. I'll sketch out some pseudo-code and see how it looks.

chrisbanes avatar May 18 '23 14:05 chrisbanes

Other examples that keep coming up:

  • Stable ID
  • Accessibility (description, focus info)
  • Background color

In order to support things like the pull-to-refresh case where on iOS it's a thing that gets set and on Android it's a wrapper widget I think this would behave a bit more like an interceptor. We create the native UI toolkit node and then each of these interceptor modifiers get a chance to either mutate and pass on the same reference or to wrap the view. The widget binding still binds to the original instance, but the view tree gets the final potentially-wrapped instance.

JakeWharton avatar Sep 08 '23 00:09 JakeWharton

Writing some things down that are in my brain so they're not lost as the weekend looms closer:

If we intercept native UI nodes with these modifiers (for pull-to-request wrapping on Android, for example) there are two cases:

First, we might be on initial insertion in which case we can do the operation and then need to insert the final UI node not the wrapped UI node. The WidgetNode should probably track the wrapped UI node for later use during insertion. Additionally, this will allow doing instance comparison on future changes to determine if we need to replace the native node in its parent.

Second, when we do need to remove the old node and insert a new node based solely on a new modifier being delivered this will not involve an applier change. As such, WidgetNode has to be able to do this based on modifier update. We need a reference to the enclosing children (which we have) and the current index of the target widget within its parent (which we do not have). So WidgetNode will probably need to change to track this index and update it as applier changes happen. Annoying, but not too expensive.

This is only focused on direct Redwood. Protocol Redwood is a different story...

JakeWharton avatar Oct 12 '23 16:10 JakeWharton

As far as I can tell, for protocol Redwood this will all be done in ProtocolNode which mostly has the same behavior as WidgetNode and also retains a parent reference. It will need the same index tracking.

JakeWharton avatar Oct 12 '23 17:10 JakeWharton

@JakeWharton Would you mind providing an update on this issue when you get a chance? The inability to set background colors on arbitrary widgets/views has come up again as an issue for the Activity team.

dnagler avatar Nov 28 '23 21:11 dnagler

I'm out until February. It was also not deemed something for stability sprint.

JakeWharton avatar Nov 28 '23 22:11 JakeWharton

Thanks for the context (and for the quick reply while on leave)! It looks like you made some progress on this since your comments on 10/12--would you mind providing an estimate for how much work is left to get this to an MVP/first release?

dnagler avatar Nov 28 '23 22:11 dnagler

For things which only mutate an existing widget (e.g., background) it should be very quick. I expect within a week, barring any other distractions.

JakeWharton avatar Nov 28 '23 22:11 JakeWharton

Mutating unscoped modifiers have landed. The test-app has been updated with a background color modifier that works on any node on any platform.

Future work is allowing the generated callback for an unscoped modifier to have the option of wrapping the native node.

JakeWharton avatar Mar 28 '24 20:03 JakeWharton