Photo: Emile Perron on Unsplash

Original address: medium.com/flutter-com…

Author: medium.com/@c.muehle18

Published: April 9, 2021 -12 minutes to read

Get a head start on Flutter Web! Today I want to tell you what I wish I had known about Flutter before I started using it. URL routing, bootstrapping, platform-specific compilation, runtime checking, responsive UI, and storage.

Before we get started

All of the examples used in this article were taken directly from my Caladrius repository on GitHub (link below). Caladrius should be a replacement for Fauxton — if none of this rings a bell, don’t worry. I’m a big fan of Apache CouchDB, and Fauxton is a web-based management tool that comes with CouchDB.

Github.com/Dev-Owl/Cal…

Github.com/apache/couc…

As Caladrius’s “Read Me” points out, our goal is to build a more complex example using Flutter Web. Of course, it should still run on mobile.

CORS

Photo: Kyle Glenn on Unsplash

The first point is not directly related to Flutter Web, but it may be encountered during development. As a quick explanation, CORS is short for Cross-Origin Resource Sharing and describes the different technologies (CSS, JS, Image, Cookies/Authentication, etc.) that browsers use to limit and define the ability to share resources between different domains. CORS policies are part of the HTTP header section (like all other headers, they are key-value pairs).

Developer.mozilla.org/en-US/docs/…

Now you may be asking yourself: “Why is this important to my Flutter web page?” Simply put, once you want to share resources or get resources from different domains, the code you’re used to (like getting images from websites) may not work.

If you perform HTTP(S) requests on your phone, the Flutter code doesn’t care about the CORS header at all. If you run the same code on the Flutter Web, it will throw an exception (if the associated CORS header exists).

To ensure that the site complies with the domain’s CORS configuration, your browser performs a check. You can’t change this (which is a good thing), but in the case where you can control other servers, you need to do the following.

  • Set the correct CORS header to enable your usage scenario.
  • Depending on the usage, you may need to add the domain name to the whitelist.

In cases where you have no control over external servers, you can still choose to set up a CORS proxy server. Therefore, take a look at the guide to Flutter – it describes some important insights into Flutter.

Flutter. Dev/docs/develo…

Please note: During the development of Caladrius, I had to enable my CouchDB to allow cross-origin authentication cookies. This will require you to set the specific domain in the Origin Domains, wildcard characters like * will not work!

Another point to note about authentication is that cookies can be transmitted as “Http Only “. If this is set by the server, your Flutter code cannot access the cookie information. Remember that your Flutter code is the JavaScript of the page.

Another very important lesson to learn about CORS and Flutter authentication (this time related to Flutter) is that you must set the “withCredentials “property of BrowserClient (your HTTP clients on the network) to true. If you want to run code in Web and App, you need to separate it before building (see platform code, Ahead section below).

Developer.mozilla.org/en-US/docs/…

Routing and deep linking

Photo: JJ Ying on Unsplash

The great thing about a website is that you can share a link with someone without any effort. If you point to something inside the app (directly to a blog post), this is called deep linking.

Fortunately, Flutter Web allows you to provide this service to users by using “named routes”. This is not new to Flutter Web. Before we talk about routing and generating links, I want to mention the URL strategy of Flutter Web.

Flutter. Dev/docs/develo…

The link above shows you how to set up the two modes.

  • # url, everything starts with example.com/#page/id/edit.
  • No #, example.com/page/id/edit.

Important: The link above also contains a key point (at the bottom) that you need to configure in the index.html file generated in your project if you plan not to host your application in the server root. You need to do this in the index.html file generated in your project.

Create your router

Now you have two options: define a named route in the associated map inside the MaterialApp widget, or create a router class.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Caladrius',
      initialRoute: 'dashboard',
      onGenerateRoute: AppRouter.generateRoute,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
   }
  
  Widget buildNamedRoute(BuildContext context){
     return MaterialApp(
         initialRoute: '/',
         routes: {
            '/': (context) => FirstScreen(),
            '/second': (context) => SecondScreen(), }, ); }}Copy the code

The example above shows how to register both ways — what you want to do is based on your use case. One important thing to know if you plan to use named routes, please note that “/second “as a named route does the following when a Flutter Web request is made.

  1. The named route is checked, and Flutter pushes the first page to the user who has signed up for /.
  2. Pian-pian now instantly pushes the page to the screen for instant likes
  3. Once on the client side, the navigation stack will have two elements/and a second

Remember, users can reload at any time, or open your page with any link — a big difference for your Flutter app on your phone.

This behavior may not be desirable, such as the lack of an option to ensure that the session exists in case “second “is a protected area in your application. The above disadvantages can be offset by using the onGenerateRoute attribute and configuring your router. The router’s job is to understand the current request (check the URL) and push the correct page to the user.

import 'package:caladrius/component/bootstrap/bootstrap.dart';
import 'package:caladrius/screens/corsHelp.dart';
import 'package:caladrius/screens/dashboard.dart';
import 'package:caladrius/component/bootstrap/CaladriusBootstrap.dart';
import 'package:caladrius/screens/database.dart';
import 'package:flutter/material.dart';

class AppRouter {
  //Create a root that ensures a login/session
  static PageRoute bootstrapRoute(BootCompleted call, RoutingData data) =>
      _FadeRoute(
        CaladriusBootstrap(call),
        data.fullRoute,
        data,
      );
  //Create a simple route no login before
  static PageRoute pageRoute(
    Widget child,
    RoutingData data,
  ) =>
      _FadeRoute(
        child,
        data.fullRoute,
        data,
      );

  static Route<dynamic> generateRoute(RouteSettings settings) {
    late RoutingData data;
    if (settings.name == null) {
      data = RoutingData.home(); //Default route to dashboard
    } else {
      data = (settings.name ?? ' ').getRoutingData; //route to url
    }
    //Only the first segment defines the route
    switch (data.route.first) {
      case 'cors':
        {
          return pageRoute(CorsHelp(), data);
        }
      case 'database':
        {
          //If the database part is missing -> Dashboard
          if (data.route.length == 1) {
            return _default(data);
          } else {
            returnbootstrapRoute(() => DatabaseView(), data); }}default:
        {
          //Fallback to the dashboard/login
          return _default(data); }}}static PageRoute _default(RoutingData data) {
    returnbootstrapRoute(() => Dashboard(), data); }}class RoutingData {
  @override
  int get hashCode => route.hashCode;

  final List<String> route;
  final Map<String.String> _queryParameters;

  String get fullRoute => Uri(
          pathSegments: route,
          queryParameters: _queryParameters.isEmpty ? null : _queryParameters)
      .toString();

  RoutingData(
    this.route,
    Map<String.String> queryParameters,
  ) : _queryParameters = queryParameters;

  //Our fallback to the dashboard
  RoutingData.home([this.route = const ['dashboard']]) : _queryParameters = {};

  String? operator[] (String key) => _queryParameters[key];
}

extension StringExtension on String {
  RoutingData get getRoutingData {
    final uri = Uri.parse(this);

    returnRoutingData( uri.pathSegments, uri.queryParameters, ); }}class _FadeRoute extends PageRouteBuilder {
  final Widget child;
  final String routeName;
  final RoutingData data;
  _FadeRoute(
    this.child,
    this.routeName,
    this.data,
  ) : super(
          settings: RouteSettings(
            name: routeName,
            arguments: data,
          ),
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              child,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              FadeTransition(
            opacity: animation,
            child: child,
          ),
        );
}
Copy the code

Don’t be overwhelmed by the above code, let me break it down for you.

  • AppRouter – Main class that generates routing data (information from URL/ named routes) and tells the application to open a page.
  • RoutingData – a data class that stores the current RoutingData, which contains the URL (if you use # mode, everything after #) and query parameters.
  • String extension method – a simple shortcut to generate a URL object from a string. The URL class provides convenient data extraction.
  • FadeRoute – a class that tells Flutter how to rehearse transitions between two routes, in this case using opacity to desalinise between two pages.

AppRouter also contains code to guide a deep link. In my use case, this ensured that the user was authenticated through CouchDB (keep in mind that my App is supposed to be an administrative interface) — more on that in the next section.

If we look at the main functionality of the AppRouter class generateRoute, its flow looks like this.

  1. Extract the current route data into our RoutingData object and, if not (empty URL), use a fallback of the default/home route.
  2. Thanks to the simplicity of RoutingData, it now uses the first part of the URL to run a switch-case statement to determine what we want to display.

My AppRouter uses something similar. NET MVC pattern, LET me show you a sample url.

Example.com/#database/userdb-123/document/helloworld?mode=edit.

The URL is split into a single piece, and in the RoutingData class, the first part is used to define the Page (or Controller). In the example above, it would be “database”. Everything after that can be used in the database interface to trigger further actions (in this case, open the database “userDB-123” and view the document HelloWorld in edit mode). Of course, you can easily change this URL handling by tweaking the generateRoute function.

Here are a few more important takeaways.

  • The RoutingData object has a getter(called fullRoute) for regenerating the URL. This is passed to the Flutter to display the URL after navigation (otherwise it will disappear).
  • RoutingData is passed to the route, and the widgets currently displayed can read all the information and act accordingly.
  • Reading RoutingData is easy, just call it in your build method.

final routingData = ModalRoute.of(context)! .settings.arguments as RoutingData;

  • All of the above works 100% the same on the phone as it does on the Web, with no need to change the code and one code base ruling everyone.

Boot and application life cycles

Photo: Gia Oris on Unsplash

Websites work a little differently from apps on your phone, but with Flutter Web you can still run the same code for both. Let me point out one thing directly: if you’re going to take any existing App and run it as a web page, it probably won’t work. Flutter is a tool, not magic! Applications must be designed to support this multi-platform scenario.

Lifecycle management is a big difference — in your App, your users can’t hit the magic reload button on any screen at any time, but on a web page, it’s easy to do. If a web page reloads, the previous context and runtime information is lost. Sure, there are ways to save information (cookies, IndexDB, etc.), but the state is gone.

This can be a challenge if you don’t build this into your application from the start. Now that you’re aware, you can build something to make the user experience flow. Let me introduce my Bootstrapper Widget.

import 'package:caladrius/main.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

typedef BootCompleted = Widget Function(a);class BootStrap extends StatefulWidget {
  final List<BootstrapStep> steps;
  final int currentIndex;
  final BootCompleted bootCompleted;

  const BootStrap(
    this.steps,
    this.bootCompleted, {
    Key? key,
    this.currentIndex = 0,}) :super(key: key);

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

class _BootStrapState extends State<BootStrap> implements BootstrapController {
  late int currentIndex;
  bool bootRunning = true;

  @override
  void initState() {
    super.initState();
    currentIndex = widget.currentIndex;
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<Widget>(
        stream: work(),
        builder: (c, snap) {
          if (snap.hasData) {
            returnsnap.data! ; }return Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        });
  }

  Stream<Widget> work() async* {
    while(bootRunning && ! (await widget.steps[currentIndex].stepRequired(preferences))) {
      if (currentIndex + 1 < widget.steps.length) {
        currentIndex++;
      } else {
        bootRunning = false; }}if (bootRunning) {
      yield widget.steps[currentIndex].buildStep(this);
    } else {
      yieldwidget.bootCompleted(); }}@override
  void procced() {
    if (currentIndex + 1 < widget.steps.length) {
      setState(() {
        currentIndex++;
      });
    } else {
      setState(() {
        bootRunning = false; }); }}@override
  void stepback() {
    if (currentIndex > 0) {
      setState(() {
        currentIndex--;
      });
    } else {
      setState(() {
        bootRunning = false; }); }}}abstract class BootstrapStep {
  const BootstrapStep();
  Future<bool> stepRequired(SharedPreferences prefs);
  Widget buildStep(BootstrapController controller);
}

abstract class BootstrapController {
  void procced();
  void stepback();
}
Copy the code

Looking up from the list above, it’s all you need to start booting your application (again, of course, on any platform).

Bootstrap Widget

Like most of the parts you work on in Flutter, the Bootstrap component is a Widget. The BootstrapStep progress is established by the BootstrapStep object, and the list of steps must be passed to the Widget. In addition to these steps, you also need to provide a BootCompleted callback. As the name implies, this callback will be triggered once all of your steps are complete, so you’ll need to provide another Widget.

Each step consists of a check — if this step is required (please note that I use the great SharedPreferences package, WHICH I hand over to the stepRequired function, but it is not a required dependency and can be removed for the guidance of this step) and a function that returns the Widget for this step.

The internal work of the Bootstrap Widget is done by the StreamBuilder. In case one of these stepRequired functions needs a bit of time, it just displays a loaded spinner.

Once all the steps are complete (or no longer needed), the BootCompleted callback is executed and the relevant widgets are displayed to the user. In addition to the above methods, you can manually change the steps by calling related functions, which are implemented through the BootstrapController passed to the step Widget. By default, the process will start at index 0 (from your step list), but you can change it if you want.

Running code

class CaladriusBootstrap extends StatelessWidget {
  final BootCompleted bootCompleted;

  const CaladriusBootstrap(this.bootCompleted, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    returnBootStrap( [ LoginBootStep(), ], bootCompleted, ); }}Copy the code

That’s all I need to use the Widget — the LoginBootStep you see above checks for something relevant to my application. If I need something later, I can just add a new step and all the code flows through it without me changing anything else.

Platform code, in advance

A code base is great and allows you to quickly extend or fix your application. Less code also means less maintenance, and as your application grows, so does the maintenance time (someone said, technical debt…). .

However, sometimes it is necessary to have a section of code that only compiles on selected platforms. Why do you say that? Because if you try to compile on the “wrong” platform, they will fail.

Sound complicated?

Actually using Flutter is very straightforward. I’ll show you everything based on an example used in Caladrius.

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

http.Client getClient() {
  throw UnimplementedError('Unsupported');
}
Copy the code

Gist.github.com/Dev-Owl/f1d…

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

http.Client getClient() {
  return http.Client();
}
Copy the code

Gist.github.com/Dev-Owl/f1d…

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

http.Client getClient() {
  final client = http.Client();
  (client as BrowserClient).withCredentials = true;
  return client;
}
Copy the code

Gist.github.com/Dev-Owl/f1d…

import 'pillowHttp/pillowHttp_stub.dart'
    if (dart.library.io) 'pillowHttp/pillowHttp_app.dart'
    if (dart.library.html) 'pillowHttp/pillowHttp_web.dart';

//Further code ...
Copy the code

Gist.github.com/Dev-Owl/f1d…

The first part shows you stubs, empty implementations that simply define the structure of the platform-specific classes or functions you want to compile.

The second and third parts show the relevant platform implementations. In my case, I want to make sure that CORS authentication is enabled in the browser (yes, you need to set this before running the request, which is OFF by default).

The last section tells you how to include the file to use it, and you can use the if condition to do the import. For the code, use the function getClient, and it doesn’t matter what platform you fill in the function. In case your target is not overridden by if, you will encounter an UnimplementedError in the Stub section (section 1).

That’s all you need to do, compile into the code for the target platform. Every once in a while, you also want to do the same checks at runtime, which is what the next section covers.

Platform switch at run time

In some cases, you want your code to behave differently (such as rendering different widgets or when it comes to persistent storage). The easiest way I’ve found to check if you’re running on the network is by including the following line.

import ‘package:flutter/foundation.dart’ show kIsWeb;

Now you can simply check if kIsWeb is true and know that you are currently running on the network. That’s it, one simple sentence 🙂

Responsive UI

Photo: Harpal Singh on Unsplash

Not directly related to Flutter Web, but still important! The UI of an application should adjust its layout and behavior to suit the user’s screen and platform as well as the platform’s expectations.

Typically, mobile apps follow the same process; You have a list or menu in which the user selects an element. Based on this selection, another screen is displayed, for example.

  1. You open your mail app, and you see your inbox.
  2. In the mailing list, you select a message to open it.
  3. The screen switches to mail details and your list is in the navigation stack.

On a larger screen (which, as I said, can be tablet directly, not web-related), you can have a main widget (mailing list) and a detail section (mailing content) on the side. Now, instead of switching screens, the details are updated once the user selects something in the main widget.

Which widgets come in handy?

First, you can simply run a MediaQuery and get the screen size. If you want to stop writing the same if block every time (again, note the technical debt), you can abstract the code into an extension (or static method).

import 'package:flutter/widgets.dart';

extension ViewMode on Widget {
  bool renderMobileMode(BuildContext context) {
    return MediaQuery.of(context).size.width < 600; }}Copy the code

If the screen is below 600DP, my widgets will follow the moving path and show you a different layout.

Using this approach also forces you to separate your widgets; Otherwise you’ll have to write a lot of repetitive code. You can also use LayoutBuilder to handle the size of your parent Widget and work in the Widget tree at any time.

API. Flutter. Dev/flutter/wid…

There are also some pre-made packages on pub.dev that let you do more complex things like my little example above (haven’t tried it yet, as I’m happy with my simple check right now).

Pub. Dev/packages/re…

Permanent storage

Photo: Steve Johnson on Unsplash

A web page’s storage space is limited, you can’t store much data. You don’t have a file system, so you can’t write and read as much as you want. Still, you can store data, and you can even store it on both platforms with 100% of the same code.

User Settings and preferences

Use the following package, which is very simple, depending on your platform, to store the data in an SQLite database or local store (key-value store).

More data?

If you need more data or want to run queries, I would recommend Moor.

Pub. Dev/packages/mo…

Network-related documentation can be found here.

moor.simonbinder.eu/web/

conclusion

Flutter Web is an important step towards letting you do more with the same code. Still, it requires some solid thinking up front. During development, you always have to ask yourself what might happen on a particular target platform, and if you need to build something to take that into account.

From my perspective, working with Flutter on multiple platforms simultaneously was rough at first, but with the learning offered here, I felt confident to move forward with my small project. Who knows, maybe I’ll have to do a second round of “Things to know before Flutter enters”.


www.deepl.com translation