Kỹ thuật gọi JavaScript từ Dart trong ứng dụng Flutter Web

Khi phát triển ứng dụng Flutter cho nền tảng Web, việc tương tác với các thư viện JavaScript (JS) có sẵn là một yêu cầu phổ biến. Bài viết này hướng dẫn cách sử dụng gói js để thiết lập cơ chế Interop (tương tác) giữa Dart và JavaScript thông qua ví dụ tích hợp bản đồ AMap.

1. Cấu hình môi trường

Để bắt đầu, bạn cần khai báo thư viện js trong tệp pubspec.yaml của dự án:

dependencies:
  js: ^0.6.1+1

2. Định nghĩa giao diện Dart cho mã JavaScript

Để gọi các hàm hoặc khởi tạo các đối tượng từ JS, chúng ta cần tạo một tệp Dart đóng vai trò làm cầu nối. Sử dụng annotation @JS để định danh các thành phần JS tương ứng.

@JS('AMap')
library amap_interop;

import 'package:js/js.dart';

@JS('Map')
class GMap {
  external GMap(dynamic container, MapConfig options);
  external plugin(dynamic name, Function callback);
}

@JS()
@anonymous
class MapConfig {
  external factory MapConfig({
    GeoPoint center,
    num zoom,
    String viewMode,
  });
}

Trong đoạn mã trên, từ khóa external cho Dart biết rằng việc triển khai thực tế của phương thức nằm ở phía JavaScript. Annotation @anonymous được sử dụng cho các lớp đại diện cho các đối tượng JS thuần túy (Object Literals), nơi chúng ta không gọi hàm khởi tạo bằng từ khóa new trong JS.

3. Xử lý các kiểu dữ liệu phức tạp

Khi làm việc với các đối tượng địa lý như tọa độ, chúng ta cần định nghĩa thêm các lớp bổ trợ:

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

Đối với kiểu danh sách, cần lưu ý rằng mảng từ JavaScript khi truyền vào Dart thường được nhận diện dưới dạng List<dynamic>. Nếu bạn cố gắng ép kiểu trực tiếp sang List<String>, ứng dụng có thể gặp lỗi runtime. Cách an toàn nhất là xử lý danh sách động và chuyển đổi dữ liệu khi cần thiết.

4. Cơ chế Callback và Bất đồng bộ

Khi truyền một hàm Dart vào JavaScript dưới dạng callback, bạn phải bao bọc hàm đó bằng allowInterop để đảm bảo tính tương thích về ngữ cảnh thực thi.

gMap.plugin('AMap.ToolBar', allowInterop(() {
  print('Plugin đã được tải xong');
}));

Để xử lý các Promise của JavaScript trong môi trường Dart, chúng ta sử dụng hàm promiseToFuture từ thư viện dart:js_util để chuyển đổi chúng thành Future.

import 'dart:js_util';

void initializeMap() async {
  var loaderPromise = load(LoaderConfig(key: 'your_key', version: '2.0'));
  try {
    await promiseToFuture(loaderPromise);
    var myMap = GMap('map-container', MapConfig(zoom: 12));
  } catch (e) {
    print('Lỗi khởi tạo: $e');
  }
}

5. Hiển thị thành phần Web trên giao diện Flutter

Để hiển thị một thẻ HTML (ví dụ <div> chứa bản đồ) vào cây Widget của Flutter, chúng ta sử dụng HtmlElementView. Đầu tiên, cần đăng ký một viewFactory thông qua platformViewRegistry.

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

// Đăng ký thẻ div để hiển thị bản đồ
void registerMapView(String viewId) {
  // ignore: undefined_prefixed_name
  ui.platformViewRegistry.registerViewFactory(viewId, (int id) {
    return DivElement()
      ..id = 'map-container'
      ..style.width = '100%'
      ..style.height = '100%';
  });
}

// Sử dụng trong Widget
@override
Widget build(BuildContext context) {
  return HtmlElementView(viewType: 'unique-map-id');
}

6. Lưu ý về Rendering Engine

Flutter Web hỗ trợ hai trình kết xuất: HTML (DomCanvas)CanvasKit (Skia).

  • DomCanvas: Sử dụng các thẻ HTML và CSS, tương thích tốt với các thư viện JS thao tác trên DOM nhưng hiệu năng có thể hạn chế với đồ họa phức tạp.
  • CanvasKit: Sử dụng WebAssembly để đưa Skia lên Web, cho hiệu năng mượt mà hơn nhưng có thể gặp vấn đề về tương tác sự kiện (click, scroll) khi lồng ghép các thành phần HTML bên thứ ba.
Trong hầu hết các trường hợp sử dụng bản đồ tích hợp qua thẻ Div, trình kết xuất HTML mặc định vẫn mang lại sự ổn định cao hơn cho các tính năng như tìm kiếm địa điểm (POI) hoặc xử lý sự kiện click trên bản đồ.

Thẻ: Dart flutter-web javascript-interop amap-api WebAssembly

Đăng vào ngày 17 tháng 7 lúc 11:00