Dart Snake Project Part 5: Dart Snake Jaspr

For the final segment in the The Dart Snake Project we will create a web implementation using the Jaspr Framework . Jaspr is a web app framework that works for building both server side rendered and client side rendered code. This will allow us to write the app with some of the niceties we had with Flutter while using actual HTML/CSS constructs not the “rendered in a browser” style web technology that Flutter Web provides. The code for this project, as well as screenshots, and other details can be found at the dart-snake GitLab project page . This is open sourced under an Apache 2.0 License.

Screenshot of the Jaspr Web version on Firefox of the snake game

Introduction to Jaspr

Jaspr is a web app framework that works for building both server side rendered and client side rendered code. It has hooks to make it easy to add in standard REST frameworks as well, but it is not about trying to perform those things. “Why use Jaspr when there is Flutter Web?” Well, it’s a bit complicated. Flutter Web was designed to make it as easy as possible to get a Flutter app deployed to the web in the same way one does with mobile or desktops. To do that it honors the same concept it does with the other deployment targets and never uses native widgets. On desktop and mobile everything is rendered in Skia , the same low level graphics engine that Chrome uses to draw your websites. The goal is as close as possible pixel-perfect representation of the controls across all the target plaforms. It therefore makes sense that Flutter would not use a bunch of HTML Document Object Model (DOM) controls like a “regular” website would to render things. It instead uses either a bunch of Canvas Elements, in HTML mode, CanvasKit, or in an upcoming release WASM . For those that wanted to use Dart to write their websites with traditional frameworks their options were to stick with doing it without a framework, like we did in the Dart Web version of Snake, maybe use one of the pseudo-ports of frameworks like Angular or Vue, or tie the code into JavaScript frameworks.

Jaspr on the other hand is about trying to give a Flutter-like framework that uses traditional DOM elements. There is a convenient Jaspr vs. Flutter Web high level comparison document for those looking to transition between the two, which I found very helpful. There is even a JasprPad environment complete with tutorials and samples to get started in learning how to code with it. At a high level we will still be creating Stateless or Stateful “widgets” that will be tied together in a declarative UI design tied into the state management systems. However the way the build methods work will be slightly different and we will be working with DOM Components, not Flutter Widgets. Ideally what this will allow is for us to create a more Flutter-like implementation of our Snake program.

Getting started with Jaspr is as easy as installing the CLI tools:

dart pub global activate jaspr_cli

…and then creating your project and running building it with said tools:

dart pub global activate jaspr_cli
jaspr create -t basic my_web_app
cd my_web_app
jaspr serve

Jaspr has several types of project templates from server-side only for completely static web sites to client side only for dynamic ones. Because our app is dynamic with no server side to speak of the client side only project is the one we will be starting with.

Presentation Layer Design

.
├── lib
├── pubspec.yaml
└── web
    ├── app.dart
    ├── game_board_widget.dart
    ├── index.html
    ├── jaspr_gameboard_extensions.dart
    ├── main.dart
    ├── riverpod_di.dart
    ├── styles.css
    └── window_keyboard_listener.dart

Our design borrows heavily from the Flutter Dart Snake Program . Just like with the other designs “all” we have to do is draw the game board state that comes from the same business layer libraries as the rest of the implementations. For the Flutter version we used a GridView object. For the Dart Web version we used the CSS Grid Layout construct. The Jaspr implementation will in effect be using the CSS Grid Layout, since we are working with DOM components, but they way it is wired together will be very similar to the way we put together the GridView in Flutter. Keyboard listeners are built into the browser, the same as with the Dart Web view. However I’d like to have an overaching class like the KeyboardListener I had in Flutter to wire it together. There is no control like that in the Jaspr framework, so we will be writing a quick WindowKeyboardListener class around that. We will be leveraging the fact that our GameBoardTile objects extended the StateNotifier class in the Riverpod state management system to keep track of changes to game board state and let the Jaspr framework know when it is time to redraw rather than directly wiring in event listeners like we did in the ncurses and Dart Web versions.

The Main App Quick Walk Through for Jaspr/Flutter Newbies

Jaspr apps bootstrap in a very similar way as the Flutter apps do, with a simple main method invoking a runApp method with a root app component passed in, see the main.dart file:

void main() {
  runApp(ProviderScope(child: App()));
}

Just like with the Flutter version we are wrapping our app in a Riverpod ProviderScope which will allow us to use the Riverpod state management system throughout. The App component root draws the base layer to the body of the DOM which was established by a stub index.html file:

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8">
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta content="width=device-width, initial-scale=1.0" name="viewport">
    <meta content="https://github.com/dart-lang/sdk" name="scaffolded-by">
    <title>Snake in Jaspr</title>
    <link href="styles.css" rel="stylesheet">
    <script defer src="main.dart.js"></script>
</head>
<body></body>
</html>

