April 9, 2025 flutter ai

Building Generative AI for DartPad

Building Generative AI for DartPad

Hello, again, and welcome to another installment of Flutter + AI = Joy.” In today’s episode, we’re taking a behind-the-scenes look at the design and implementation of the generative AI features in the latest version of DartPad. Before we get started, if you haven’t already read Amanda’s most excellent blog post for an overview of the new functionality, I recommend starting there.

Streaming Responses

The first thing I needed to do to enable generative AI in DartPad was to add support for streaming responses. Even as fast as Gemini 2.0 Flash is, if you have to wait for the complete code for anything beyond Dart hello, world”, you’re gonna get antsy.

The backend service for DartPad is built on top of shelf, the package that provided server-wide support for Dart before it was cool. Shelf supports streaming, but the docs aren’t exactly available on the topic. Also, streaming hasn’t been used in DartPad before, so it was a bit of an experiment. Ultimately I turned gzip and I/O buffering off for the code-gen API endpoints and streamed the generated code back as UTF8-encoded bytes, which the client is expected to decode.

And this worked great — the server sent data back in chunks of bytes and the client decoded them back into a string, updating the UI for each chunk that it received. Except that the client only got one chunk with the complete response for every request. And this was true even though the server was sending back multiple chunks as Gemini provided them. So what was the problem?

It took a ridiculously long time (days!) to figure out that the Dart http package on the web was using XMLHttpRequest, which collapsed streaming responses into a single response, killing any chance to provide progress updates. With some hacking around, I figured out that the fetch API did the right thing, so the http package needed an update. I discovered this in February of 2025. The good news is that the Dart team had already done that work in November of 2024 and that the PR was pending! Once that PR landed, we were good to go.

Error Handling

My initial design proposal called for adding a Gemini menu to DartPad with New, Update, Fix and Image to Code functionality:

Image to Code was bundled together with Dart/Flutter Snippet (New) and Update code (Update) via the ability to attach images. Bringing up a dialog to enter a prompt made sense for New and Update, since DartPad doesn’t know what kind of code you want to generate or what updates you want to make.

For Fix, however, it was annoying to have to tell DartPad what the error was, since the analyzer was reporting the errors to me! So I hijacked the analyzer error message UI with the idea of building the prompt to suggest a fix for the user. The result is that now there’s a lightbulb to indicate analyzer messages and to provide an easy way for the user to trigger the menu of potential fixes. Right next to that, I added a Gemini sparkle icon:

Clicking on the sparkle bundles up the error message automatically, asks Gemini for a fix and provides you a diff:

That’s just magic! Once I had it working for analyzer errors, I needed it for run-time errors, too, so I added the Gemini sparkle to the console output window.

When you press on the sparkle icon in this case, DartPad will bundle up your run-time error and suggest a fix.

Unfortunately, there was some work to enable the magic for run-time errors. Previously, there had been no reason to distinguish between normal console output and error output. That meant there was no good way to decide when to show the blue sparkle. However, you certainly do not want to show the Suggest Fix button when a Dart app is printing the last 10 numbers of pi. Luckily, John Ryan, Flutter DevRel and engineering lead for DartPad, came to the rescue with a fix that allowed me to reliably show the blue sparkle only when it was needed.

UX Shortcuts

After a long time on Unix before Windows and a long time on Windows before Mac, I’ve become a keyboard guy. I want to know all of the keyboard shortcuts so I can avoid using the mouse. While building and testing DartPad, I spent a lot of time in the prompt and code generating dialogs, both of which require you to press the Accept button. So I was doing that a lot. This annoyed me, so I added a keyboard shortcut:

  • Ctrl+Enter (Cmd+Enter on macOS) will trigger the Accept action

And because I’m super lazy:

  • Accepting the generated code will trigger the Run action

  • Or if hot reload is enabled, the Reload action will be triggered instead

I added all of this simply because I couldn’t figure out a case when that isn’t what you wanted to happen. This means that you can enter your prompt, press Ctrl/Cmd+Enter once to generate the code, then again to accept it and it will automatically be run/reloaded for you. No muss, no fuss. No [mouses](https://en.wikipedia.org/wiki/Computer_mouse#:~:text=A computer mouse (plural mice,motion relative to a surface.) harmed in the creation of this feature.

Necessity is not the mother of invention; laziness is.” –J. Michael Sells (my dad)

Future Hopes & Dreams

The initial goal for adding generative AI features to DartPad was to do so with a simple one-and-done style prompt in a modal dialog instead of the multiple prompts of a chat-style UI. Plus, by adding the new functionality without interfering with any of the existing DartPad UI, we could test it first to see if anyone cared.

It’s already apparent that you care. And that you really don’t like the modal dialogs. Instead, you want the prompt and iterate style of a chat interface (aka vibe coding). Toward that end, personally I’d like to see DartPad move towards something like this in the future:

What do you think? How would you like DartPad to work wrt generative AI? Please drop your thoughts below!

April 2, 2025 flutter ai

Building the Flutter AI Toolkit

Building the Flutter AI Toolkit

It has been quite an exciting few years since I left Google in 2022. I had been on the Flutter team for 3 years by that point, helping it go from v1.0 to v3.0, from hundreds of thousands of users to millions. It was exciting, but time for a change. And boy did I get some! Over the next 3 years I did the following:

  • Worked on the developer experience for VR/AR/XR at Meta in their Reality Labs division.
  • Helped to ship the v1.0 for both the consumer and the enterprise versions of Cody from Sourcegraph, a long-time start-up with amazing code search capabilities. Seriously, if you haven’t tried Sourcegraph Code Search, you should. And if your company is using Code Search, their AI coding assistant Cody integrates with the same code. Give em both a try!
  • Retired from full-time employment after a 35-year software engineering career (and more than that if you count when I started writing code on my Apple ][+ at the age of 14).
  • Put up my shingle as an independent consultant specializing in applied AI.
  • Built the Flutter AI Toolkit.
  • Started giving Flutter talks again with Dart and Flutter and AI, Oh My! at FlutterCon in 2024 (you click the link to watch the talk).
  • Built the generative AI features in the latest version of DartPad.
  • Have built some other AI stuff too that hasn’t shipped yet. Stay tuned!

Even before I left the Flutter team, I was spending my spare time digging into AI. When I was at Meta, I was focused on xR, but again, spending my spare time on AI. And then ChatGPT flipped the table, so when the opportunity to work on AI in the developer tooling space came up, I couldn’t say no.”

In June of last year, I decided that I wanted to control my own time again, so I retired.” That’s not to say that I wanted to stop the work — I love what I do — it’s just that my pesky job was taking up all my time. As the VP of Product, I had all kinds of influence on what other folks did but no time to get my own hands dirty! Once I retired, of course, I wanted to spend time with Flutter; it’s got the best API, the best team and the best community in tech, and I’ve seen more than my share. But now I had a new focus: applied generative AI.

The first thing I did in this new area was work with the Flutter and Firebase teams on the creation and application of the Flutter AI Toolkit.

Hello, Flutter AI Toolkit

The goal of the Flutter AI Toolkit (or just AIT from now on) is that it provides an LLM chat widget that you can easily plug into your existing Flutter apps. The Flutter team shipped the initial version in December, 2024, which you can read all about in Amanda’s most excellent blog post on the topic. Since then, I’ve been taking user feedback to add new features and fix issues. The current version as I write this is 0.6.8 but I’m going to be shipping a 0.7.0 soon.

To get started, I’ll refer you to the flutter_ai_toolkit package README. Once you’ve done that, a minimal chat app in Flutter using Gemini 2.0 is as easy as this:

import 'package:flutter/material.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
import 'package:google_generative_ai/google_generative_ai.dart';

void main() => runApp(_MainApp());

class _MainApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
    home: Scaffold(
      body: LlmChatView(
        provider: GeminiProvider(
          model: GenerativeModel(
            model: 'gemini-2.0-flash',
            apiKey: 'GEMINI-API-KEY',
          ),
        ),
      ),
    ),
  );
}

