Skip to content

Wrappers

Ethan edited this page Feb 18, 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(String id, Text value) : $value = runtimeOverride(id) as Text? ?? value;

  $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);
}

Hot wrappers

Notice the first constructor with the runtimeOverride line? That's because the above class is also a hot wrapper. A hot wrapper is a class that makes it easy for Eval to dynamically substitute a component with an Eval equivalent at runtime, such as when using code push. For example, let's say you define your app like so:

class MyApp extends StatelessWidget {
  MyApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // Use a hot wrapper for the app bar
        appBar: $AppBar('#myapp-build-appbar-1', value: () => AppBar(const Text('My App!'))),
         
        // Use a hot wrapper for the app content
        body: $Column('#myapp-build-column-1', value: () => Column(children: const<Widget>[
           _buildMainScreen(),
           _buildExtraContent()
        ]));
      );
    );
  }
}

Here we are using standard Dart code for all of the content of the app up until the Column and AppBar, which we replace with hot wrapper $Column and $AppBar instances and specify globally unique IDs to identify them (eg '#myapp-build-column-1'). Now, if we want to push some new code to our app, we will be able to easily override any code inside of the Column or AppBar constructor without affecting anything else or each other. For example, if we simply want to change the title in the AppBar, we can push a piece of code that will induce dart_eval to use a bridge call for the content of the app bar, but the Column will remain native Dart code. This is highly beneficial to performance, since most of the app's code can continue to run in native, compiled Dart while only the piecies you've changed will be run in the slower dart_eval VM.

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