Skip to content

nottmey/server-driven-ui-poc

Repository files navigation

Proof of Concept: Server-driven UI via a Dart + Protocol Buffers Generator

Interesting Files

Prerequisites

  • fvm (brew tap leoafarias/fvm && brew install fvm)
  • melos (dart pub global activate melos)
  • protoc 3+ (brew install protobuf)
  • protoc dart plugin (dart pub global activate protoc_plugin)
# in project root
fvm install
melos bootstrap

# normally in a project you would add the generator, 
# in this example it's already configured
flutter pub add --dev generator_package --path ../generator_package

# normally in a project you would add the generated protocol package,
# in this example it's already configured
flutter pub add proto_package --path ../proto_package

Usage

# in `flutter_project`
dart run generator_package # creates/updates `proto_package`
# in `proto_package`
protoc --dart_out=grpc:lib/proto --proto_path=proto $(find proto -iname "*.proto")

# in `dart_server` start server
dart run --enable-vm-service

# in `flutter_project` start and see server generated ui
flutter run

# now, feel free to stop app/server or modify+safe `dart_server/lib/example_data.dart` for hot reload (propagating from server to client)
# the new representation should directly appear in the app regardless of startup order, directly after reloading

Limitations

  • We can't use build_runner since we may not know the output in advance, and we seem to not be able to analyze external dependencies directly.
  • We can only define recursive structures like the widget tree inside the same proto file, which forces us to define all widgets in the same file. Also, widget parameters may also contain nested widget and widget parameter types may also form recursive structures. This forces us to put all generated messages (except enums) into the same proto file.
  • Currently, we can't differentiate easily whether a parameter is explicitly set to null or just unset, because protocol buffers only know the set/unset semantic (this would be solvable by another level of indirection for every parameter type: using a oneof with either the value or a bool which tracks the explicitly_null state, which doesn't seem practical)
  • Currently, we only use default values when they are a literal by copying them from their source (when value is optional and unset at runtime). Other default values are hard to re-use because of needed imports or using inaccessible constants.
  • Multiple protocol buffer enums can't use the same names within the same scope, so we have to use pseudo messages as context for their definition. In the following example LEFT and RIGHT can't be reused.
    enum ScrollbarOrientation {
      LEFT = 0;
      RIGHT = 1;
      TOP = 2;
      BOTTOM = 3;
    }
    
    enum AxisDirection {
      UP = 0;
      RIGHT = 1;
      DOWN = 2;
      LEFT = 3;
    }
    So we choose to do it with encapsulation, which prevents the collision:
    message ScrollbarOrientation {
      enum Enum {
        LEFT = 0;
        RIGHT = 1;
        TOP = 2;
        BOTTOM = 3;
      }
    }
    (Another option would be to prefix the enum values, like recommended in the style guide. However, in our context, this may still lead to collisions and can create very long names, e.g. MATERIAL_NAVIGATION_DESTINATION_LABEL_BEHAVIOR_ONLY_SHOW_SELECTED)

TODOs

This proof of concept could be extended by doing the following:

  • add parameterization of library destination
  • add parameterization of which constructors to use
  • add second example of generated package with just a few constructors enabled (fast generation run)
  • rename the first example of generated package to be a full flutter library setup (slow generation run)
  • use fast package in example
  • handle callback function types via a first example: navigation to a new experience
  • create simple routing via experience name (experience reference routes to new page)
  • create simple state management (global key-value, reference-able in experience definition, hydrate-able via backend, update-able via backend)
  • allow callback function types to set values in simple state management
  • generate server driven ui file via generator_package
  • re-write example to use pure backend-based widgets, and use all implemented features

Larger implementation would make this more production ready:

  • create a mechanism to make field numbers stable over time (e.g., with a committed dictionary)
  • enable a recursive combination of experiences for combining (e.g.) a home page (backend delivers all associated experiences in on go)
  • watch mode for generator
  • allow creation of complete AST via protocol
  • handle dynamic and object types in type mapping
  • handle maps in type mapping
  • handle type parameters (e.g., by making them always dynamic)

About

Proof of Concept: Server-driven UI via a Dart + Protocol Buffers Generator

Resources

Stars

Watchers

Forks

Languages