There’s not much to this code, but it brings up some interesting points anyway:

  • The name of the chat widget is LlmChatView
  • It supports many options, but only one is required — a provider that connects your LLM to the features needed by the chat widget
  • There are two LLM providers that come out of the box: GeminiProvider and VertexProvider
  • These two providers both take a GenerativeModel instance provided by the Google AI Dart SDK and Vertex AI in Firebase Flutter packages respectively, enabling you to configure them fully as you choose.
  • The separation of the chat widget from the communication channel with the LLM allows you to plug in any LLM-like service you’d like.

If you run this code, you’ll get a very simple, but fully functional, cross-platform AI chat app. A slightly fancier version is hosted online that you can try right now. It looks like this by default:

Notice the suggestions and the welcome message, both of which you can specify as optional parameters to LlmChatView. Also notice the + button, which allows the user to upload an image or a file, and the mic button, which allows the user to provide voice input.

Custom Styling

Beyond the basic look n’ feel, the demo example also provides some styling options:

Out of the box, the Flutter AI Toolkit supports being hosted in MaterialApp or CupertinoApp. However, as of the pre-release status at the time of this writing, it does not automatically adapt to the current theme provided by either. Instead, the look n’ feel was designed by a Google UX designer to have its own modern style. And if you’d like to tailor it to your app, you have complete control (as you can see above). Check out the dark_mode and custom_styles examples for the details.

When I went to implement styling support, I took my inspiration for the styling support from the flutter_markdown package. Theming is great for when you’d like to have a lot of a widget in your app, e.g. you don’t want to set the styles for each Text widget — you want to use a theme for that. However, I figured that you’d have about one LlmChatView widget, so theming didn’t make as much sense as just passing in a style object like you can do with the Markdown widget.

That said, it’s an obviously missing feature to be able to add the ability to pick up on the current Material or Cupertino theme and set those as defaults for an LlmChatWidget. If this is something you’re passionate about, PRs are gladly considered!

Custom Response Widgets: Beyond Text

By default, the AIT shows LLM responses as Markdown:

But what if you want the AI to respond with something richer — like a card UI, a chart or a recipe? As an example, consider a recipes app that allows the user to maintain a database and to add to it via a conversation with an LLM. To be able to get the data out of the LLM to add to the database, you don’t want to scrape it out of Markdown — you want well-formed data that you can parse reliably. You can get that by configuring your model to output JSON. By default, that JSON would look like this when shown to the user:

Of course, this isn’t particularly useful for the user, so the AIT allows you to show your own Flutter widget, as the recipe example does:

In addition to showing the recipe in a way the user can read, the example also provides an Add Recipe button to allow the user to easily drop the recipe into their database, all from the comfort of their AI chat.

Pluggable LLMs

As big of a fan as I am of the Gemini models (2.5 pro experimental is blowing the doors off as I write this), different apps have different needs. Since I had to make LLMs pluggable to support Gemini and Vertex (both of which have their own API over the Gemini LLMs), I thought it would be useful to support the ability to talk to any LLM-like backend. And the community has responded with the flutter_ai_providers package. Together with what comes out of the box and what is provided by the community, the AIT supports the following models:

  • Google Gemini 1.x and 2.x, flash and pro, stable and experimental
  • OpenAI GPT and o series of models
  • Anthropic Claude series of models
  • 100s of locally hosted Ollama models, including Google Gemma3, QWQ, Deepseek, Llama3.x, etc.

If that isn’t enough for you, you can implement the LlmProvider interface and plug in anything you like. For debugging purposes, I built EchoProvider but my favorite use of this feature is hooking up the LlmChatView to flows that I define in Firebase Genkit.

Check out the AI Toolkit docs for more details, but it’s a pretty simple interface. Even the Gemini and Vertex AI providers are only ~150 LOC each.

Can Haz More?

I’ve just touched on some of the big pieces of the AIT; for the full details, I recommend the AI Toolkit docs. In addition, there are extensive feature-specific samples on GitHub.

For something a bit more end-to-end, I’ve also built out a full chat app using the AIT:

Notice the list of chats on the left. Those are stored in Firestore and the app provides full management of the history and name of each chat, so the user can rename them, delete them and create new ones. There’s even a little magic that automatically names each chat based on the first question the user answers, just for fun.

In addition, there’s a more fully-featured version of the recipe sample using Firebase for the storage of recipes as well as the Vertex AI LLM.

If you’re interested in other Flutter and Firebase AI samples, I’ll point you at the Quickdraw and Planning samples:

Where Are We?

I was not alone in building the AIT. In addition to the contributors on the repo (thank you, contributors!), I want to give a special shout-out to Rory Blyth. He’s been my friend for decades now and been working on generative AI since the GPT-1 era. He did a bunch of advance work on the mic input and the recipe sample as well as being my build buddy through the whole thing. Thanks, Rory!

It is amazing to me how much — and how quickly! — AI is changing the development tooling landscape. Hopefully you’ll find that the Flutter AI Toolkit makes it easy for you to integrate LLM chats into your Flutter apps. Customize the chat interface, display rich responses beyond text, and leverage community-driven providers for a myriad of LLM options. Dive into the docs, explore the samples, and don’t hesitate to contribute or report issues. Or just drop me a line; I’m super interested to see what you’re building!

September 30, 2024 flutter

Dart and Flutter packages need your Love!

Dart and Flutter packages need your Love!

Do you have some free time? Are you excited to get involved in the Dart and Flutter ecosystem? Then have I got a deal for you!

Last week, the Founder and CEO of Shorebird, who is coincidentally also the Founder of the Flutter project at Google, had this to say about the current start of the state of the package ecosystem that lies at the core of Dart and Flutter:

The reason that Eric was asking for a list of importand & abandoned” packages was so that the community could decide which packages needed some more love. Such a list is important, for example, if you’re the FlutterFlow corporation and at a recent keynote, you’ve recently announced that you have $1M to contribute to the Flutter community (thanks Axel and Abel!).

For the rest of us, such a list is important if we’d like to get involved with the Dart and Flutter community and pitch in on some of the packages that need a little bit of love.

Towards that end, I’ve produced an initial list of the top 500 packages with a calculated love number”, which is equal to the number of days since publication divided by the popularity. This should really be called the needs love number,” since the higher the number, the more a package needs love.

I’ve got two ranges of numbers that sometimes cross into the red zone in this spreadsheet. The first is the number of days since publication, which is marked in red if the package hasn’t been published within the last 6 months. Similiarly, if the love # is >225 (180 days/80% popularity), I mark it as red.

provider & permissions_handler: have love

You can see several packages marked as red in this screenshot, the first being the provider package. The provider package has a good excuse for not being updated recently — the author has decided that he’d rather people use the riverpod package as a replacement. That’s fair and it says so in the notes (and you can suggest notes for packages you have info about in the publove repo). Even so, Remi is keeping provider relatively up to date, as evidenced by the fact that his love # is still in a good range. Likewise with permission_handler from baseflow is also still in a good range. Thanks to both of you for the great work!

font_awesome_flutter: has love

The font_awesome_flutter package also has a red love #. So is it in trouble? Let’s look at the repo:

Those are pretty respectable numbers, so I’d say that the folks at fluttercommunity.dev are doing pretty well with it. Thanks!

google_sign_in: needs love!

On the other hand, the Flutter team itself isn’t able to always keep up with demand. For example, the google_sign_in package has a love # of 291:

It does seem like the google_sign_in package would use some love. Do you have a special affinity for social auth? If so, perhaps submit a PR? You can tell by the numbers that PRs get a higher priority than just plain ol’ issues.

group_list_view: needs love!

The package in the list of the top 500 with the highest love # (1369) is group_list_view. Does it need love?

Having not been published in 3.5 years and not having a verified publisher, perhaps Daniel could use some help with this one, especially with a 95% popularity score and 1299 likes.

flutter_barcode_scanner: really needs love!

Probably the hardest kind of package to keep well-maintained in a plugin, which have platform-specific code. For example, the flutter_barcode_scanner package has some outstanding issues:

Even though this package has 1342 likes, it has been published in 3 years and has no verified publisher. It could clearly use some love.

Where are we?

I pulled this list together because I want the number of Dart and Flutter packages that get regular love to increase and to squeeze the red out of this spreadsheet. If you’re like to give me a hand with finding the packages that need love, feel free to lend a hand with the publove repo. The core data all comes from the excellent pub_api_client package that Leo maintains w/o any docs or support from Google (thanks, Leo!). Also, if you’re interested in pub.dev stats in general, I just stumbled onto the pubstats.dev site, which looks promising.

Scrolling through the publove list, there’s a lot of red. All of us working together is what makes Dart and Flutter great, so if you’ve got some free time and there’s something on this list that appeals to you, lend a hand!

April 11, 2020 flutter

Understanding Flutter: deep links on the web

Understanding Flutter: deep links on the web

NOTE: This content has been subsumed by the package I wrote to support declarative routing via the Navigation 2.0 API: go_router. This package is what I use now for my own projects and I hope that you’ll find it useful for your projects as well. Enjoy.

Lately I’ve been working on a little side project with a friend of mine and I’m the coder, so I get to pick the tech I want for the implementation. My choice is easy: Flutter! We’re targeting the web with our project for two reasons: 1) it makes distribution easy and 2) it enables us to do social media marketing with deep links to content that gets people to come to the site, sign up, … profit!

Of course, for that to work, deep linking has to work, which is a whole good news/bad news thing with Flutter web. The good news is: all the bits and pieces are there to enable deep linking. The bad news is, like a father in the wee hours of Christmas morning, there is some assembly required.

The sample code

This blog post builds up a very simple app step-by-step in a way meant to take you from the basics of navigation in Flutter (which I assume you already know) to the advanced considerations you’ll need to take into account for your Flutter web app to work well. All of the code from this blog post is available step-by-step as commit history in this github repo. The last commit represents best practices as far as I know them.

What is deep linking?

While mobile apps also support deep linking, for the purposes of this post, we’ll focus on the web-specific definition from Wikipedia:

deep linking is the use of a hyperlink that links to a specific, generally searchable or indexed, piece of web content on a website (e.g. ”http://example.com/path/page), rather than the website’s home page (e.g.,”http://example.com). The URL contains all the information needed to point to a particular item.

It’s that last bit (emphasis mine) that we care about — a shared URL that our Flutter app can use to get the user to the appropriate content.

Deep linking scenarios

There have been a few interesting articles lately on Flutter web as related to navigation and especially deep linking, like Deep Linking for Flutter Web by Rody Davis, which shows how to use fluro (“The brightest, hippest, coolest router for Flutter”) and Flutter web: Navigating URLs using named routes by Per Classon, a member of the Material team at Google (hi, Per!). However, while both articles are a great intro to the topic, neither of them covers everything I need to make sure that my app is a good web citizen.

If you’re not familiar with the basics of Flutter navigation and routes, I recommend the Navigation and routing page from the docs. However, I’m interested in more advanced scenarios that have specific implications on the web:

  • Navigating to sub-pages w/ parameters, e.g. /family?fid=452 or /family/452
  • Navigating to a sub-sub page w/ parameters and parent-child dependencies, e.g. /family/452/person/42
  • Error handling for navigation to a sub (or sub-sub) page w/ invalid parameters
  • Error handling for navigation to a page that’s not there

There’s a lot here, so let’s dig in.

Sample Data Model

To start, let’s say we want to navigate through a hierarchy of people grouped into families:

  • families
    • family
      • name=Sells
      • people
        • person: name=Chris, age=50
        • person: name=John, age=25
        • person: name=Tom, age=24
    • family
      • name=Addams
      • people
        • person: name=Gomez, age=55
        • person: name=Morticia, age=50
        • person: name=Pugsley, age=10
        • person: name=Wednesday, age=17

In Dart, this data can be represented like so:

class Person {
  final String name;
  final int age;
  Person({this.name, this.age});
}

class Family {
  final String name;
  final List<Person> people;
  Family({this.name, this.people});
}

class App extends StatelessWidget {
  static final title = 'Flutter Web Deep Linking Demo';
  static final families = [
    Family(
      name: 'Sells',
      people: [
        Person(name: 'Chris', age: 50),
        Person(name: 'John', age: 25),
        Person(name: 'Tom', age: 24),
      ],
    ),
    Family(
      name: 'Addams',
      people: [
        Person(name: 'Gomez', age: 55),
        Person(name: 'Morticia', age: 50),
        Person(name: 'Pugsley', age: 10),
        Person(name: 'Wednesday', age: 17),
      ],
    ),
  ];
  ...
}

This data can be arranged nicely to give us a sample to play with, e.g. a home page, a sub-page and a sub-sub-page.

image-20200411174338110

By the time we’re done, we want to be able to be able to copy a deep link to a person and for that to navigate correctly.

Tip #1: pass settings to MaterialPageRoute

Intuitively, because we want to navigate to sub-pages for each family and for each person, we know we have to pass arguments to our named navigation routes, as in this code.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(App.title)),
        body: ListView(
          children: App.families
              .map((f) => ListTile(
                  title: Text(f.name),
                  // navigate to the /family page to show a specific family
                  onTap: () => Navigator.pushNamed(context, '/family',
                      arguments: {'family': f})))
              .toList(),
        ),
      );
}

class FamilyPage extends StatelessWidget {
  final Family family;
  FamilyPage(this.family);

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(family.name)),
        body: ListView(
          children: family.people
              .map((p) => ListTile(
                  title: Text(p.name),
                  // navigate to the /person page to show a specific person
                  onTap: () => Navigator.pushNamed(context, '/person',
                      arguments: {'family': family, 'person': p})))
              .toList(),
        ),
      );
}

class PersonPage extends StatelessWidget {
  final Family family;
  final Person person;
  PersonPage(this.family, this.person);

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(person.name)),
        body: Center(child: 
                Text('${person.name} ${family.name} is ${person.age} years old')),
      );
}

This code shows the basics of navigation that will work just fine for navigation inside the app. Click on a family and you’ll navigate to the /family route with that family as an argument and likewise for a person and the /person route. The arguments themselves are passed in a handy map.

Here’s the code to create the pages with the appropriate constructor args:

import 'package:flutter/material.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  static final title = 'Flutter Web Deep Linking Demo';

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: title,
        theme: ThemeData(primarySwatch: Colors.blue),
        home: HomePage(),
        onGenerateRoute: (settings) {
          final args = settings.arguments as Map<String, dynamic>;
          switch (settings.name) {
            case '/':
              return MaterialPageRoute(builder: (_) => HomePage());
                        // pull the family object out of the arguments object
            case '/family':
              final family = args['family'] as Family;
              return MaterialPageRoute(builder: (_) => FamilyPage(family));
            // pull the family and person objects out of the arguments object
            case '/person':
              final family = args['family'] as Family;
              final person = args['person'] as Person;
              return MaterialPageRoute(
                  builder: (_) => PersonPage(family, person));
            default:
              return MaterialPageRoute(
                  builder: (_) => Four04Page('unknown route: ${settings.name}'));
          }
        },
      );
}

Here we’re using settings.name to find out which route we’d like and constructing the appropriate page widget with data from the settings.arguments. That widget we then pack into a MaterialPageRoute and return, which Flutter uses to push the route onto the stack of routes and show it. There are other ways to do routing with Flutter besides implementing onGenerateRoute for MaterialApp, but only a couple ways that allow you to to parse the arguments passed to Navigator.pushNamed. This is the method I like best.

In the case that there is no matching route, we show a 404 page:

class Four04Page extends StatelessWidget {
  final String message;
  Four04Page(this.message);

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text('Page Not Found')),
        body: Center(child: Text(message)),
      );
}

And what’s so wrong with this solution you ask? Well, for one thing, notice the address bar on the person page:

image-20200411193430649

That’s right; there’s nothing in the address bar except that base URL for the app. If it doesn’t change as you go from page to page in the app, then the user has no deep link to share. Luckily, we can fix this by simply passing the settings object passed to onGenerateRoute onto to the MaterialPageRoute constructor like so:

onGenerateRoute: (settings) { // take the settings here
  ...
  return MaterialPageRoute(settings: settings, // and pass them here
                           builder: (_) => FamilyPage(family));
...

As you already know, the settings object contains the name of the route, which Flutter than uses to populate the address bar, since we’ve been so nice as to supply it:

image-20200411194657671Now we have the name of the route in the address bar as expected. However, that’s still not enough for deep linking (in fact, we’re just getting started).

Tip #2: pass object IDs in the route name; not objects in arguments

If you were to simply refresh that page at this point, here’s what you get:

image-20200411195020036

The problem, which Flutter is doing a very good job of hiding, is that we’re trying to pull arguments out of a settings object that hasn’t got any. Those arguments are only set when we call Navigator.pushNamed. When we refresh the page, Flutter is taking the URL from the browser and doing the navigation itself, which ends up in our onGenerateRoute implementation. However, it doesn’t know anything about your app or it’s data, so settings.arguments is going to be null. Now you’re out of luck because you have no idea what data to build your page around. The only information you’ve got is what’s in settings.name.

So what do you do? That’s right — pack everything you want into the route name itself.

Time-out: A note about data models and unique IDs

If we’re going to pass references to objects around as strings, we’re going to need the objects to have unique identifiers:

class Person {
  final String id; // require unique id
  final String name;
  final int age;
  Person({@required this.id, this.name, this.age});
}

class Family {
  final String id; // require unique id
  final String name;
  final List<Person> people;
  Family({@required this.id, this.name, this.people});
}

static final families = [
  Family(
    id: 'f1', // provide unique id
    name: 'Sells',
    people: [
      Person(id: 'p1', name: 'Chris', age: 50), // provide unique id, etc.
      Person(id: 'p2', name: 'John', age: 25),
      Person(id: 'p3', name: 'Tom', age: 24),
    ],
  ),
  Family(
    id: 'f2',
    name: 'Addams',
    people: [
      Person(id: 'p1', name: 'Gomez', age: 55),
      Person(id: 'p2', name: 'Morticia', age: 50),
      Person(id: 'p3', name: 'Pugsley', age: 10),
      Person(id: 'p4', name: 'Wednesday', age: 17),
    ],
  ),
];

In this case, I simply added an id property to family and person objects, which sweeps many issues under the carpet. You have many questions to consider when adding unique IDs to your data model:

  • Are they globally unique? Anything with a globally unique ID can be shared globally, e.g. YouTube videos have globally unique IDs. Every user may not have access to every object in your app’s data, e.g. Google Docs all have globally unique IDs but not everyone can access every doc.

  • Are they unique to the user that owns the data? If so, the deep linking can still useful for that user, perhaps as a favorite link in their browser.

  • Are they unique to the parent object? In that case, the parent object must be available when looking up the object. In the case of our sample, notice that Chris and Gomez have duplicate IDs; clearly we’ll need a family object around to disambiguate when looking up a person object by ID.

  • Where do the IDs come from? It’s very easy to generate globally unique IDs; in fact, there are several Dart packages that generate unique IDs on demand. However, the problem there is storage cost and long URLs. Even so, globally unique IDs solve a lot of problems and are used a lot.

    On the other hand, you may create a new object, send it the database and then let the backend assign a shorter ID that’s still unique; lots of data storage solutions provide this service. The problem here is what ID do you use in the UI while you wait for the database to give you back the real ID, assuming you want to show the object prior to confirmation of synchronization with the backend.

    Or you may decide to let the client generate short but unique IDs, perhaps using max on a sequence of integers. The problem in that case is what happens in the case of a collision.

I didn’t say I had any answers to these questions; only that you have to consider them…

Time-in: passing object IDs in routes

Once have have unique IDs in your data, you can use them to pass around in your route names:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(App.title)),
        body: ListView(
          children: App.families
              .map((f) => ListTile(
                  title: Text(f.name),
                    // pass the family id as a query parameter
                  onTap: () => Navigator.pushNamed(context, '/family?fid=${f.id}')))
              .toList(),
        ),
      );
}

Here we’re passing the family object’s ID via a standard web-style query parameter, e.g. /family?fid=f1. We can do the same thing for the person, but it needs two arguments:

class FamilyPage extends StatelessWidget {
  final Family family;
  FamilyPage(this.family);

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(family.name)),
        body: ListView(
          children: family.people
              .map((p) => ListTile(
                  title: Text(p.name),
                  onTap: () =>
                            // pass the family id and the person id as query parameters
                      Navigator.pushNamed(context,
                                          '/person?fid=${family.id}&pid=${p.id}')))
              .toList(),
        ),
      );
}

Parsing them is like parsing query parameters on the web:

onGenerateRoute: (settings) {
  // split out the real route name from the args, e.g. /family and fid=f1
  final parts = settings.name.split('?');
  final args = parts.length == 2 ? Uri.splitQueryString(parts[1]) : null;
  switch (parts[0]) {
  ...
  case '/family':
      // look up the family and complain if it's not found
      final family =
          App.families.singleWhere((f) => f.id == args['fid'], orElse: () => null);
      if (family == null)
        return MaterialPageRoute(
            settings: settings,
            builder: (_) => Four04Page('unknown family: ${args["fid"]}'));

      return MaterialPageRoute(settings: settings, builder: (_) => FamilyPage(family));

    case '/person':
      // look up the family and person, complaining if either is not found
      final family =
          App.families.singleWhere((f) => f.id == args['fid'], orElse: () => null);
      if (family == null)
        return MaterialPageRoute(
            settings: settings,
            builder: (_) => Four04Page('unknown family: ${args["fid"]}'));
      final person =
          family.people.singleWhere((p) => p.id == args['pid'], orElse: () => null);
      if (person == null)
        return MaterialPageRoute(
            settings: settings,
            builder: (_) => Four04Page('unknown person: ${args["pid"]}'));

      return MaterialPageRoute(
          settings: settings, builder: (_) => PersonPage(family, person));
  ...
  }
},

At the top of the code, we split a URL in the form /name?args, e.g. /family?fid=f1. We switch on the route name, using the query parameters to look up the objects we need to construct the page widgets, using our 404 page to complain if we can’t find them. Suddenly we’ve got the first glipses of deep linking!

image-20200411210303289

Not only have we baked the object IDs into the link but if we refresh or past the link into a new Chrome page, it will actually show the same content! Or, if you want to change the person to p2 by editing the URL directly, you’ll see Morticia!

image-20200411210446221

If you get a little too excited and enter an ID that’s not available, the app will let you know that you’ve stepped off the path.

image-20200411210638111

Time-out: A note about the # in the URL

By now you have noticed the # in the URL of your Flutter app, which isn’t typical of most web apps. This is a special designation for the browser when it splits the URL into two sections. Section one, to the left of the #, is for the HTTP server. This is how your Flutter app gets downloaded to your users’ machines.

Section two, to the right of the #, is used for client-only information, e.g. if you wanted to scroll to a specific anchor on your HTML page. Single Page Applications (or SPAs as the cool kids call em) use section two data to navigate between client-side pages” of the app without going back to the server. Since your Flutter web app is just a really fancy SPA, it uses section one of the URL to download the entire app onto the user’s machine and then uses section two for navigation between pages” as defined by Flutter. In fact, if you were to take the # out of the URL, bad things would happen, since the server won’t know what the heck you’re talking about:

image-20200411223958634

You can certainly fix this on the server with redirection, e.g. /person => /#/person. While you’re at it, you’ll also want to redirect 404s to your own Flutter-based 404 page. Apparently most web servers consume Apache .htaccess files, which is supposed to make this kind of server-side forwarding easy to do. I haven’t tried this myself so please let me know in the comments if this actually works. Thanks!

