Play with flutter web — implementation of Gode map plug-in

Time:2020-9-11

Play with flutter web -- implementation of Gode map plug-in

1. Wordy

Last year, I wrote a simple Gade map plug-in to flutter_ Deer was used. At that time, it supported both Android and IOS. A while ago, an issue asked whether it would support fluent web. At that time, I was a little confused. After all, JS was unfamiliar to me… But first write down the requirement and wait for time to study it.

After a month, I suddenly thought of it. First went to search for relevant information, found that is the realization of Google maps. And that’s all using a Google_ Open source library of maps. This library is actually based on JS_ Wrapping encapsulates the JS Library of Google maps to achieve the purpose of calling JS code with dart code.

I can’t help it. It seems that I can only encapsulate JS of Gaud map. I wanted to use it according to the gourdjs_wrappingAfter that, we find that dart SDK provides JS API operationdart:jsIt also provides a more user-friendly package:js 。

2. Dart calls JS

I’ll try to be more detailed in this part. After all, there are not many relevant materials (I don’t want to collect them soon ~ ~). To avoid people being as confused as I was in the beginning. Let’s take the API of goldmap as an example to illustrate how to realize dart calling JS code,

First, in thepubspec.yamlAdd dependency:

dependencies:
  #  https://pub.flutter-io.cn/packages/js#-readme-tab-
  js: ^0.6.1+1

establishamapjs.dartImport, filepackage:jsAt the same time@JSAnnotation specifies the library name:

@JS('AMap')
library amap;

import 'package:js/js.dart';

thereAMapIt is actually the library name of Golder JS.

Play with flutter web -- implementation of Gode map plug-in
If we want to implement the call above, we need to define itMapParticipants:

@JS('AMap')
library amap;

import 'package:js/js.dart';

//Here, 'new map (ID)' calls the 'new' of JS AMap.Map (id)`
@JS()
class Map {
  external Map(String id);
}

If you callMapMay be withMap<K, V>It’s ambiguous, so we can give annotations@JSSpecify name to resolve the problem:

@JS('Map')
class AMap {
  external AMap(String id);
}

And addexternalKeyword means “external”, that is to say, this method is implemented by JS code.

Let’s take a look at the map document:
Play with flutter web -- implementation of Gode map plug-in
MapThe construction method of is not only as simple as a div ID, but also can beHTMLDivElementSo we can’t use the previous string type. At the same timeMapOptionsThis initialization parameter object.

@JS('Map')
class AMap {
  external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
}

andMapOptionsIt’s actually aMap<K, V>Structure is not a class, so we need to add@anonymousAnnotation, otherwise createMapOptions That’s itnew AMap.MapOptions This is obviously not in the JS library.

@JS()
@anonymous
class MapOptions {
  external factory MapOptions({
    ///Initial center longitude and latitude
    LngLat center,
    ///Zoom level of map display
    num zoom,
    ///Map view mode, default to '2D'
    String /*‘2D’|‘3D’*/ viewMode,
  });
}

If you want to get or modify some parameters, you can add the correspondinggetsetmethod.

@JS()
@anonymous
class MapOptions {
  external LngLat get center;
  external set center(LngLat v);
  external factory MapOptions({
    LngLat center,
    num zoom,
    String /*‘2D’|‘3D’*/ viewMode,
  });
}

MapOptions In the code forLngLat Object. The document for this class is as follows:
Play with flutter web -- implementation of Gode map plug-in
Therefore, the corresponding dart package is as follows:

@JS()
class LngLat {
  external num getLng();
  external num getLat();
  external LngLat(num lng, num lat);
}

I didn’t write it completely here. I only provided what I usedgetLnggetLatmethod.


Here we use our current results:

JS code:
Play with flutter web -- implementation of Gode map plug-in
Dart Code:

MapOptions _mapOptions = MapOptions(
  zoom: 11,
  viewMode: '3D',
  center: LngLat(116.397428, 39.90923),
);
AMap aMap = AMap('container', _mapOptions);

We can also find that most of the basic types can be one-to-one corresponding to JS, such as string, num, bool, and list that I use. For map types, we need to encapsulate them ourselves.

3. Advanced

1.List

Array instances from JavaScript are alwaysList<dynamic>
JavaScript arrays have no specific element type, so the array returned by JavaScript functions cannot guarantee its element type without checking each element.

For example, suppose JS has an arraylist = ['Android', 'iOS', 'Web'];It seems to think it’s aList<String>In fact, it isList<dynamic>

