Flutter Web. Part 1

Hi, my name is Maxim, I am a Flutter developer at Surf.

Flutter allows you to assemble a single code base not only into mobile and desktop applications, but also into web applications. But how does Flutter Web work and are there any special features of interaction with the platform? We will figure this out in a series of articles. And this is the first one.

Why Flutter Web is needed

Flutter Web is not a replacement for Html/Css/Js. It is absolutely not suitable for creating classic websites. Blogs, portfolios, bulletin boards, landing pages — Flutter will not show itself there.

Of course, all this can be done, but the price will be too high. This includes the impressive weight of the finished application, and non-native behavior for the web environment, and the lack of SEO, and high requirements for the client's performance.

At the same time, Flutter is a great tool for developing web applications.

For example, if you are a bank and you get kicked out of the app store.

Or a messenger, and want to be available to clients not only on mobile devices, but also integrated into CRM web pages?

Or are you a cloud vector editor and want to give users the ability to work without installation, on any OS and from anywhere in the world directly in the browser?

We will not consider the specifics of developing applications on Flutter – they are no different from developing for mobile OS. We will focus on interaction with the platform. In our case, this is a web browser and the native language for this environment – JavaScript.

Browser API

Dart was planned as a js “killer”, but something went wrong.

It didn't catch on in the community, and a special version of Google Chrome with native support for Dart as the main language eventually evolved into Flutter.

This shady past still serves Flutter well and allows you to directly call Browser APIThere is a package for these purposes. webwhich in Dart 3.4 replaced a whole pool of plugins built into dart-sdk:

All of these packages are marked as deprecated, and their support will soon be limited. And eventually they will be removed from the Dart SDK.

Browser API — a powerful tool that allows us to interact with the browser. In a sense, when developing mobile applications, we can perceive it as an Android/IOS SDK.

It allows access to cache storage, Indexed DB, address bar, microphone, webcam. Learn more about Browser API capabilities here

Calling the Browser API from Dart

Let's get started with Flutter Web. To do this, let's add the package of the same name to our project.

dart pub add web

We import the dependency into the file.

import 'package:web/web.dart' as html;

The HTML prefix is ​​used to avoid name conflicts with other packages and built-in language features. The simplest thing we can do is to find out User-Agent user and determine the web browser from which he uses our application.

Text('Browser: ${userAgent2Browser(html.window.navigator.userAgent)}')

So we get the object Window from the web package, using the html prefix.

From Navigator we get User-Agent and we get the browser name from it using the userAgent2Browser method, which we implemented ourselves.

Simple, isn't it?

Simple, isn't it?

URL launcher

Now it's more complicated. Let's open an external link in the next tab. To implement an analogue of the package url_launcher in the browser, it is enough to access the Window object and pass the required web address

 html.window.open('https://www.google.com', 'Google')

Note that the method open() allows you to transfer parameters to it and open a new tab not just as a tab, but as a separate window, and even set the size or position.