Time-in: do we have deep linking yet?

At this point, you might thing that you were done with our deep linking support for Flutter web. If only that were so…

Tip #3: construct pages with object IDs, not objects

We have a problem in how we look up objects in our code right now, although it might be hard to see:

onGenerateRoute: (settings) {
      ...
      // objects on the network are further away than they appear...
      final family =
          App.families.singleWhere((f) => f.id == args['fid'], orElse: () => null);
      ...
}

The onGenerateRoute method is synchronous; if requires you to provide a route immediately. However, what if your object is stored on disk or on the network or needs to be requested via carrier pigeon? Our little model is simple and not representative of the real world, so it’s deceptive; it’s going to be more common that you’ll need to load the data from somewhere when you’re mapping object IDs to their respective objects. To support that case, I recommend you get into the habit of passing the object IDs to the page constructors so that the page itself can do the asynchronous lookup. That way your page can show a loading indicator as well as an error message if lookup fails. Here’s a simulated example of such a thing:

class FamilyPage extends StatelessWidget {
  final Future<Family> family;
  FamilyPage(String fid) : family = _load(fid); // construct with an object ID

  // load object by id
  static Future<Family> _load(String fid) async {
    // simulate a network lookup...
    await Future.delayed(Duration(seconds: 3));

    final family = App.families.singleWhere((f) => f.id == fid, orElse: () => null);
    if (family == null) throw 'unknown family: $fid';

    return family;
  }

  // funky FutureBuilder code to show three states: loading, failed, succeeded
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: FutureBuilder<Family>(
            future: family,
            builder: (_, snapshot) => snapshot.hasData
                ? Text(snapshot.data.name)
                : snapshot.hasError ? Text('Page Not Found') : Text('Loading...'),
          ),
        ),
        body: FutureBuilder<Family>(
          future: family,
          builder: (_, snapshot) => snapshot.hasData
              ? ListView(
                  children: snapshot.data.people
                      .map((p) => ListTile(
                          title: Text(p.name),
                          onTap: () => Navigator.pushNamed(
                              context, '/person?fid=${snapshot.data.id}&pid=${p.id}')))
                      .toList(),
                )
              : snapshot.hasError ? Text(snapshot.error) : CircularProgressIndicator(),
        ),
      );
}

The code to load the family object is just like before, except we’re doing it asynchronously to simulate a networked environment. If there’s an error, we throw it as a string, which is caught by FutureBuilder. The FutureBuilder code is funky because we’re using the result of our asynchronous loading in two places (the appbar’s title and body) and because we’re dealing with three states: loading, failure and success, all of which result in three different UIs, e.g. here’s the one for loading:

image-20200411220419590

Notice, however, that this is an environment in which we can take our sweet time loading data and giving the user progress indication, error messages, etc. This is unlike the onGenerateRoute method, which must return synchronously (and can now be greatly simplified):

onGenerateRoute: (settings) {
  final parts = settings.name.split('?');
  final args = parts.length == 2 ? Uri.splitQueryString(parts[1]) : null;
  switch (parts[0]) {
    case '/':
      return MaterialPageRoute(settings: settings, builder: (_) => HomePage());

    case '/family':
      // pass the family object ID and let the page look it up 
      return MaterialPageRoute(settings: settings, 
                               builder: (_) => FamilyPage(args['fid']));

    case '/person':
      // pass the family and person object IDs and let the page look them up 
      return MaterialPageRoute(
          settings: settings, builder: (_) => PersonPage(args['fid'], args['pid']));

    default:
      return MaterialPageRoute(
          settings: settings,
          builder: (_) => Four04Page('unknown route: ${settings.name}'));
  }
},

Now would be a good time to think about using the repository pattern in your code so that you can abstract away the data loading, use in-memory caching, etc. but that’s beyond the scope of this post.

At this point, we’ve got the basics working — you have a link that you can provide to your app that provides enough information for loading objects for each of your app’s pages. However, because we’re using a flat namespace for our URLs, the navigation may not work the way you expect.

Tip #4: use nested routes to build a stack

You may not have realized it, but every time you press Refresh on one of your app’s pages or press Enter inside the address bar (even if you don’t change the URL), you’re actually reloading your app, i.e. starting over from main. If you’re on the home page when that happens, then you reset the browser and app navigation stack of your app to have one entry:

Navigation Stack Index Route
Home page 0 (shown) /

Notice the lack of back button in your app in this case:

image-20200411222952342

If you click on the list entries to navigate to the person page, thus also going through the family page, you’ll have a navigation stack with three entries:

Navigation Stack Index Route
Person page 2 (shown) /person?fid=xxx&pid=yyy
Family page 1 /family?fid=xxx
Home page 0 /

However, if you do a deep link in a new browser instance to the person page, you’re getting a stack with just two entries in it:

Navigation Stack Index Route
Person page 1 (shown) /person?fid=xxx&pid=yyy
Home page 0 /

This leaves you in a strange place: if you navigate to the person page normally, pressing back gets you to the family page. However, if you just start from scratch with a deep link to the person page, Flutter’s back button takes you to the home page. You might ask: where did the family page go when I do deep linking?

A better question: why the heck is the home page there in the first place?!

The answer is simple: so that you have a way to get back to the home page instead of being stranded in the middle of the app with no way (sic) home.

The real question: how does the home page get onto the navigation stack?

At this point, we’ve actually found Flutter’s support for deep linking (yay!) When you start your app, Flutter (web or mobile or desktop or projected on the insides of your eyelids) will look for a route name from your MaterialApp initialRoute property. If it’s null (and it should always be null), then Flutter will get it’s initial route from the launching host. As I mentioned, Android and iOS both also have the concept of deep linking and this is how those are handled. In the case of the web, that initial route will come from the address bar. This is the behavior we want, which is why we should leave initialRoute as null.

If the initial route starts with a / then Flutter will start building a stack of routes, using / as the delimiter. For example, if we have the deep link /one/two, then it’s going to build a navigation stack like the following:

Navigation Stack Index Route
Two page 2 (shown) /one/two
One page 1 /one
Home page 0 /

This is what we want: you should use slashes in your routes so that the deep linking behavior you get is the same as the behavior you get when you’re navigating normally in your app. In the case of our sample, that means our person route really needs to change to use a / instead of the &, i.e. /person?fid=xxx&pid=yyy => /family?fid=xxx/page?pid=xxx.

Composing the route in the new form is easy:

Navigator.pushNamed(context, '/family?fid=${snapshot.data.id}/person?pid=${p.id}')

However, it’s parsing it that gets a little trickier, since we’re no longer looking for a route name that starts with /person but rather one that looks like /family(something)/person(something). The way to solve this problem is, of course, with regular expressions:

onGenerateRoute: (settings) {
  final name = settings.name;

  // start from most specific first
  if (name.startsWith('/family') && name.contains('/person')) {
    final re = RegExp(r'\/family\?fid=(?<fid>[^\/]+)\/person\?pid=(?<pid>[^\/]+)$');
    final match = re.firstMatch(name);
    if (match != null) {
      final fid = match.namedGroup('fid');
      final pid = match.namedGroup('pid');
      return MaterialPageRoute(settings: settings, builder: (_) => PersonPage(fid, pid));
    }
  }

  if (name.startsWith('/family')) {
    final re = RegExp(r'\/family\?fid=(?<fid>[^\/]+)$');
    final match = re.firstMatch(name);
    if (match != null) {
      final fid = match.namedGroup('fid');
      return MaterialPageRoute(settings: settings, builder: (_) => FamilyPage(fid));
    }
  }

  if (settings.name == '/') {
    return MaterialPageRoute(settings: settings, builder: (_) => HomePage());
  }

  return MaterialPageRoute(
      settings: settings, builder: (_) => Four04Page('unknown route: ${settings.name}'));
},

Instead of just blindly splitting the name looking for arguments, we have more complicated route names now, so we have to have a more nuaced way of recognizing them. Further, to reduce the complexity just a little, we now check the route names from more specific to more generic, which specifically allows us to look for /family routes with a simple use of startsWith, safe in the knowledge that if there was a /family/person route to be found that it would have been handled earlier in the code.

However, with that bit of code, now our deep link to /family?fid=xxx/person?pid=yyy gives us the same navigation stack as if we’d navigated there interactively in our app. In the case of deep linking, the Flutter back button on the AppBar is different from the back button on the browser.

In all cases, the browser back button will take us back to the page where we just were, so if we paste a deep link into a new browser window, the browser back button will take us back to the blank Chrome page — it’s only seen one of the Flutter app pages, so it’s got just one in it’s stack.

On the other hand, during the deep linking process, Flutter has built up a stack of routes as deep as appropriate based based on your initial route. When you press the Flutter back button, you’ll get to pages you might not have seen yet from that stack.

This might be confusing to the user, but it seems the most useful blending of the two worlds; the browser always takes you back to the last page you saw and the Flutter back button takes you to the logical page on the stack so you can always get back to home in your app. win-win.

And with that, we’ve finally in the state we want to be in; we have achieved deep linking in our Flutter web app!

Bonus Tip #1: better code through abstraction

You may look at the complicated logic for string matching and formatting littered throughout the sample code and think to yourself, there has to be a better way! In fact, there are several Flutter routing helper packages on pub.dev that you may want to check out. Myself, I didn’t understand what they were doing or why I cared until I dug this deep (sic) into the topic myself (and hence this blog post). That said, now that you understand it, too, you can build yourself a little helper that does the job for you.

abstract class Router {
  // derived classes implement these methods
  bool matches(RouteSettings settings);
  MaterialPageRoute route(RouteSettings settings);

  // helper for 
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    final router = routers.firstWhere((r) => r.matches(settings), orElse: () => null);
    return router != null
        ? router.route(settings)
        : MaterialPageRoute(
            settings: settings, 
            builder: (_) => Four04Page('unknown route: ${settings.name}'));
  }
  
  static final routers = [
    // start with most specific one first
    PersonPageRouter(),
    FamilyPageRouter(),
    HomePageRouter(),
  ];
}

Here I’ve build a little Router helper class. It’s meant to be the base for every page-specific router in my app, e.g. a PersonPageRouter knows how to route to a PersonPage. The matches and route methods are for the derived class to implement. Further, the Router provides an implementation of onGenerateRoute based on the list of routers it also contains.

The magic is in the matches and route method implementations, like this one for the FamilyPageRouter:

class FamilyPageRouter extends Router {
  static final _routeRE = RegExp(r'^\/family\?fid=(?<fid>[^\/]+)$');

  @override
  bool matches(RouteSettings settings) => settings.name.startsWith('/family');

  @override
  MaterialPageRoute route(RouteSettings settings) {
    assert(matches(settings));
    final match = _routeRE.firstMatch(settings.name);
    return match == null
        ? null
        : MaterialPageRoute(
            settings: settings, builder: (_) => FamilyPage(match.namedGroup('fid')));
  }

  static Future<T> navigate<T>(BuildContext context, Family family) =>
      Navigator.pushNamed<T>(context, '/family?fid=${family.id}');
}

The matches method simple asks the router if the route name is a match. If it is, it asks the route method to parse the route name and produce the appropriate route. Notice also that if it can’t find the information it needs to build a route, e.g. no family object ID, it returns null. Flutter will use this to abort the routing, which is a handy way to stop the user from going anywhere with a malformed URL.

In addition, each router provides a static method called navigate that takes the appropriate arguments to be able to push the correctly formatted named route. This allows us to avoid sprinking that knowledge all over your code. Instead, to route to the family page app, you simple have to do the following:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(App.title)),
        body: ListView(
          children: App.families
              .map((f) =>
                  ListTile(title: Text(f.name), 
                           // navigate to the page for the selected family
                           onTap: () => FamilyPageRouter.navigate(context, f)))
              .toList(),
        ),
      );
}

With this bit of refactoring, implementing the routing for the MaterialApp is very simple:

class App extends StatelessWidget {
  static final title = 'Flutter Web Deep Linking Demo';
  static final families = ...;

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: title,
        theme: ThemeData(primarySwatch: Colors.blue),
        home: HomePage(),
        // all of your navigation is handled here
        onGenerateRoute: Router.onGenerateRoute,
      );
}

Adding new routes is now a matter of defining a new Router implementation and adding it to the list of routers. Easy Peazy.

Bonus Tip #2: slash-only URLs without parameter names

Since we’re now using slashes to separate sub-pages, notice that our URL is verbose and redundant:

/family?fid=xxx/person?pid=yyy

It’s pretty clear simply by the placement which is the family ID and which is the person ID. For that reason, many apps prefer a syntax like this:

/family/xxx/person/yyy

The trick here is that Flutter is going to attempt to get a route for each of the segments, even though segments like /family without the family ID don’t make any sense. However, so long as you don’t return null for the first one (home) and the last one (the person page in our case), it’s ok to return a null in the middle and Flutter will not place them onto the navigation stack. To enable this bit of magic, we want our routers to produce routes like the following:

Navigation Stack Index Route
Person page 2 (shown) /family/xxx/person/yyy => PersonPage
n/a n/a /family/xxx/person => null
Family page 1 /family/xxx => FamilyPage
n/a n/a /family => null
Home page 0 / => HomePage

With our little router helper, we can get the slash-based URL formatting we want by simply changing the regular expression and the route name to use a slash instead of ?fid=:

class FamilyPageRouter extends Router {
  // use / instead of ?fid=
  static final _routeRE = RegExp(r'^\/family\/(?<fid>[^\/]+)$');

  @override
  bool matches(RouteSettings settings) => settings.name.startsWith('/family');

  @override
  MaterialPageRoute route(RouteSettings settings) {
    assert(matches(settings));
    final match = _routeRE.firstMatch(settings.name);
    return match == null
        ? null
        : MaterialPageRoute(
            settings: settings, builder: (_) => FamilyPage(match.namedGroup('fid')));
  }
  
  static Future<T> navigate<T>(BuildContext context, Family family) =>
      // use / instead of ?fid=
      Navigator.pushNamed<T>(context, '/family/${family.id}');
}

In the case of /family, which should return null (because it has no family ID), the trick is the combination of the matches method, which knows that this route is meant to be handled in this router, and the route method, which sees that the data in /family isn’t enough to actual produce a route, so returns null. The person page works the same way, resulting in the following:

image-20200412010641533

Our new deep linking format produces the navigation stack we want. And, with that earlier bit of refactoring magic and the updated router implementations, we can decide to format the route names however we like without having to change any of the rest of the app’s code.

Where are we?

Adding deep linking to your Flutter app comes with considerations about how to format route names to get a navigation stack that makes sense to your users. For Flutter on the web, we have to pile on the fact that users can enter URLs directly into the address bar and share the URLs far and wide; this is often something we want (let’s go viral!), but comes with more considerations, including making sure that the route name shows up in the address bar, handling malformed URLs and packing object IDs into the route name itself. This in turn has implications about how to structure the data model and where and how to load objects asynchronously, if needed.

Flutter for web is relatively new and still in beta. The needs of web have pushed on Flutter at it’s core, including a new Navigation design proposal that should make it easier to do the right thing when it comes to routing in your app, whether it runs on the web or not. Regardless, by understanding what’s going on and following the best practices outlined above, you can build great Flutter apps on the web right now!

August 4, 2019 flutter

Fun with Curl and Dart

Fun with Curl and Dart

If you’re a Dart programmer, the curl command doesn’t really help you. Oh, it can tease you with of its wonderful functionality, but you still have to take anything you can do with curl and manually translate it into Dart code.

Until now.

Updating curlconverter.com for Dart

For almost 5 years now, Nick Carneiro has maintained a wonderful site for converting curl commands into network code for your favorite language: curlconverter.com.