When we run our program our Dart program will be compiled through the dart2js compiler into the main.dart.js file referenced in the header. This will allow the page to be bootstrapped with all our code. With that lets look at our application component.

The Main App Component

The App itself is a simple StatefulComponent that will host our game, the game board widgets, and the score widget. The full code can be found in the app.dart file. We will start with the build method:

  @override
  Iterable<Component> build(BuildContext context) sync* {
    yield WindowKeyboardListener(
      keyDownEventHandler: keyboardhandler,
    );
    yield GameBoardWidget();
    yield DomComponent(
      id: 'score',
      tag: 'div',
      child: Text(
          game.gameOver ? 'Game Over, final score: $score' : 'Score: $score'),
    );
  }

We essentially have three widgets that we are returned from our build method: the WindowKeyboardListener to globally listen for key events, the GameBoardWidget class that visually represents our game board, and a DomComponent where we will store our score text. Jaspr uses Dart Streams for its return system since that is a more organic way of handling objects with respect to the DOM than the Flutter Widget paradigm but the premise is essentially the same. As this is the root of the application all of these will be added to the body of the DOM at the top level with children DOM objects being created underneath. We can see this in the Firefox Inspector for this page:

Screenshot of the Firefox Inspector showing the HTML layout described in the below text.

Our Window-level event component is attached to the top represented by the “event” highlight. Our two divs are represented and now populated. The construction of the Grid we will get to in a moment. The rest of the app class is setup and keyhandler code which looks very much like the previous Flutter version:

  Game setupGame(GameBoard gameBoard, int width, int height) {
    final game = Game(
        width: width,
        height: height,
        beforeStep: (game) {
          try {
            gameBoard.beforeStepUpdate(game);
          } on Exception catch (e, s) {
            print(e.toString());
            print(s.toString());
          }
        },
        afterStep: (game) {
          try {
            gameBoard.afterStepUpdate(game);
            if (game.score != score) {
              setState(() {
                score = game.score;
              });
            }
          } on Exception catch (e, s) {
            print(e.toString());
            print(s.toString());
          }
        },
        onGameOver: (game) {
          setState(() {});
        },
        onStopRunning: (game) {});

    return game;
  }

Just like in the Flutter version our beforeStep is nothing but updating the business layer gameBoard call and afterStep is just the game board call and calling a state change notification updating the latest score. This method isn’t just like the Flutter implementation. It is identical to it.

  void keyboardhandler(KeyboardEvent event) {
    switch (event.keyCode) {
      case KeyCode.UP:
        game.snake.changeHeading(Direction.up);
        break;
      case KeyCode.DOWN:
        game.snake.changeHeading(Direction.down);
        break;
      case KeyCode.LEFT:
        game.snake.changeHeading(Direction.left);
        break;
      case KeyCode.RIGHT:
        game.snake.changeHeading(Direction.right);
        break;
      default:
        print('Unknown key: ${event.key}');
    }
  }

The keyboard event handler is exactly the same as with Flutter but with the KeyboardEvent being of the HTML variety versus the Flutter variety.

  @override
  void initState() {
    super.initState();
    game = setupGame(gameBoard, width, height);
    game.start();
  }

  @override
  void dispose() {
    game.stop();
    super.dispose();
  }

The initState call that setups up the game and the dispose are also identical to the Flutter version. In terms of the game board widgets themselves, in the game_board_widget.dart file we again are hewing very close to the Flutter structure.

Game Board Widgets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class GameBoardWidget extends StatelessComponent {
  static final gridStyles = Styles.combine([
    Styles.grid(),
    Styles.box(width: Unit.pixels(400), height: Unit.pixels(400)),
    Styles.raw({
      'margin-left': 'auto',
      'margin-right': 'auto',
    }),
  ]);

  const GameBoardWidget({super.key});

  @override
  Iterable<Component> build(BuildContext context) sync* {
    const tileSize = 10.0;
    yield DomComponent(
      tag: 'div',
      styles: gridStyles,
      children: gameBoard.tiles
          .map((t) => TileWidget(
                tileSize: tileSize,
                position: t.position,
              ))
          .toList(),
    );
  }
}

The GameBoardWidget class is doing essentially the same thing as in Flutter except instead of populating a GridView object we are adding children to the DOM of this widget. The styles for the game board are being created programmatically in lines 2-9, which will create the same sort of CSS you will find in the Dart Web CSS file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

class TileWidget extends StatelessComponent {
  final double tileSize;
  final BoardPosition position;

  const TileWidget({super.key, required this.tileSize, required this.position});

  @override
  Iterable<Component> build(BuildContext context) sync* {
    final state = context.watch(gameTileProvider(position));
    yield DomComponent(
        tag: 'div',
        styles: Styles.combine([
          position.styles,
          Styles.background(color: state.color),
          Styles.box(width: Unit.pixels(10), height: Unit.pixels(10)),
        ]));
  }
}