html.window.open('https://www.google.com', 'Google', 'left=100, top=100, width=500, height=300, 'popup');

Alert

An equally classic example of a simple use of the Browser API is displaying a dialog.

final isAccess = html.window.confirm("Open Google?");

if (isAccess) {
  html.window.open("https://www.google.com", "Google");
} else {
  html.window.alert("=(");
}

These examples show that interacting with the browser from Dart is quite simple.

But this approach covers only the basic capabilities. If we want to do something more interesting and complex, we cannot do without the package dart:js_interop

JavaScript interoperability

JavaScript interoperability — is a mechanism that ensures compatibility between Dart and JavaScript. It allows the two languages ​​to exchange data, call each other's methods directly and through additional wrappers and interfaces.

This mechanism is provided by the package dart:js_interopwhich is already included in the Dart SDK. To use it, just add its import.

import 'dart:js_interop';

This package contains many objects and methods that help build interaction between js and dart.

The dart:js_interop and dart:js_interop_unsafe packages replace package:js, dart:js, and dart:js_util in Dart 3.4.

These are the ones that developers use today. They allow you not only to interact with the js layer, but also to prepare a web application for compilation to WebAssembly.

Old packages have limited support. And there is a chance that they will be removed in new versions of the language. So if you use these packages, now is the time to think about migrating. Who wants problems in the future?

Calling Js from Dart

Let's start with a familiar method window.alert()

// Указываем название JS-метода
// @JS('window.alert')
// на самом деле вызов можно упростить и вызывать alert
// на прямую, браузер автоматически ищет методы и объекты в window

@JS('alert')
// Указываем что метод  внешний
// и объявляем входные параметры и возвращаемый тип
external void showAlert(String message);

...
ElevatedButton(
      onPressed: () {
        // используем так, будто это метод dart
        showAlert("Hello from dart");
      },

      child: const Text("alert"),
    ),
...

Possibilities dart:js_interop allow us to describe the interface of this method and call it as if it were a dart method.

And here we don’t even need to convert types, everything will be done for us during the compilation process.

What if you need it the other way around? Let's try calling a Dart method from Js.

Calling Dart from JS

/// Объявляем метод
void dartPrint(String message) {
  print('JS say: $message');
}

/// Регистрируем в текущем контексте
html.window.setProperty(
    'printOnDart'.toJS, // Даем название 
    dartPrint.toJS, // Указываем, какой метод будет вызван
);

Here pay your attention to the getter .toJS it is provided by the library dart:js_interopit serves to convert types from Dart to Js.

In a similar way, we can register a callback and handle js events on the Dart side.

void onWindowEvent(html.Event e) {
  print(e.type);
}

html.window.onblur = onWindowEvent.toJS;
html.window.onfocus = onWindowEvent.toJS;

This is how, for example, we can track the activity of the browser tab in which our application is running.

Using JS libraries

Let's complicate and diversify our examples – we'll use a third-party js library for displaying push notifications from dart.

First, let's connect it to web/index.html – let's add it to the block <head>

      <...>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.8/push.min.js"></script>
</head>

Now let's make sure that the library is connected correctly. To do this, use the DevTools console and call the library method

Calling libraries is no different from what we already tried with the window object. But there is one complication. Let's look at the method description Push.create() in the documentation:

Push.create("Hello world!", {
    body: "How's it hangin'?",
    icon: "/icon.png",
    timeout: 4000,
    onClick: function () {
        window.focus();
        this.close();
    }
});

The method takes 2 parameters:

1. The string that will be the title of the notification.

2. Some structure in which named parameters are located body, icon, timeout And onClick.

  • It may seem that the second parameter is very similar to Map<String, dynamic>. But this is not entirely true.

  • When using JS-interop, we establish communication between two different languages, with different type systems. The Dart SDK developers have provided auto-casting for simple and some intermediate data types:

  • basic types dart types: void, bool, num, double, int, String;

  • references to dart objects compiled into js ExternalDartReference;

  • JSAny and heirs, standard js types (JSString, JSFunction, JSArray and others).

Full list here

The Map type is not in this list. But we can transform it Map V JSON (JavaScript Object Notation), which will become the equivalent Map in js.

Let's describe the interface for interacting with a js object Push:

/// Указываем что данный объект является JS объектом
@JS() 
/// Для использования, нам не нужен инстанс этого объекта,
/// мы вызываем только статические методы
@staticInterop 
class Push {
    /// Объявляем интерфейс внешнего метода
    /// В данном случае нам нужно помочь dart 
    /// определится с типами. 
    /// JSAny? - это родительский тип для всех типов в JS
    /// Можем считать его аналогом Object? в Dart
    external static void create(String title, JSAny? options);
}

All we have to do is transform it Map V JSON :

 Push.create(
     'Title', {
         'body': 'Hello, World!',
    }.jsify()
);

This approach works, but it must be used with caution. Method jsify() And dartify()which are used for reverse transformation, will most likely be removed in the next versions of the library dart:js_interop.

Getter toJS is not available for structures, and specifying keys as a string is not the safest idea – it is easy to make a typo. So how to transform such objects?

We describe the structure and mark it as external @JSExport():

@JSExport()
class Options {
  final String? body;
  final String? icon;

  Options({
    this.body,
    this.icon,
  });
}

Adding a type extension for JSObject with the same fields and data types as the main structure:

extension type OptionsExternal(JSObject _) implements JSObject {
  external String? body;
  external String? icon;
}

We use the method to register the dart object as a js object createJSInteropWrapper<T>(objectInstance):

Push.create(
    'Title',
    createJSInteropWrapper<Options>(
        Options(
            body: 'Hello, World!',
            icon: 'path.to/icon.png',
        ),
    ) as OptionsExternal,
);

End of the first part

The Js-Dart interlanguage interaction is quite simple. But writing large functionality only in Dart is not always convenient. At a minimum — due to the lack of IDE hints about available methods in Js objects. At a maximum — due to the verbose transformation of complex objects. It is much more convenient to develop large modules in the native language (Js) and leave a simple interface for interaction with Dart.

In the next part of the article, we will look at how to develop our own libraries using TypeScript and prepare the application for cross-platform compilation.

More useful information about Flutter is available in the Surf Flutter Team Telegram channel.

Cases, best practices, news and vacancies for the Flutter Surf team in one place. Join us!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *