More than a Flutter Web App, is a full Flutter WebSite!

Mariano Zorrilla
Flutter Community
Published in
9 min readJun 21, 2019

Tap Hero Flutter Web: https://gametaphero.web.app

This journey started with my Flutter Game for mobile, which added Desktop support and then, Flutter Web support (so excited!).

Flutter Web is still over baby steps but moving so fast that I could actually port my game with small little changes.

The first thing you’ll notice is the “flutter_web” packages, those are needed to be changed from any mobile/desktop code.

Next step, you’ll notice two main.dart files. One is your good old one, and the other is necessary to make the Web magic run as we need.

Let’s get starter!

The first “issue” I had, having a website, is “how to move from one path to the other?”. I used:

Navigator.of(context).push(MaterialPageRoute(builder: (_) => Game()));

Nothing, new… but I needed to update the URL and the first solution came with the amazing dart:html library:

import 'dart:html' as html;@override
void initState() {
html.window.history.pushState("", "Game", "/game");
super.initState();
}

I can even move back and forward through the history with:

html.window.history.back();
html.window.history.forward();

That’s great! Now my Web App looks more like a Website! My current url:

https://gametaphero.web.app/game

But! (there’s always a but…), if you go directly to that URL, the website doesn’t open, you’ll get the terrible 404 screen 😩😩😩

First Solution!

If the websites read the converted web/main.dart into my index.html one… could I have more files? 🤔🤔🤔

I decided to create initgame.dart, web/game.dart and game.html files:

// initgame.dartvoid main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Game'),
home: Game(),
);
}
}

The new file to be “transformed” into Js:

// web/game.dart
import 'package:mygame/initgame.dart' as game;
main() async {
await ui.webOnlyInitializePlatform();
game.main();
}

And the game.html to point into that one:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game On!</title>
<script defer src="game.dart.js" type="application/javascript"></script>
</head>
</html>

Now, we run the code again and voila!

Remember to update the pushState method to have the correct URL:

html.window.history.pushState("", "Game", "/game.html");

You can press: cmd + R or F5 all you want, it works! IT WORKS! 🎉🎊 You can go either from:

http://gametaphero.web.app and click “TAP TO START”

or you can skip all that and play directly from:

https://gametaphero.web.app/game.html

Does what we need and is one step closer to our Flutter Website… but that’s a lot of files! Imagine if we have a /support, and /privacy, and /download, and, and, and 🙃🙃🙃 With just 4 paths we’ll have 12 files ONLY to allow users to move directly to a specific screen. Should be a better way to do it…

Second Solution!

Alright… if we’re reading an html file, it should be a way to read the content of it, right? RIGHT?! (first-time web developer intensifies)

What if we had like a tag or something inside the .html, passing that value as a parameter and then do the logic… like deep linking, you know.

First thing first, let’s delete everything! Wait, not everything… only the initgame.dart and the web/game.dart files.

(Ptssss: never delete files before making sure your new logic actually works, your current logic is always the “best” logic).

We need to make a small change inside our game.html one:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game On!</title>
<script id="route">game</script>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
</html>

I’d added a small text string <script id=”route”>game</script> and now we’re back into our main.dart.js file.

So, if you go into https://gametaphero.web.app/game.html you’ll be redirected to your web/main.dart file, then into your main.dart and being able to read the text inside the new script tag. How to do it, you may ask:

import 'dart:html' as html;
import 'dart:js' as js; // only if you want to do Js logic
void main() {
var routePath = "/";

var route = html.window.document.getElementById("route");
if (route != null) {
routePath += route.innerHtml;
}

// js.context.callMethod('alert', <String>[routePath]);
// this method will show an alert over your browser,
// delete it if your logic works.
runApp(MyApp(routePath));
}
class MyApp extends StatelessWidget {

final String route;

MyApp(this.route);
@override
Widget build(BuildContext context) {
return ...;
}
}

Now you can apply a “deep linking” logic inside your main.dart build method:

Widget page() {
switch (route) {
case "/": return Home();
case "/game": return Game();
case "/download": return Download();
// more cases and screens as you need!
default: return Home(); // to be double sure
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: page(),
);
}

Save, RUN, cross your fingers, don’t wait (because Flutter builds so fast 🔥) and:

IT WORKS!!! 🎉🎊🔥

We literally saved lots of work with this logic! No need to create multiple dart files, all we need to do is one extra .html file per screen and that’s it! My dream to build a full Website using a “mobile developer logic” is one step closer to reality! ❤️

Best Solution (so far)

Being super excited, I jumped into Flutter Study Group Zoom call (Every Wednesday 24hs or Q&A) and Simon was there. Being a superhuman being of knowledge, he knew a better way to tackle this drama and not even having .html per path.

We need a bit more of work but, after that, new pages will be A,B, C-simple (Tpum tssssss 🥁)