The TileWidget is looking very similar to the Flutter version as well and doing the exact same thing in terms of wiring up to listen to state changes for its respect GridBoardTile by watching the result of the gameTileProvider for its given position. This will cause the Jaspr framework to be aware of the state change and force the rebuild of this component with the new state.

Just as with Flutter you want to approach this with a declarative UI perspective where the build method never has knowledge of its previous state and is always drawing the state of the objects that it is handed. The big difference between the Jaspr implementation and Flutter is the return of a DomComponent rather than a Container wiget. We are using several of Jaspr’s styling objects and methods to programmatically create the styles as well. How is it that the BoardPosition object from the business logic layer knows how to generate a Jaspyr style and how does the TileState from that layer know how to generate a color? For this we are using Dart’s extension methods again, which you can find in the jaspr_gameboard_extensions.dart file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension TileStateExtensions on TileState {
  Color get color {
    switch (this) {
      case TileState.border:
        return Colors.black;
      case TileState.background:
        return Colors.white;
      case TileState.food:
        return Colors.gold;
      case TileState.head:
        return Colors.darkOliveGreen;
      case TileState.body:
        return Colors.green;
    }
  }
}

extension BoardPositionExtensions on BoardPosition {
  // use Styles.gridItem once fix is published to pub.dev
  Styles get stylesGI => Styles.gridItem(
          placement: GridPlacement(
        columnStart: LinePlacement(x + 1),
        columnEnd: LinePlacement(x + 1),
        rowStart: LinePlacement(y + 1),
        rowEnd: LinePlacement(y + 1),
      ));

  Styles get styles => Styles.raw({
        'grid-area': '${y + 1} / ${x + 1} / ${y + 1} / ${x + 1}',
      });
}

The TileStateExtensions are identical to the ones in Dart Web since we are using the same Color object types. Our BoardPositionExtensions have two methods: styles, which is whate we are using, and stylesGI, which we currently are not. My preferred way would be to do it the stylesGI method way but a bug in the current Jaspr build, since corrected, causes a problem with that so for the time being it will use this raw style way. It does show a positive aspect of Jaspr which is that it has mechanisms for dropping down to raw HTML/CSS processing when necessary.

The Game Tile State Provider

The gameTileStateProvider implementation which is identical to the Flutter version. Because they are literally identical I am linking back to the description of the implementation from the Flutter post

The WindowKeyboardListener Component

In order to keep the code in the widgets feeling more Flutter like I decided to opt for writing a WindowKeyboardListen class rather than directly wiring in the keyboard listener in the same way we did with Dart Web. The implementation itself is doing the exact same thing but it is encapsulated in this StatefulComponent that knows how to build and dispose of itself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
typedef KeyboardEventHandler = Function(KeyboardEvent event);

class WindowKeyboardListener extends StatefulComponent {
  final KeyboardEventHandler? keyDownEventHandler;
  final KeyboardEventHandler? keyUpEventHandler;
  final KeyboardEventHandler? keyPressedEventHandler;

  const WindowKeyboardListener({
    super.key,
    this.keyDownEventHandler,
    this.keyUpEventHandler,
    this.keyPressedEventHandler,
  });

  @override
  WindowKeyboardListenerState createState() => WindowKeyboardListenerState();
}

class WindowKeyboardListenerState extends State<WindowKeyboardListener> {
  StreamSubscription<KeyboardEvent>? keyDownSubscription;
  StreamSubscription<KeyboardEvent>? keyUpSubscription;
  StreamSubscription<KeyboardEvent>? keyPressedSubscription;

  @override
  void initState() {
    super.initState();
    if (component.keyDownEventHandler != null) {
      keyDownSubscription =
          window.onKeyDown.listen(component.keyDownEventHandler);
    }

    if (component.keyUpEventHandler != null) {
      keyUpSubscription = window.onKeyUp.listen(component.keyUpEventHandler);
    }

    if (component.keyPressedEventHandler != null) {
      keyPressedSubscription =
          window.onKeyPress.listen(component.keyPressedEventHandler);
    }
  }

  @override
  void dispose() {
    keyDownSubscription?.cancel();
    keyUpSubscription?.cancel();
    keyPressedSubscription?.cancel();
    super.dispose();
  }

  @override
  Iterable<Component> build(BuildContext context) sync* {}
}

The bulk of the work is being done in the initState method, lines 25-40. We establish the mapping to the keyboard event streams from the Window object. Beacuse we are setting up stream subscriptions we want to properly dispose of them if our object is ever removed from the widget tree. This is done in the dispose method in lines 43-48.

Conclusion

We were able to build the Snake game using Jaspr in a very similar way to the Flutter version. It is so similar in fact that a bunch of the code is actually identical.

With that we’ve also completed the run through of all the implementations of Snake in Dart for all of the potential clients. Because a big part of this was started as how to do server side Dart I am hoping to have some follow up posts comparing the three web targeting implementations.