玩玩Flutter Web——实现高德地图插件
wptr33 2024-12-05 17:00 16 浏览
1.啰嗦几句
去年写了一个功能简单的高德地图插件给flutter_deer使用,当时支持了Android与iOS两端。前一阵子有一个issue问是否会支持Flutter Web,当时我有点懵,毕竟js我都不熟。。。不过先记下这个需求,等着有时间了去研究一下。
过了一个月,突然想起了这件事。就先去搜索了一下相关资料,发现都是实现的谷歌地图。而这些都使用到了一个google_maps的开源库。这个库其实就是借助js_wrapping封装了谷歌地图的js库,达到使用Dart代码调用js代码的目的。
没办法了,看来我也只能去封装高德地图的js了。本想着照葫芦画瓢使用js_wrapping去实现,后面发现Dart sdk有提供操作js api的dart:js,同时也提供了更易于使用的package:js。
2.Dart调用JS
这部分我尽量说的细一点,毕竟目前相关资料不多(还不快收藏起来~~)。避免大家像我一开始一样一头雾水。下面就以高德地图的Api来举例说明如何实现Dart调用JS代码,
首先在pubspec.yaml添加依赖:
dependencies:
# https://pub.flutter-io.cn/packages/js#-readme-tab-
js: ^0.6.1+1
创建amapjs.dart文件,导入package:js,同时用@JS注解指定库名:
@JS('AMap')
library amap;
import 'package:js/js.dart';
这里的AMap实际就是高德js的库名。
如果我们要实现上图的调用,就需要接着定义Map对象:
@JS('AMap')
library amap;
import 'package:js/js.dart';
// 这里`new Map(id)` 调用js的`new AMap.Map(id)`
@JS()
class Map {
external Map(String id);
}
这里如果直接调用Map 可能会和Map<K, V>产生歧义,所以我们可以给注解@JS指定name来化解问题:
@JS('Map')
class AMap {
external AMap(String id);
}
而添加external关键字的意思是指“外在”,也就是说这个方法是js代码实现的。
下面我们看一下Map的文档:
Map的构造方法不止一个div id这么简单,也可能是HTMLDivElement,所以我们不能使用之前的String类型了。同时有MapOptions这个初始化的参数对象。
@JS('Map')
class AMap {
external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
}
而MapOptions实际是一个Map<K, V>结构,并不是一个类,所以我们需要添加@anonymous注解,否则创建MapOptions 就成了new AMap.MapOptions ,这个显然不在js库中。
@JS()
@anonymous
class MapOptions {
external factory MapOptions({
/// 初始中心经纬度
LngLat center,
/// 地图显示的缩放级别
num zoom,
/// 地图视图模式, 默认为‘2D’
String /*‘2D’|‘3D’*/ viewMode,
});
}
如果你想获取或修改某些参数,可以添加对应的get、set方法。
@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 的代码中出现了LngLat 对象,这个类的文档如下:
所以对应的Dart封装如下:
@JS()
class LngLat {
external num getLng();
external num getLat();
external LngLat(num lng, num lat);
}
这里我没有写完全,只提供了我用到的getLng、getLat方法。
这里我们使用一下我们目前的成果:
JS代码:
Dart代码:
MapOptions _mapOptions = MapOptions(
zoom: 11,
viewMode: '3D',
center: LngLat(116.397428, 39.90923),
);
AMap aMap = AMap('container', _mapOptions);
到这里我们也能发现,大多数的基础类型我们都是可以和js去一一对应上的,比如我用到的String、num、bool、List,对于Map类型需要我们自己封装。
3.进阶
1.List
来自JavaScript的数组实例总是List<dynamic>
JavaScript数组没有具体的元素类型,因此JavaScript函数返回的数组不能在不检查每个元素的情况下保证其元素类型。
举个例子:假设js有个数组list = ['Android', 'iOS', 'Web'];,看似以为它是个List<String>,其实它是List<dynamic>。
// true
print(list is List);
// false
print(list is List<String>);
在高德里有个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;
...
}
// 使用时
pois.forEach((poi) {
if (poi is Poi) {
poi.citycode;
...
}
});
这里的List我尝试过使用List<Poi>,测试也没什么问题。但是pois确实返回的是List<dynamic>,所以稳妥的写法还是使用List<dynamic>>,使用时再转换或强转。
2.回调
也就是传递函数,这里以地图插件加载方法来举例。文档如下:
JS代码如下:
mapObj.plugin(["AMap.ToolBar"], function() {
//加载工具条
var tool = new AMap.ToolBar();
mapObj.addControl(tool);
});
其实这里的function就对应Dart的Function:
@JS('Map')
class AMap {
external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
/// 加载插件
external plugin(dynamic/*String|List*/ name, void Function() callback);
}
如果function有参数也是一样的。唯一的区别在于使用时的不同:
import 'package:js/js.dart';
// 错误
mapObj.plugin(['AMap.ToolBar'], () {
mapObj.addControl(ToolBar());
});
// 正确
mapObj.plugin(['AMap.ToolBar'], allowInterop(() {
mapObj.addControl(ToolBar());
}));
如果将Dart函数作为参数传递给JS Api,则需要使用allowInterop或allowInteropCaptureThis方法确保兼容性。
3.异步
举例:高德推荐使用JSAPI Loader来进行加载地图及插件。使用方法如下:
这部分代码的封装很简单:
@JS('AMapLoader')
library loader;
import 'package:js/js.dart';
/// 高德地图 Loader js
external load(LoaderOptions options);
@JS()
@anonymous
class LoaderOptions {
external factory LoaderOptions({
///您申请的key值
String key,
/// JSAPI 版本号
String version,
//同步加载的插件列表
List<String> plugins,
});
}
主要还是使用上,怎么将Js的Promise转换成Dart的Future 。这里就用到了promiseToFuture方法,源码如下:
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;
}
使用代码示例:
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('初始化错误:$e');
});
4.显示地图
使用上面的方法,我将我使用到的高德api进行了封装,完成了JS调用部分的工作。到这里就剩下了地图显示及相应的逻辑实现了。
功能的逻辑实现这里就不多说了,主要说说如何显示地图。
首先在web目录的index.html中添加js(在main.dart.js之前):
<script src="https://webapi.amap.com/loader.js"></script>
其实与Android的AndroidView和iOS的UiKitView相同,Web这边有个HtmlElementView。(Flutter sdk:Dev channel 1.19.0-1.0.pre)
它需要一个由PlatformViewFactory注册的唯一标识符viewType。
/// 这里使用时间作为唯一标识
_divId = DateTime.now().toIso8601String();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) => HtmlElement());
return HtmlElementView(
viewType: _divId,
);
地图创建需要div的id或者HTMLDivElement,所以我们需要创建一个div。在Dart的dart:html中为我们提供了DOM element、CSS样式、本地存储、音视频、事件等(4万行代码不是盖的...)。其中就有这里需要的HTMLDivElement:
@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();
}
整理后,完整代码如下:
import 'dart:html';
import 'dart:ui' as ui;
String _divId;
DivElement _element;
@override
void initState() {
super.initState();
/// 这里使用时间作为唯一标识
_divId = DateTime.now().toIso8601String();
/// 先创建div并注册
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) {
/// 地图需要的Div
_element = DivElement()
..style.width = '100%'
..style.height = '100%'
..style.margin = '0';
return _element;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
/// 创建地图
var promise = load(LoaderOptions(
key: 'xxx',
version: '2.0',
plugins: ['AMap.Scale'],
));
promiseToFuture(promise).then((value) {
AMap aMap = AMap(_element);
}, onError: (e) {
print('初始化错误:$e');
});
});
}
@override
Widget build(BuildContext context) {
return HtmlElementView(
viewType: _divId,
);
}
这里其实有点问题,HtmlElementView没有和AndroidView、UiKitView一样给予onCreatePlatformView创建回调,导致我直接创建的地图会显示不出来,所以我使用了addPostFrameCallback来处理。或者参考这个issue,自定义PlatformViewLink来实现。
不过我遇见的问题不止这些,大都是地图的显示问题。比如:
- 高德地图的logo、定位、比例尺这类不显示,部分在地图的左上角被地图层覆盖。
- 地图上的覆盖物添加后无法修改。
- 地图上的覆盖物在地图放大缩小后位置偏离。(和第二点类似)
可以看出问题都是渲染上的,查询相关资料得知现在是基于HTML DOM的模型,该模型结合了HTML,CSS和Canvas API来实现页面,官方将此实现称为DomCanvas渲染系统。而目前在尝试使用第二种方法CanvasKit,CanvasKit 使用WebAssembly和WebGL将Skia引入Web,利用硬件加速从而提高了渲染复杂和密集图形的能力。
现阶段Flutter Web 默认使用DomCanvas,所以我尝试使用以下命令启用CanvasKit渲染引擎来看看效果:
flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true
运行后发现这些问题得到了解决,但是又产生了新的问题。比如地图不能点击、拖动,文字乱码。。。
当然官方的文章也指出,现阶段CanvasKit引擎还是比较粗糙的,而DomCanvas引擎相对更加稳定。
最终我还是使用了稳定的方案。因为其他功能,比如poi的搜索、地图点击这类不涉及显示的功能,测试均是正常的,基本可以满足使用。
实现的功能:
- 自动定位并根据当前经纬度进行POI搜索
- 点击地图获取经纬度并进行POI搜索
- 点击地址信息,移动地图至当前位置
- POI搜索功能
其实从Flutter Web去年的首个预览版、到去年底的Beta版、到现在。我都会将flutter_deer在web端运行一下,我能明显的感觉到它表现的越来越好了,比如有时文字不居中、动画表现不一致、Stack的层级显示不正确等许多小问题都得到了解决。说不定哪天我再次运行起来,上面说的问题都解决了,哈哈!!
这次发现Flutter Web也支持了PWA,我在PC和手机体验下来发现是真的很不错~~,手机端看上去几乎可以以假乱真,其实上面的GIF就是PC端的效果。
最后,我将这部分完整代码(https://github.com/simplezhli/flutter_2d_amap)都已提交至Github,现在这个小插件已经支持Android、iOS和Web了,欢迎体验!!最最后,点赞支持一下~
相关推荐
- 台积电提出SRAM存内计算新方法,能效比可达89TOPS/W
-
芯东西(公众号:aichip001)编译|高歌编辑|云鹏芯东西3月16日消息,近期,台积电的研究人员在ISSCC2021会议上公布了一种改良的SRAM存储器阵列,该SRAM阵列采用22nm工...
- Golang中如何判断两个slice是否相等?
-
在Golang中,要判断两个slice是否相等是不能直接使用==运算符的(==只能说明两个slice是否指向同一个底层数组)。如果两个slice的底层数组相同,但长度或容量不同...
- JS入门基础知识(js基础知识总结笔记)
-
JS对象操作对象增删改查创建对象letobj={}新增属性obj.a=1修改属性obj.a='a'...
- 趣谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64
-
大家好,我是Echa。好久没跟粉丝们细聊JavaScript那点事了。做一名全栈工程师,JS基础还是要打牢,这样的话不管底层业务逻辑以及第三方框架怎么变化,都离不开基础。本文文章属于基础篇,阅读有点...
- 告别 substr() 和 substring()?更可靠的 JavaScript 字符串截取方法
-
JavaScript提供了三个主要的字符串截取方法:...
- golang第九天,切片(slice)介绍(golang 切片作为参数)
-
什么是切片golang切片是对数组的抽象。go的数组长度不可改变,在特定场景中这样的集合就不太适用,go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追...
- Go语言零到一:数组(go struct数组)
-
引言...
- 你说你熟悉Slice,这道slice题你能答对吗?
-
每当你花费大量时间使用某种特定工具时,深入了解它并了解如何高效地使用它是很值得的。...
- Python 3.14七大新特性总结:从t-string模板到GIL并发优化
-
Python3.14已进入测试阶段,根据PEP745发布计划,该版本已停止引入新功能,也就是说新特征就应该已经固定下来了。所以本文基于当前最新的beta2版本,深入分析了Python3.14中...
- Python 幕后:Python导入import的工作原理
-
更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)Python最容易被误解的方面其中之一是import。...
- Python元类实现自动化编程的正确姿势
-
元类是Python中用于创建类的类。通过元类机制,开发者可在运行时动态创建和修改类,为框架开发、设计模式实现和高级架构设计提供核心支持。在Python语言的高级特性中,元类占据着独特而重要的地位。作...
- Python字符串详解与示例(python字符串类型及操作)
-
艾瑞巴蒂字符串的干货来了,字符串是程序中最常见的数据类型之一,用来表示数据文本,下面就来介绍下字符串的特性,操作和方法,和一些示例来吧道友:1.字符串的创建在python中字符串可以永单引号(...
- 恕我直言!你对Python里的import一无所知
-
文章来源:https://mp.weixin.qq.com/s/4WAOU_Lzy651IE-2zZSFfQ原文作者:写代码的明哥...
- Python基础:字符串操作(python字符串的用法)
-
字符串是Python中最常用的数据类型之一,用于表示文本数据。我们将学习如何对字符串进行常见的操作,包括创建、访问、修改和处理字符串。通过掌握这些技巧,您将能够更好地处理和操作文本数据。让我们开始吧!...
- Python 中 字符串处理的高效方法,不允许你还不知道
-
以下是Python中字符串处理的高效方法...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
-
- 台积电提出SRAM存内计算新方法,能效比可达89TOPS/W
- Golang中如何判断两个slice是否相等?
- JS入门基础知识(js基础知识总结笔记)
- 趣谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64
- 告别 substr() 和 substring()?更可靠的 JavaScript 字符串截取方法
- golang第九天,切片(slice)介绍(golang 切片作为参数)
- Go语言零到一:数组(go struct数组)
- 你说你熟悉Slice,这道slice题你能答对吗?
- Python 3.14七大新特性总结:从t-string模板到GIL并发优化
- Python 幕后:Python导入import的工作原理
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)