Flutter for Web – Migrate Your Mobile App + Basics

Flutter for web is finally here and I'm sure that all the developers who've played with Flutter are very excited to see it finally reach the light of the world. While Flutter for web is still in a technical preview, which brings a couple of caveats, that doesn't stop you from learning how to work with it.

You're going to learn how to set up your machine to allow for Flutter web development, how to create a new Flutter web project, and also, how to migrate an existing mobile Flutter project to support the web.

Update your IDE & extensions

To make Flutter for web work, your Flutter version must be at least 1.5.4. Also, be sure to update VS Code / IntelliJ editors together with the Dart & Flutter extensions. The new versions provide simplifications for Flutter web development.

Install new packages

Web development requires the webdev package. If you want to create a new web project using a template, you'll also need the stagehand package. You will use these packages to run some commands manually or the IDE will run them on your behalf. 

First way of installing (the simplest for a Flutter dev)

Open the terminal and run

flutter packages pub global activate webdev

flutter packages pub global activate stagehand

This will add the packages into <your Flutter installation folder>\.pub-cache\global_packages

To run webdev and stagehand commands directly from the terminal, you have to add the following into your PATH environment variable

<your Flutter installation folder>\.pub-cache\bin

The issue with the first way

Even though you've run the above commands correctly, when you try to use commands like webdev serve or stagehand flutter-web-preview, you might get an error saying that such commands cannot be found.

If you try to use the Flutter plugin in VS Code (more on that below), you might get the following error messages:

Should something like this happen, I recommend you to use the second approach - install Dart SDK separately.

If, for some reason, you prefer not to install Dart SDK separately, you can always prefix the commands with flutter packages pub global run.

This way, you'll be able to use the packages activated the "first way". The command webdev serve will turn into flutter packages pub global run webdev serve.

Second way of installing (separate Dart SDK)

If you have the issues described above, you may want to take this second approach.

First, install Dart SDK for your respective platform using a terminal or an installer. Make sure that the following is in your PATH environment variable:

<your Dart SDK installation folder>\bin

Now, activate webdev and stagehand for the whole of Dart SDK by running:

pub global activate webdev

pub global activate stagehand

This way, you'll be 100% able to use the VS Code/IntelliJ extensions and run commands without stupidly long prefixes.

Technical preview specifics

Web development with Flutter is currently only in a technical preview. The people over at Google have forked the "official" Flutter repository into a separately developed flutter_web repo to make that if they break things, only the flutter_web repository will be affected.

What this means for you, is that apart from some web-specific setup, in Flutter for web, you cannot *YET* use the default widgets from package:flutter. Instead, there's a package:flutter_web.

Don't worry though as Google is actively working to merge flutter_web into the main flutter repo.

Creating a new Flutter web project

There are multiple ways to create a Flutter web project:

  • run stagehand flutter-web-preview in your desired folder
  • use VS Code Flutter extension
  • use an IntelliJ Flutter plugin

This tutorial will take you through the VS Code experience but it shouldn't make a big difference if you're using another editor. Just make sure that everything is up-to-date. The older versions of plugins and Flutter SDK simply cannot work with the web, as described in the first section above.

In VS Code, open the command palette (Ctrl + Shift + P) and run Flutter: New Web Project.

New Web Project command in VS Code

This will run the  stagehand flutter-web-preview command for you and you'll be presented with a project with a familiar, yet a bit different structure which is specific to the web.

Structure of a Flutter web project

Of course, in the near future it will be possible to develop web apps and mobile apps in the same Flutter project, but because of the unstable nature of a technical preview, you shouldn't really mix mobile and web development just yet.

Instead of the android and ios folders, there's now a web folder which is pretty lean, since all the web needs is the index.html and some Javascript... But, where is the JS file to start up the application?

Well, because Dart can be transpiled to plain Javascript, the main.dart file in the web folder is the one to start up the app in the browser.

