1. Say something long
Last year I wrote a simple autonamap plugin for Flutter_Deer that supported both Android and iOS. Some time ago, there was an issue asking whether Flutter Web would be supported. I was a little confused at that time, after all, I was not familiar with JS. But take note of this requirement until you have time to research it.
After a month, I suddenly remembered this matter. First went to search the relevant information, found that are the implementation of Google Maps. This is all done using an open source library called Google_maps. This library is actually a JS library wrapped with Google Maps through Js_wrapping, which uses Dart code to invoke JS code.
There is no way, it seems that I can only go to the package of Gaud map JS. I wanted to use Js_wrapping to implement it, but later I found that Dart SDK has Dart: JS that provides JS API, and also provides package: JS that is easier to use.
2. Call JS Dart
This part I try to say a little more details, after all, the current relevant information is not much (not fast collection ~~). So people don’t get confused like I did in the first place. The following uses the AMap Api as an example to illustrate how Dart calls JS code.
First add the dependency to pubspec.yaml:
dependencies:
# https://pub.flutter-io.cn/packages/js#-readme-tab-
js: ^0.61.+1
Copy the code
Dart file, import package:js and specify the library name with the @js annotation:
@JS('AMap')
library amap;
import 'package:js/js.dart';
Copy the code
Here AMap is actually the autonavi JS library name.
If we want to implement the above call, we need to define itMap
Object:
@JS('AMap')
library amap;
import 'package:js/js.dart';
// here 'new Map(id)' calls js' new amap.map (id) '
@JS()
class Map {
external Map(String id);
}
Copy the code
Map
may be confused with Map
, so we can give @js annotation name to resolve the problem:
@JS('Map')
class AMap {
external AMap(String id);
}
Copy the code
Adding the external keyword means “external”, which means that the method is implemented in JS code.
So let’s seeThe Map document: Map
The constructor for “div id” is more than a div ID, but it could beHTMLDivElement
So we can’t use the previous String type. At the same time there isMapOptions
The initialized parameter object.
@JS('Map')
class AMap {
external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
}
Copy the code
MapOptions is actually AMap
structure, not a class, so we need to add @anonymous annotation, otherwise creating MapOptions will become new map. MapOptions, which is obviously not in the js library.
@JS()
@anonymous
class MapOptions {
external factory MapOptions({/// initial center latitude and longitude LngLat center, /// map display zoom level num zoom, /// map view mode, default is'2D'the String/ * '2 d' | '3 d' * / viewMode,
});
}
Copy the code
If you want to get or modify certain parameters, you can add corresponding get and set methods.
@JS()
@anonymous
class MapOptions {
external LngLat get center;
external set center(LngLat v);
external factory MapOptions({
LngLat center,
num zoom,
String / * '2 d' | '3 d' * / viewMode,
});
}
Copy the code
MapOptions
Is shown in the codeLngLat
Object, this class is documented as follows:So the corresponding Dart encapsulation is as follows:
@JS()
class LngLat {
external num getLng(a);
external num getLat(a);
external LngLat(num lng, num lat);
}
Copy the code
Here I have not written completely, only provide the getLng, getLat methods I used.
Here we use our results so far:
JS code:The Dart code:
MapOptions _mapOptions = MapOptions(
zoom: 11,
viewMode: '3D',
center: LngLat(116.397428.39.90923)); AMap aMap = AMap('container', _mapOptions);
Copy the code
Here we can also find that most of the basic types can be matched with JS one by one, such as String, num, bool, List. For Map types, we need to encapsulate them ourselves.
3. Advanced
1.List
JavaScript arrays have no specific element types, so an array returned by a JavaScript function cannot guarantee its element type without checking each element.
For example: suppose js has an array list = [‘Android’, ‘iOS’, ‘Web’]; It looks like it’s a List
, but it’s actually a List
.
// true
print(list is List);
// false
print(list is List<String>);
Copy the code
Poi = Array< POI > = Array< POI > = Array< POI >
@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; . }/ / when used
pois.forEach((poi) {
if(poi is Poi) { poi.citycode; . }});Copy the code
I tried using List
, so it’s safe to use List
>, and then cast or cast as you use it.
2. The callback
Also known as the transfer function, here to map plug-in loading method for example. The documentation is as follows:The JS code is as follows:
mapObj.plugin(["AMap.ToolBar"].function() {
// Load the toolbar
var tool = new AMap.ToolBar();
mapObj.addControl(tool);
});
Copy the code
Dart function:
@JS('Map')
class AMap {
external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
/// Load the plug-in
external plugin(dynamic/*String|List*/ name, void Function() callback);
}
Copy the code
The same is true if function has 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());
}));
Copy the code
This is required if you pass the Dart function as a parameter to the JS ApiallowInterop
orallowInteropCaptureThis
Method to ensure compatibility.
3. The asynchronous
Example: Autonavi recommended useJSAPI Loader
To load maps and plug-ins. The usage method is as follows:Wrapping this part of the code is simple:
@JS('AMapLoader')
library loader;
import 'package:js/js.dart';
/// autonavi Loader js
external load(LoaderOptions options);
@JS()
@anonymous
class LoaderOptions {
external factory LoaderOptions({/// JSAPI version number String version, // List
plugins,})
;
}
Copy the code
How to convert A Js Promise into a Dart Future? Use the promiseToFuture method 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;
}
Copy the code
Examples of code to use:
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');
});
Copy the code
4. Display the map
Using the above method, I will use the Autonavi API for encapsulation, complete the JS call part of the work. This leaves the map display and the corresponding logic implementation.
The logical implementation of the function is not said here, mainly about how to display the map.
First add js to your web directory’s index.html (before main.dart.js) :
<script src="https://webapi.amap.com/loader.js"></script>
Copy the code
Just like AndroidView for Android and UiKitView for iOS, there’s an HtmlElementView on the Web side. (Flutter SDK: Dev Channel 1.19.0-1.0.pre)
It requires a unique identifier, viewType, registered by the PlatformViewFactory.
/// Use time as a unique identifier
_divId = DateTime.now().toIso8601String();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) => HtmlElement());
return HtmlElementView(
viewType: _divId,
);
Copy the code
Map creation requires a DIV ID or HTMLDivElement, so we need to create a div. Dart: HTML gives us DOM Elements, CSS styles, local storage, audio and video, events, etc. (40,000 lines of code is not covered…) . Here’s the HTMLDivElement you need:
@Native("HTMLDivElement")
class DivElement extends HtmlElement {
// To suppress missing implicit constructor warnings.
factory DivElement._() {
throw new UnsupportedError("Not supported");
}
factory DivElement(a) => 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();
}
Copy the code
After finishing, the complete code is as follows:
import 'dart:html';
import 'dart:ui' as ui;
String _divId;
DivElement _element;
@override
void initState(a) {
super.initState();
/// Use time as a unique identifier
_divId = DateTime.now().toIso8601String();
// create a div and register it
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) {
/// Div for the map_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,
);
}
Copy the code
HtmlElementView doesn’t give a callback to onCreatePlatformView like AndroidView or UiKitView does, so the map I created directly doesn’t show up. So I’m using addPostFrame AllBack to do that. Or refer to this issue and customize PlatformViewLink.
But I had more problems than that, mostly with maps. Such as:
-
The logo, positioning and scale of Amap are not displayed. Part of the logo is covered by the map layer in the upper left corner of the map.
-
The overlay on the map cannot be modified after being added.
-
The overlay on the map is out of position when the map is zoomed in and out. (Similar to point 2)
It can be seen that the problems are all in the rendering. By querying relevant information, it is known that the model is based on THE HTML DOM, which combines HTML, CSS and Canvas API to realize the page. This is officially called the DomCanvas rendering system. A second approach is being tried, CanvasKit, which uses WebAssembly and WebGL to bring Skia to the Web, leveraging hardware acceleration to improve the ability to render complex and dense graphics.
Flutter Web currently uses DomCanvas by default, so I tried to enable the CanvasKit rendering engine with the following command to see the effect:
flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true
Copy the code
After running, these problems are solved, but new problems arise. For example, maps can’t be clicked, dragged, the text is garbled… Of course, official articles also point out that at this stageCanvasKit
The engine’s still a little rough, andDomCanvas
The engine is more stable.
2021-02-18 Supplement: Use 1.27.0-4.0. PreCanvasKit
Rendering engine, maps can be clicked and dragged, but Spaces garbled. It’s a lot better than it was before.
I ended up with a stable solution. Because other functions, such as poI search, map click and other functions that do not involve display, the test is normal, basically can meet the use. Take a look at the current results:
Functions achieved:
- Automatic positioning and POI search based on the current latitude and longitude
- Click on the map to get latitude and longitude and perform a POI search
- Click on the address information to move the map to the current location
- POI search function
Actually, from the first preview of Flutter Web last year, to the Beta version at the end of last year, until now. I would run Flutter_deer on the Web end, and I could obviously feel that its performance was getting better and better. For example, sometimes the text was not in the center, the animation was not consistent, and the hierarchy of the Stack was not displayed correctly, and many other small problems were solved. Maybe one day I will run again and all the above problems will be solved, haha!!
This time I found that Flutter Web also supports PWA. I found that Flutter Web is really good when I tried it on PC and mobile phone. The mobile phone almost looks like the real thing.
Finally, I have submitted this part of the complete code to Github, now the small plug-in support Android, iOS and Web, welcome to experience!! Finally, give me a thumbs up
Reference 5.
-
chartjs
-
flutter_google_maps
-
Amap JS API
-
Add a website to your desktop
-
Flutter web support updates