Skip to content

Wrappers

Ethan edited this page Oct 26, 2023 · 7 revisions

Wrappers allow Eval to use instances created outside of the Eval environment. This is necessary when calling from Eval into a native Dart function that returns a value, and can be used when passing an argument into an Eval function.

For example, a Flutter Text bridge class may look something like this (abbreviated for clarity):

class $Text$bridge extends Text with $Bridge {
  @override
  $Value? $bridgeGet(String identifier) {
    switch (identifier) {
      case 'build':
        return $Function((rt, target, args) {
            return $Widget.wrap(super.build(args[0].$value));
        });
    }
    throw UnimplementedError();
  }

  Widget? build(BuildContext context) => 
      $_invoke('build', [$BuildContext.wrap(context)]);
} 

Here we are using wrappers in two places:

  1. When calling build in bridgeGet, a wrapper ($Widget) is used to give the Eval environment access to the resulting Widget object that Text will natively produce if we don't override its build method in an Eval subclass.
  2. When overriding build for native Dart use, we wrap the BuildContext argument with $BuildContext, so that the Eval environment understands the arguments if we do override its build method in an Eval subclass.

A wrapper for Text itself, on the other hand, looks something like this:

class $Text implements Text, $Instance {
  
  $Text.wrap(this.$value);

  @override
  final Text $value;
  
  @override
  $Value? $getProperty(Runtime runtime, String identifier) {
    switch(identifier) {
      case 'build':
        return $Function(
            (rt, target, args) => $Widget.wrap((target.$value as Text).build(args[0].$value)));
    }
  }

  Widget build(BuildContext context) => $value.build(context);

  @override
  int $getRuntimeType(Runtime runtime) => runtime.lookupType(FlutterTypes.text);
}

Bimodal wrappers

The Text wrapper shown above is also a bimodal wrapper since it implements both the $Instance and Text interfaces. Wrappers don't have to be bimodal, and non-bimodal wrappers that implement only $Instance may be significantly easier to write. Bimodal wrappers are only required when specifying a wrapped type as a generic type parameter. Why is this? Well, let's take a look at the following class:

class State<T extends Widget> {
  State(this.widget);
  final T widget;
}

and a naive bridge class for this widget (abridged for clarity):

class $State$bridge<T extends Widget> extends State<T> with $Bridge<State<T>> {
  static const $type = ...;
  static const $declaration = ...;

  @override
  $Value? $bridgeGet(String identifier) {
    switch (identifier) {
      case 'widget':
        return $Widget.wrap(widget); // << What do we do here?
    }
  }

  @override
  T get widget => $_get('widget');
}

You can see this class has a problem. When we want to retrieve the widget parameter as a $Value, we don't know what its exact type will be, only that it must extend Widget, so that's the most specific type we can wrap it in - not very useful.

With the use of bimodal wrappers, we can amend the function to this:

...
  @override
  $Value? $bridgeGet(String identifier) {
    switch (identifier) {
      case 'widget':
        if (widget is $Instance) {
          return widget as $Instance;
        }
        return $Widget.wrap(widget);
    }
  }
...

This works only if the class we specify as the T type parameter implements both its original type (allowing it to be used as a Widget and stored in the widget field) and $Instance, allowing it to be returned from $bridgeGet.

Clone this wiki locally