onGenerateRoute

Let’s go back to our main.dart file, we need to make some changes but we ended up with a similar solution as the one before:

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) {
switch(settings.name) {
case "/": return Welcome.route();
case "/game": return Game.route();
case "/privacy": return Privacy.route();
case "/support": return Support.route();
case "/download": return Download.route();
default: return Welcome.route();
}
},
initialRoute: "/",
);
}
}

You may ask what the .route(); the method is all about… Is just a static method that builds a route for you. On every StatefulWidget you’ll need to add the following:

static Route<dynamic> route() {
return SimpleRoute(
name: '/',
title: 'Tap Hero',
builder: (_) => Welcome(),
);
}

For my game, will be something like this:

static Route<dynamic> route() {
return SimpleRoute(
name: '/game',
title: 'Game On',
builder: (_) => Game(),
);
}

Those hold a similar logic to our path and title name but adding the Route logic of Flutter.

class SimpleRoute extends PageRoute {
SimpleRoute({
@required String name,
@required this.title,
@required this.builder,
}) : super(settings: RouteSettings(
name: name,
));

final String title;
final WidgetBuilder builder;

@override
Color get barrierColor => null;

@override
String get barrierLabel => null;

@override
bool get maintainState => true;

@override
Duration get transitionDuration => Duration(milliseconds: 0);

@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return Title(
title: this.title,
color: Theme.of(context).primaryColor,
child: builder(context),
);
}
}

Pro Tip!

If you want to have an animated Fade Animation, you could do something like this:

class FadeRoute extends PageRoute {
FadeRoute({
@required String name,
@required this.title,
@required this.builder,
}) : super(settings: RouteSettings(
name: name,
));

final String title;
final WidgetBuilder builder;

@override
Color get barrierColor => null;

@override
String get barrierLabel => null;

@override
bool get maintainState => true;

@override
Duration get transitionDuration => Duration(milliseconds: 500);

@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return Title(
title: this.title,
color: Theme.of(context).primaryColor,
child: builder(context),
);
}

@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}

You’re all set! Congratulation! You don’t need the .html file anymore, the routes will take that task for you ❤️❤️❤️

Your turn

This is now a challenge for you! Build your next website (or first one) using Flutter Web:

https://flutter.dev/web

I needed all this to add a Privacy Policy for my Flutter Game, without it, the Play Store and App Store will reject in one second. So, if a small problem like that one sparked my curiosity to build an entire Website… I hope that fire gets into you as well 🔥🔥🔥

Pro Tip 2!

You want “free” hosting for your Flutter Website?! I have a small list for you:

1- https://surge.sh/
2-
https://firebase.google.com/
3-
https://codemagic.io/

The amazing guys of CodeMagic have their own CI/CD implementation for Flutter Web for you to host your new amazing Flutter Website!

Pro Tip 3!

You can actually detect the platform/userAgent of your users and recommend them to download the full Flutter experience for mobile!

class Platform {
var _iOS = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];

bool isIOS() {
var matches = false;
_iOS.forEach((name) {
if (html.window.navigator.platform.contains(name) || html.window.navigator.userAgent.contains(name)) {
matches = true;
}
});
return matches;
}

bool isAndroid() =>
html.window.navigator.platform == "Android" || html.window.navigator.userAgent.contains("Android");

bool isMobile() => isAndroid() || isIOS();

String name() {
var name = "";
if (isAndroid()) {
name = "Android";
} else if (isIOS()) {
name = "iOS";
}
return name;
}

void openStore() {
if (isAndroid()) {
html.window.location.href = "your Play Store URL";
} else {
html.window.location.href = "Your App Store URL"
}
}
}

If your users are mobile, use isMobile(); as a flag to show a label, button or similar. Use name(); to display a message like:

“Download the App for Android”

and, finally, use openStore(); when the user clicks on your button, will be redirected to the specific platform Store.

Pro Tip 4!

It’s a Website! You can customize your entire experience to make it better and even more beautiful:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="description" content="Tap Hero">
<meta name="keywords" content="Flutter, Tap, Hero, Game">
<meta name="author" content="Mariano Zorrilla">
<meta name="theme-color" content="#6200EA" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" sizes="57x57" href="icons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="icons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="icons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="icons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="icons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="icons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="icons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="icons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="icons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="manifest" href="icons/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="icons/ms-icon-144x144.png">
<title>Tap Hero</title>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>

You can add icons, theme color (status bar for Chrome), description, title, etc!

Open Source

This project when Open Source to help the community and to have more Flutter Web developers! Show some love and take a look at the repo:

LINK: https://github.com/mkiisoft/taphero_web

--

--

Mariano Zorrilla
Flutter Community

GDE Flutter — Tech Lead — Engineer Manager | Flutter | Android @ Venmo