I guess his first most favorate language must be Python, since that’s the default, but since last week, there’s been a new language listed: Dart. In addition to providing this site, Nick did one other thing: he exposed the underlying tech as an OSS repo on GitHub: curlconverter. And that repo is a wonder to behold, because it was factored in such a way that I could add Dart generation support for Dart in a weekend. Truly an example for other multi-language tools to follow.

Chrome DevTools: Network

The reason you care about turning curl into Dart is because the Chrome DevTools turn the curl command format into a universal capture format for requests in Chrome. That means that you can go to your favorite web site, dig around on the Network tab of the Chrome DevTools, right-click on the bit that you like and pull it out as a curl command.

When you do, you’ll end up with something like this:

curl "https://www.fantasynamegen.com/barbarian/short/" -H "Upgrade-Insecure-Requests: 1" -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" -H "Referer: https://www.fantasynamegen.com/" --compressed

Even if you never run this as a curl command, you can still use it to generate code for your language of choice.

Dart networking code

Pasting the curl from Chrome DevTools into Nick’s curl converter website gives you Dart code that looks like this:

import 'package:http/http.dart' as http;

void main() async {
  var headers = {
    'Upgrade-Insecure-Requests': '1',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
    'Referer': 'https://www.fantasynamegen.com/',
    'accept-encoding': 'gzip',
  };

  var res = await http.get('https://www.fantasynamegen.com/barbarian/short/', headers: headers);
  if (res.statusCode != 200) throw Exception('get error: statusCode= ${res.statusCode}');
  print(res.body);
}

This is actually a complete program and if you put it into a file, e.g. fanname.dart, you’re almost ready to run it. You also need a minimal pubspec.yaml that looks like this:

# pubspec.yaml
name: fanname
dependencies:
  http:

The pubspec file lists the name of your app and it’s dependencies, specifically the http package. These two files together are enough to run the program and get the same output as the curl command:

$ pub get
$ dart fanname.dart

The call to pub get pulls down the dependencies and the call to dart runs your program. Of course, this assumes that you’ve got the Dart SDK installed and on the PATH. Assuming you do, you’ll have output that looks like this:

Actually, the output in this case goes on for a lot longer than that, so it isn’t particularly helpful, but with a little filtering, it can be:

$ dart fanname.dart | grep '<li>' | sed 's/<\/*li>//g'
Vadryt
Hildecon
Axra
Rordryt
Freyagar
Egelkele
Mand
Sigceo
Krokrolm
Ric
Vase
Hildekrucen
Rabeorth
Thakald
Morncrom
Lafthe
Garrak
Caror'n
Theodtarg
Ordhall

Where are we?

If the network code you want is already being executed via curl or on the web, that means you can take that curl command and turn it into Dart code with zero effort on curlconverter.com. The code from that site can be executed just like a curl command immediately or used as the start of your own code.

The example I was showing in this post generated HTML, but if instead the result was JSON, then you can take that JSON output and paste it into quicktype.io to get the JSON serialization/deserialization code for Dart, too. It’s a great time to be a Dart programmer!

December 29, 2017

Returning to D&D

Returning to D&D

30 years ago, I paused a 5-year love of D&D to attend college. About six months ago, I picked it up again with my adult sons, who’d gathered for my birthday. We started with $20 on the D&D 5e Starter Set, which includes everything you need for a group to play: a subset of the 5th Edition D&D rules, a starter module, a set of pre-rolled characters and dice. My youngest son had learned all of the rules watching Critical Role, so I lead the interactive story telling as the DM, asking him for the specific mechanics as needed.

Paper & Pencil

We had a blast. I can’t tell you how many hours of joy we got for $20 (although of course that lead to much more money spent shopping for the perfect dice, buying the full set of 5e rules, spell chards, etc.). However, we ran into two classes of problems with the paper & pencil play: 1) it only really works if you’re all together in the same room (my sons are normally spread between three cities) and 2) some logistical issues like progressive map reveal (aka fog of war”) and managing encounters (i.e. initiative, HP, AC, attacks, damage, etc.

Remote Players

The first issue we easily solved with Google Hangouts, which we tried both at the local Google office in a conference room with a dedicated video conferencing system (an option I realize isn’t available to everyone) and in my home office with a dedicated video conferencing computer, which I think is available to most anyone:

I happen to be using a dedicated device for Google Hangouts, but we’ve also used a separate laptop that worked as well.

RPG Logistics Software

The logistics problems were the same as when I played D&D in the 80s, but I had less patience for them now that I know the power of software. So, I immediately started designing a solution in my head (as I am wont to do). However, as much as I enjoy writing software, I wanted to get back to playing faster than that! After some research, I found two main applications built to solve my RPG problems: Roll20 and Fantasy Grounds.

Fantasy Grounds has a lot going for it and I know many people love it. However, there weren’t a lot of getting started” videos that I found inspiring and it required the local installation of a 32-bit client app. That seemed a little backwards to me. Also, it cost money just to get started.

On the other hand, Roll20 is web-based, has a ton of intro videos on YouTube and is free (of course, they have paid options, too)! The videos made it look great, so my local son and I tried a little mock battle. When that worked, we got the party together to try the free module that comes with Roll20. We lost an hour or so getting used to things, which is to be expected, but I swear we spent another whole hour trying the built-in video conferencing that comes with Roll20. In theory it’s great, as everyone shows up right on the game table, but in practice, it makes the whole app unstable as hell. I recommend turning it off completely and using a dedicated PC for video conferencing as we do.

Some of the Roll20 controls are clunky, e.g. having to switch between select and drag mode when interacting with maps, and there can be some data entry when it comes to picking spells and the like.

That said, we love Roll20. We’ve moved on from the free content (which ain’t great, but gave us the practice we needed) to the content you can purchase that’s been poured directly into the Roll20 format, including the maps, monsters, characters, NPCs, handouts, etc. My sons love to see the maps and where they are relative to the stuff in the room and the monsters. My favorite is being able to drag a monster or character onto the map, click on the Initiative” link on the monster/character sheet, see the dice roll in 3D (the value is captured in the Turn Tracker) and keep track of who’s turn it is with a simple button click. Then, on each person’s turn, they can roll to attack by clicking on their weapon or spell, automatically showing advantage/disadvantage rolls (if needed) and damage, even lighting up the scores in red or green for critical hits or fumbles (the latter of which I manage with the separate iFumble app). Also, the built in fog of war mode and DM layer on the maps means that I can reveal things to the players as appropriate.

Once you play with content specifically formatted for Roll20, you won’t want to go back. Pretty much all of the official 5e modules seem to be available, including the starter module which we bought again for $20 (and again, totally worth it). We’d already gotten about half-way through via paper and pencil, so our first session with real content was a great comparison. With the kinks worked out, we had a great time and even included my little brother who lives in a fourth city in another time zone.

Where are we?

Since starting again six months ago, I’ve played with three groups of players, all of whom are composed mainly of adults that have little or no D&D experience. Certainly 5e D&D captures the flavor of 2e that I grew up on (aka Advanced D&D), so there’s no trouble transitioning beyond learning some different rules. I do prefer the character sheet management provided by D&D Beyond (the official logistics software provided by Wizards of the Coast), but it doesn’t provide online play like Roll20 or Fantasy Grounds. My dream is that WotC purchase Roll20, drop it into D&D Beyond, polish it up and charge whatever they need to keep it running. I’d pay!

Of course, the main joy is the memories you build in the key moments of interactive story telling with friends and family. However you end up doing that, you can’t go wrong.

November 12, 2017

WebAssembly explodes client-side programming

WebAssembly explodes client-side programming

Back in the olden days, you could use whatever programming language you wanted to write your client-side code: ASM, C, C++, BASIC, Java, C#, etc. Hell, every language had a client-side UI framework attached (often more than one). Then the web hit and we used those same languages to generate HTML on the server-side. It was a polyglot world for client-side development right up until mobile made itself the #1 targeted format in the world (cue record scratching noise).

In a mobile/cloud world, the server is alive with programming languages, including Java, C#, Swift, JavaScript, Erlang, Clojure, Go and 100 others. But on the client-side, we’re relegated to just a handful:

  • Mobile Web: JavaScript (and its variants)
  • Mobile Native: C/C++, Java, Objective-C/Swift, C#

That’s pretty much it. As a C# guy personally interested in building a web app (specifically a Progressive Web App that works great on both desktop and mobile devices), I was pretty frustrated. I could write any language I wanted so long as it was JavaScript.

If you want, you could point out the large number of languages that transpile” to JavaScript, including some C# variants, but every one of those that I’ve tried has been a very leaky abstraction — you have to know that they are just JavaScript translators and write your code accordingly. It’s a very different experience than the multi-language support on the Java VM or .NET, where the VM provides a common environment for every language without any one language being special” (although you could argue that Java shines through into languages like Clojure and C# into F#, but not nearly to the same degree). Really, in the browser, JavaScript is the only first-class language, along with HTML being the only UI framework.

So, with PWAs on my mind, HTML in my heart and JavaScript at my fingers, I started looking into JavaScript frameworks. Unfortunately, they all of them left me wanting something much more like those old Microsoft UI frameworks, but for the web.

And then came Steve.

The mother of all web demos

In June of 2017, Steve Sanderson gave a talk entitled Web Apps can’t really do that, can they? in which he demonstrated the following:

This talk blew my mind. This was the browser-based full stack for .NET that I hadn’t even realized that I’d wanted. Steve’s demo made me realize that not only could Microsoft create an amazing end-to-end experience for .NET developers that wanted their code to fit into the modern web, but they could do it in a way that worked across all form factors and all platforms.

This demo wasn’t the end. Steve recently posted an update talking about all of the contributors he’s had since his demo, but also the move to the more full-featured Mono .NET implementation provided by Xamarin.

What’s coming

Anyone experienced with Microsoft sees a clear path sketched out by the TOC to any Windows-based UI framework book:

  • Easy onboarding with templates for Visual Studio [Code] to help developers quickly build .NET-based client-side and full-stack apps
  • Great tooling: Intellisense, Goto Definition, debugging, etc. will all be baked in
  • Components: out of the box components will be great and cover the common cases
  • Component Ecosystem: non-UI libraries and advanced UI use cases will be served by a 3rd party community (can you say Json.NET and Telerik”?)
  • Data Binding/State Management: Microsoft invented data binding for Visual Basic and Steve already showed this off during his demo
  • HTML Templates: ASP invented the inside-out model we think of as modern HTML templates and Steve has already shown Razor redone for .NET and the web
  • Reified classes: Silverlight did this thing were you could send .NET objects over the wire and use the same class implementation on both sides. Steve’s already shown how this can be done over the web using JSON and REST, which means that any language can play on either side. Do this with GraphQL and you’ll get querying only what you need and data change notifications via subscriptions.
  • et cetera

And as huge as this will be for the .NET community (can you say Silverlight without the mess”?), that’s not all.

WebAssembly is the key

Right now, Steve’s proof of concept .NET Runtime is a 4MB download for any .NET app. That’s clearly not going to work. As Steve mentions, there are lots of optimizations to do that’ll bring down the size, but the real optimization win is going to be when the next version of WebAssembly comes out, including two big features: plugging into the browser’s VM and access to the DOM. The WebAssembly working group are already discussing both of these (e.g. here’re the notes from a recent meeting discussing garbage collection).

With access to the browser’s VM, a .NET app won’t need to ship it’s own runtime for every app — it’ll just plug .NET objects into the one provided by the browser. Now the .NET runtime for the browser can be trimmed to a bridge to what the browser provides as well as the .NET Framework base libraries. Combine that with tree shaking” (removing the IL your code never uses), code splitting and on-demand loading and you’ve got a .NET WebAssembly app that even anyone on just their phone can love.

But it’s really the 2nd thing that the WebAssembly working group is enabling — access to the DOM — that enables a Cambrian explosion of client-side programming languages. WebAssembly already provides a language-independent calling layer enabling various languages to call not only between itself and JavaScript but also between every other language that compiles down to WebAssembly. Combine that with a standardized UI layer — the HTML DOM — and WebAssembly enables a universal client-side runtime for any language. What this means is that I no longer have to have the Angular, React and Vue bindings to Bootstrap: if someone provides the UI components for Bootstrap in any language that compiles down to WebAssembly, I can use it from any other language.

Just one more thing

And as amazing as WebAssembly is about to get for the client-side, I doubt this thing will stop there. NodeJS was invented to bring JavaScript and it’s underlying runtime to the server. Imagine doing the same thing for WebAssembly but for any language, server-side or client-side. Done right, WebAssembly has the potential to be what Java and .NET have both tried and failed to be — the universal runtime for all languages across all platforms.

But even if it doesn’t get that far, Steve has shown us that WebAssembly enables an amazing client-side experience. I can’t wait to get my hands on it.

November 11, 2017

Building a modern, mobile-first app

Building a modern, mobile-first app

A while ago, I started building a brand new client app. I wanted to use all of the modern techniques to make it a great app and to make sure it had maximum reach. For me, that meant a Progressive Web App that would work from desktop and mobile browsers. And so that meant JavaScript. That was OK; I’d been through the 5 stages of grief for JavaScript years ago but the real question is, which client-side framework do I choose? To answer that question, I spent a great deal of time with a LOT of them:

  • Angular: I was a huge AngularJS (v1) fan, but when the template for an Angular 2 project included as many files as for a J2EE project, I was out. Too much, too soon.
  • Polymer: great framework with great UI components and tools, but the bet on standardizing HTML Imports as the core reuse mechanism didn’t pay off. Instead, Polymer 3 seems to be heading down a put your HTML into a string” road, which I don’t like. Plus, the code I wrote in Polymer felt wrong somehow, particularly when it came to state management. This would become a theme.
  • React/Redux: I love writing React code. The rendering model really speaks to me. However, as soon as I wanted to share data between components, things broke down quickly. As much as I love React, I hate Redux. The model is nice but the code is insane. And the fact that there are dozens of other ways to manage state in React, fragmenting the community, is also insane. No thanks.
  • Lots o’ little libs: I spent some time going back to basics with HTML and Bootstrap and jQuery. And while I enjoyed picking up jQuery again and Bootstrap certainly met my UI needs, the lack of a virtual DOM or a standardized component model (Custom Elements still hasn’t been adopted widely enough and there’s a lot of boilerplate code) put me on a path of reinventing the solutions all of the other frameworks have already solved. Plus, still no great way to solve the state management problem.
  • Stencil: I really like the Stencil approach of pushing the common idioms (virtual-DOM-based rendering, JSX, class-based components) along with compile-time tooling to create HTML custom elements, but there aren’t any UI frameworks for it yet. I didn’t want my limited free time to start by wrapping Bootstrap in Stencil. According to a recent podcast, the next version of Ionic will come with a set of Stencil UI components — perhaps I’ll revisit it then.
  • VueJS: This is something I was looking into recently and it’s very promising. It’s simple, so it can be adopted easily, but full-featured so it can be used for real-world stuff. There’s a range of usage from include this JS file and write this code” to run this CLI and get a Webpack-based build with hot-loading”. There are plenty of UI frameworks, including for Bootstrap. Routing turned out to be way simpler than in React and state management looks like it will be sane. I wish it were class-based, but Vue is still the #1 contender.

Where are we?

Maybe it’s just how I was brought up, but I miss the single-vendor, single-lib, single-tool, single-way model from my Microsoft days. It wasn’t cross-platform or OSS, but WinForms was a great end-to-end solution for building Windows apps. Of course, I say all of this while still wanting a responsive app built using HTML and deployed via the browser that works everywhere for everyone. It’s a heterogeneous world and that’s a very good thing, but it does have it’s downsides, too.

Tell me what you think — did I mischaracterize your favorite JS framework or miss it altogether? I’d love to hear that there’s an end-to-end toolchain that’ll make me forget WinForms.


← Newer Entries Older Entries →