The other main.dart file, the one inside the lib folder, holds the Flutter code that you're interested in. The generated code (excluding the comments) looks like the one below. The best thing about it is, that it's almost the same as classic Flutter code. There's just one difference. Will you spot it?

lib/main.dart

import 'package:flutter_web/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Hello, World!',
            ),
          ],
        ),
      ),
    );
  }
}

I've already described the difference a few paragraphs above. For the time being (until it's out of the technical preview), you don't import Flutter packages from package:flutter, but instead, you use package:flutter_web.

Running the default project

Launching a Flutter web project is the same as with a mobile project. However, while native mobile must run on an Android or iOS emulator, web projects obviously run in a browser.

While the finished & built app can run just fine on any browser (Chrome, Firefox, Safari), development is currently supported only on Chrome.

In VS Code, start debugging by hitting F5. This will automatically open a new development browser window.

As of now, Flutter for web supports only hot restart (not hot reload).
If you make changes to the UI, hit Ctrl + Shift + F5 to see them.

Migrating mobile projects to the web

Can you actually run your mobile Flutter projects on the web? Absolutely!

Until Flutter for web is not deemed stable, you cannot yet develop for the web and mobile simultaneously in a single project. The way that you can migrate your mobile apps to the web is simply to copy and paste all the files which you have in the lib folder.

Then just change the imports from package:flutter to package:flutter_web.

There are still some limitations when it comes to Flutter web projects. For example, Icons are not yet being displayed. However, most of the core functionality already works as you'd expect.

The following code is taken from a tutorial on routes in Flutter. Check it out, if you want to learn more about routing.

While it's not groundbreakingly complex, it's definitely not a "hello world" app. The project has two routes to navigate between, those routes exchange data, and there's a custom logic for navigation.

The only thing that needed to change in the code were the aforementioned import statements.

main.dart

import 'package:flutter_web/material.dart';

import 'route_generator.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // Initially display FirstPage
      initialRoute: '/',
      onGenerateRoute: RouteGenerator.generateRoute,
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Routing App'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text(
              'First Page',
              style: TextStyle(fontSize: 50),
            ),
            RaisedButton(
              child: Text('Go to second'),
              onPressed: () {
                Navigator.of(context).pushNamed(
                  '/second',
                  arguments: 'Hello from the first page!',
                );
              },
            )
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  // This is a String for the sake of an example.
  // You can use any type you want.
  final String data;

  SecondPage({
    Key key,
    @required this.data,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Routing App'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text(
              'Second Page',
              style: TextStyle(fontSize: 50),
            ),
            Text(
              data,
              style: TextStyle(fontSize: 20),
            ),
          ],
        ),
      ),
    );
  }
}

route_generator.dart

import 'package:flutter_web/material.dart';

import 'main.dart';

class RouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    // Getting arguments passed in while calling Navigator.pushNamed
    final args = settings.arguments;

    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => FirstPage());
      case '/second':
        // Validation of correct data type
        if (args is String) {
          return MaterialPageRoute(
            builder: (_) => SecondPage(
                  data: args,
                ),
          );
        }
        // If args is not of the correct type, return an error page.
        // You can also throw an exception while in development.
        return _errorRoute();
      default:
        // If there is no such named route in the switch statement, e.g. /third
        return _errorRoute();
    }
  }

  static Route<dynamic> _errorRoute() {
    return MaterialPageRoute(builder: (_) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Error'),
        ),
        body: Center(
          child: Text('ERROR'),
        ),
      );
    });
  }
}

Now you can launch the app and while not everything will look perfectly (Icons are not yet functional on the web), and also the route switching is not yet reflected in the URL, the app runs smoothly.

Conclusion

Flutter for the web is still evolving and while it's not yet ready for production, you can definitely feel that it's a game changer. Writing native mobile apps and web apps in one codebase without using HTML and CSS is something no other performant framework has accomplished. The future is surely bright!

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>