// true
print(list is List);
// false
print(list is List<String>);

There is a POI search function in godry, which will return aArray<Poi>The implementation code is as follows:

@JS()
@anonymous
class PoiList {
  external List<dynamic> get pois;
}

@JS()
@anonymous
class Poi {
  external String get citycode;
  external String get cityname;
  external String get adname;
  external String get name;
  ...
}

//In use
pois.forEach((poi) {
  if (poi is Poi) {
       poi.citycode;
       ...
  }
});

I’ve tried to use the list hereList<Poi>There’s nothing wrong with the test. But POIs does returnList<dynamic>So the safe way to write it is to use itList<dynamic>>When used, it can be converted or forced.

2. Callback

This is the transfer function. Here we take the map plug-in loading method as an example. The documents are as follows:
Play with flutter web -- implementation of Gode map plug-in
JS code is as follows:

mapObj.plugin(["AMap.ToolBar"], function() {
    //Load toolbar
    var tool = new AMap.ToolBar();
    mapObj.addControl(tool);
});

Actually, it’s herefunctionThat’s dart’sFunction

@JS('Map')
class AMap {
  external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
  ///Loading plug-ins
  external plugin(dynamic/*String|List*/ name, void Function() callback); 
}

IffunctionIt’s the same with parameters. The only difference is in use:

import 'package:js/js.dart';
//Error
mapObj.plugin(['AMap.ToolBar'], () {
  mapObj.addControl(ToolBar());
});
//Right
mapObj.plugin(['AMap.ToolBar'], allowInterop(() {
  mapObj.addControl(ToolBar());
}));

If you pass the dart function as an argument to the JS API, you need to use theallowInteroporallowInteropCaptureThisMethod to ensure compatibility.

3. Asynchronous

Example: recommended by GolderJSAPI LoaderTo load maps and plug-ins. The usage is as follows:
Play with flutter web -- implementation of Gode map plug-in
The encapsulation of this part of the code is very simple

@JS('AMapLoader')
library loader;

import 'package:js/js.dart';

///Gaode Map Loader JS
external load(LoaderOptions options);

@JS()
@anonymous
class LoaderOptions {

  external factory LoaderOptions({
    ///The key value you requested
    String key,
    ///Jsapi version number
    String version,
    //List of plug-ins loaded synchronously
    List<String> plugins,
  });
}

It’s mainly used. How to use jsPromiseConverted to dart’sFuture 。 I’ll use it herepromiseToFutureThe source code is as follows:

Future<T> promiseToFuture<T>(jsPromise) {
  final completer = Completer<T>();

  final success = convertDartClosureToJS((r) => completer.complete(r), 1);
  final error = convertDartClosureToJS((e) => completer.completeError(e), 1);

  JS('', '#.then(#, #)', jsPromise, success, error);
  return completer.future;
}

Use code example:

import 'dart:js_util';

var promise = load(LoaderOptions(
  key: 'xxx',
  version: '2.0',
  plugins: ['AMap.Scale'],
));

promiseToFuture(promise).then((value) {
  AMap aMap = AMap('container');
  ...      
}, onError: (e) {
  Print ('initialization error: $e ');
});

4. Display map

Using the above method, I encapsulated the Gaud API I used and completed the work of JS call. Here is the map display and the corresponding logic implementation.

The logic implementation of the function here will not say much, mainly about how to display the map.

First, in the web directoryindex.htmlAdd JS (inmain.dart.jsBefore:

<script></script>

In fact, it is similar to AndroidAndroidViewAnd IOSUiKitViewSame, there’s one on the webHtmlElementView。(Flutter sdk:Dev channel 1.19.0-1.0.pre)

It needs aPlatformViewFactoryUnique identifier of the registrationviewType

///Time is used as the unique identifier here
_divId = DateTime.now().toIso8601String();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) => HtmlElement());

return HtmlElementView(
  viewType: _divId,
);

Map creation requires div ID orHTMLDivElement, so we need to create a Div. At DART’sdart:htmlProvides us with DOM element, CSS style, local storage, audio and video, events, etc. (40000 lines of code are not covered…). One of them is needed hereHTMLDivElement

@Native("HTMLDivElement")
class DivElement extends HtmlElement {
  // To suppress missing implicit constructor warnings.
  factory DivElement._() {
    throw new UnsupportedError("Not supported");
  }

  factory DivElement() => JS('returns:DivElement;creates:DivElement;new:true',
      '#.createElement(#)', document, "div");
  /**
   * Constructor instantiated by the DOM when a custom element has been created.
   *
   * This can only be called by subclasses from their created constructor.
   */
  DivElement.created() : super.created();
}

After sorting out, the complete code is as follows:

import 'dart:html';
import 'dart:ui' as ui;

String _divId;
DivElement _element;

@override
void initState() {
  super.initState();
  ///Time is used as the unique identifier here
  _divId = DateTime.now().toIso8601String();
  ///First create div and register
  // ignore: undefined_prefixed_name
  ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) {
    ///Div for maps
    _element = DivElement()
      ..style.width = '100%'
      ..style.height = '100%'
      ..style.margin = '0';

    return _element;
  });
  SchedulerBinding.instance.addPostFrameCallback((_) {
    ///Create a map
    var promise = load(LoaderOptions(
      key: 'xxx',
      version: '2.0',
      plugins: ['AMap.Scale'],
    ));

    promiseToFuture(promise).then((value) {
      AMap aMap = AMap(_element);
    }, onError: (e) {
      Print ('initialization error: $e ');
    });
  });
}

@override
Widget build(BuildContext context) {
  return HtmlElementView(
    viewType: _divId,
  );
}

There is a problem here,HtmlElementViewThere is no andAndroidViewUiKitViewGive the sameonCreatePlatformViewCreate callback, so the map I created directly will not be displayed, so I used itaddPostFrameCallbackTo deal with it. Or refer to the issue and customize itPlatformViewLinkTo achieve.

However, I encountered more than these problems, mostly the display of maps. For example:

  • The logo, location and scale of Gaode map are not displayed, and some of them are covered by map layer in the upper left corner of the map.
  • The overlay on the map cannot be modified after adding.
  • The cover on the map deviates when the map is zoomed in and out. (similar to the second point)

It can be seen that the problems are all on the rendering, query the relevant information and know that it is based onHTML DOMThe model combinesHTMLCSSandCanvas APITo implement the page, the official call this implementationDomCanvasRendering system. Now we are trying to use the second methodCanvasKitCanvasKituseWebAssemblyandWebGLtakeSkiaThe ability of rendering complex and dense graphics is improved by introducing web and hardware acceleration.

At this stage, the default use of flutter web isDomCanvas, so I tried to use the following command to enableCanvasKitRendering engine to see the effect:

flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true

After running, it is found that these problems have been solved, but new problems have arisen. For example, the map can’t be click, drag, the text is garbled…
Play with flutter web -- implementation of Gode map plug-in
Of course, the official article also pointed out that at this stageCanvasKitThe engine is still rough, andDomCanvasThe engine is relatively more stable.

In the end, I used a stable solution. Because other functions, such as POI search, map click and other functions that do not involve display, the test is normal and can basically meet the use. Let’s talk about the effect display at this stage:


Functions achieved:

  • Automatic positioning and POI search based on current longitude and latitude
  • Click on the map to obtain latitude and longitude and conduct POI search
  • Click the address information to move the map to the current location
  • POI search function

In fact, from the first preview version of flutter web last year, to the beta version at the end of last year, and now. I’m going to take the flutter_ After running deer on the web, I can clearly feel that it is getting better and better, such as sometimes the text is not centered, the animation performance is inconsistentStackMany small problems have been solved, such as incorrect display of hierarchy of. Maybe one day I run again, the above problems are solved, ha ha!!

This discovery also supportsPWA, I found it’s really good from my PC and mobile phone experience ~ ~ ~ it seems that the mobile phone can almost confuse the real with the fake. In fact, the GIF above is the effect of the PC end.

Finally, I have submitted this part of the complete code to GitHub. Now, this small plug-in already supports Android, IOS and web. Welcome to experience it!! Finally, I’d like to support it~

5. Reference

  • chartjs
  • flutter_google_maps
  • Gaude map JS API
  • [PWA tutorial] add website to desktop
  • Flutter web support updates

Recommended Today

Monkey patch monkey patch programming method and its application in Ruby

What ismonkey patch (Monkey Patch)? In a dynamic language, functions are added and changed without modifying the source code. Purpose of using monkey patch:1. Additional function2. Function change3. Fix program errors4. Add a hook to execute some other processing while executing a method, such as printing logs, realizing AOP, etc,5. Cache, when the amount of […]