Dynamic HTML Elements — An Approach to Flavors in Flutter Web
By: Gonçalo Palma
Tags: Web, Flutter Web, Dart, Flutter

Photo by David Vig on Unsplash

When we are developing mobile Flutter applications, we know we can make use of flavors. Flavors allow us to handle different configurations for different environments — development, staging, production — adding custom assets and settings to each version of the app.

However, we don’t have the same in Flutter for Web. And sometimes it poses a problem — how can we create different versions of the same web application using a system like flavors? For example, how can we have different keys for JS libraries like Google Maps or have different Firebase configurations initialised in our Index.html file?

The Problem — Setting up Google Maps

In this article we are going to explore how we can add these configurations dynamically and per environment.

In recent months, the Flutter team has been working on creating Flutter Web implementations of their most popular plugins, such as url_launcher, google_sign_in and google_maps_flutter. In the case of both google_sign_in and google_maps_flutter there's a small catch - we need to add to our index.html file a new line where we add the correct API key:

This might be fine if we are creating a prototype application or if we want that our API key is shared between the different environments. But what if we want to have different keys?

The First Solution — Multiple Index.html Files

One of the possible solutions to our problem is to create several index.html files that reflect our environments: index-dev.html , index-stg.html , index-prd.html . In each of these files we could set up the configurations specific to each environment.

Then, in order to run the correct configuration, all we had to do is to copy the contents from the correct environment into index.html and voilá!

However, we must stop and reflect — does this solution fix our problem? Or does it originate additional problems down the line?

By creating multiple files, we are triplicating the effort — if tomorrow we need to add a new library, all three files need to be updated. Or if a key needs to be changed, we need to change it only in the necessary files.

On the other hand, this relies on our memory to always change the files when running or deploying our app or to create tools that enable us to do this process. The first approach might lead to incorrect configurations being pushed to production while the later need us to expend some time to create the tools.

Ultimately, if having different configuration files for the index.html is cumbersome what can we do? We can try to load the configurations dynamically via Dart.

Loading Configurations Dynamically with Dart

As stated in previous articles, long before Flutter adopted Dart, Dart was a proposed solution for JavaScript in Web. This means that there are already some out-of-the-box solutions to communicate with JavaScript (see Using JavaScript in Flutter Web), and potentially with HTML, which is precisely what we need for our situation.

But first let us analyze our problem again. As described in the documentation for google maps, we need to add a:

script element to our index.html file

element to our file This element will be in the document’s head

Inside it we will add a src with the Google Maps API and the correct API Key.

Creating a Custom HTML element with Dart

From the official documentation on Adding an Element to the DOM Tree we see that it is possible to create different HTML elements to be inserted in the DOM (Document Object Model) since Dart already provide them out-of-the-box.

In our case, we want to create a <script> element, and conveniently we have in Dart the ScriptElement that represents this object. Which means that the first thing that we need to do is to create a new instance of this object:

With the element created, we can now access and modify its properties, such as src and id :

Since we want our element to be added in the <head> , we need now to be able to access it, and we can do so with the document variable. Then, we can simply access the head and append our element:

We can now add this function to our main function, so that it runs before our app starts:

When running our app, we will be able to look at the Developer Tools and search the element tree for the super-script :

Added <script> element, highlighted in blue

Loading Our Configurations Dynamically

As said before, Flutter Web does not support flavours yet, which means that it won’t be possible to do the following:

At the end of the day, what we want is a convenient method of passing from the command line (or IDE) to our app the environment that we are currently using, without having to change our code.

With flutter run help -v we can see the list of all available arguments to run our app and we will find the following:

Which means that if we use:

We will be able to retrieve the flavor in our Dart code:

Additionally, we can use the parameter defaultValue to set dev as a default:

With this, we can now create two different flavors — dev and prod . For each, we will create a new key:

Why are we using const arguments? Because as we can see from Antonello Galipò's article Excluding Dart code from the release compiled executable, if we follow this approach our dev keys won't be compiled in the production release code, and our prod keys won't be compiled in our development release.

Finally, we can combine this with the creation of our ScriptElement to retrieve the correct environment key:

Conclusion

Já está! 🎊

With this approach we are:

Creating <script> elements dynamically;

elements dynamically; Storing all our keys and configurations in Dart, making it the de-facto source of truth for all our API keys

Using the command line argument --dart-define to change environments on the go:

We could expand the use of -dart-define to not only store the keys that we use in the index.html file but also all other keys and configurations needed for our code.

On the other hand, we could also explore adding all the necessary script lines for libraries such as firebase analytics via this method, or even the firebase configurations.

In the end, this let us more and more centralize our source of truth in Dart, without the need of using separate files. This makes it easier for us and for other to understand our code and easier to change all the keys in the future when necessary.

To see the full code for this article, check the following repository: