486 lines
14 KiB
Markdown
486 lines
14 KiB
Markdown
|
|
# Flutter WebView Bridge Package - План реализации
|
|||
|
|
|
|||
|
|
## 📦 Обзор пакета
|
|||
|
|
|
|||
|
|
**Название**: `flutter_webview_bridge`
|
|||
|
|
**Цель**: Создать Flutter пакет для двустороннего взаимодействия между Flutter-хостом и Flutter Web-приложениями через WebView
|
|||
|
|
|
|||
|
|
## 🏗️ Архитектура пакета
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
flutter_webview_bridge/
|
|||
|
|
├── lib/
|
|||
|
|
│ ├── src/
|
|||
|
|
│ │ ├── bridge/
|
|||
|
|
│ │ │ ├── webview_bridge.dart # Основной класс моста
|
|||
|
|
│ │ │ ├── message_handler.dart # Обработка сообщений
|
|||
|
|
│ │ │ ├── message_serializer.dart # Сериализация сообщений
|
|||
|
|
│ │ │ └── callback_manager.dart # Управление callback'ами
|
|||
|
|
│ │ ├── web/
|
|||
|
|
│ │ │ ├── web_bridge.dart # Flutter Web сторона
|
|||
|
|
│ │ │ └── web_utils.dart # Утилиты для Web
|
|||
|
|
│ │ ├── host/
|
|||
|
|
│ │ │ ├── host_bridge.dart # Flutter Host сторона
|
|||
|
|
│ │ │ └── webview_controller.dart # Контроллер WebView
|
|||
|
|
│ │ └── models/
|
|||
|
|
│ │ ├── bridge_message.dart # Модель сообщения
|
|||
|
|
│ │ └── bridge_config.dart # Конфигурация
|
|||
|
|
│ ├── flutter_webview_bridge.dart # Основной экспорт
|
|||
|
|
│ └── bridge_webview.dart # WebView виджет
|
|||
|
|
├── web/
|
|||
|
|
│ └── bridge.js # JavaScript мост
|
|||
|
|
├── example/
|
|||
|
|
│ ├── lib/
|
|||
|
|
│ │ ├── main.dart # Пример Flutter Host
|
|||
|
|
│ │ └── web_app.dart # Пример Flutter Web
|
|||
|
|
│ └── web/
|
|||
|
|
│ └── index.html # HTML оболочка
|
|||
|
|
└── test/
|
|||
|
|
└── bridge_test.dart # Тесты
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📋 Структура сообщений
|
|||
|
|
|
|||
|
|
### Модель сообщения
|
|||
|
|
```dart
|
|||
|
|
class BridgeMessage {
|
|||
|
|
final String type; // Тип события/запроса
|
|||
|
|
final String? id; // UUID для request-response
|
|||
|
|
final Map<String, dynamic>? data; // Полезная нагрузка
|
|||
|
|
final DateTime? timestamp; // Временная метка
|
|||
|
|
final String? source; // Источник (host/web)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Типы сообщений
|
|||
|
|
- **Request-Response**: `id` обязателен, ожидается ответ
|
|||
|
|
- **Event-Notify**: `id` отсутствует, одностороннее уведомление
|
|||
|
|
- **Stream-Subscription**: для подписки на обновления
|
|||
|
|
|
|||
|
|
## 🔧 Этапы разработки
|
|||
|
|
|
|||
|
|
### Этап 1: Базовая инфраструктура (1-2 дня)
|
|||
|
|
|
|||
|
|
#### 1.1 Создание структуры пакета
|
|||
|
|
```bash
|
|||
|
|
flutter create --template=package flutter_webview_bridge
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2 Основные модели
|
|||
|
|
- `BridgeMessage` - модель сообщения
|
|||
|
|
- `BridgeConfig` - конфигурация моста
|
|||
|
|
- `BridgeException` - исключения моста
|
|||
|
|
|
|||
|
|
#### 1.3 Зависимости в pubspec.yaml
|
|||
|
|
```yaml
|
|||
|
|
dependencies:
|
|||
|
|
flutter:
|
|||
|
|
sdk: flutter
|
|||
|
|
flutter_inappwebview: ^6.0.0
|
|||
|
|
uuid: ^4.0.0
|
|||
|
|
js: ^0.6.7
|
|||
|
|
|
|||
|
|
dev_dependencies:
|
|||
|
|
flutter_test:
|
|||
|
|
sdk: flutter
|
|||
|
|
flutter_lints: ^3.0.0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 2: Flutter Host сторона (2-3 дня)
|
|||
|
|
|
|||
|
|
#### 2.1 WebView контроллер
|
|||
|
|
```dart
|
|||
|
|
class BridgeWebViewController {
|
|||
|
|
InAppWebViewController? _webViewController;
|
|||
|
|
final Map<String, Completer<BridgeMessage>> _pendingCallbacks = {};
|
|||
|
|
final StreamController<BridgeMessage> _messageStream = StreamController.broadcast();
|
|||
|
|
|
|||
|
|
Future<void> initialize(String url);
|
|||
|
|
Future<void> sendMessage(BridgeMessage message);
|
|||
|
|
Stream<BridgeMessage> get messageStream;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2 JavaScript Handler
|
|||
|
|
```dart
|
|||
|
|
void _setupJavaScriptHandler() {
|
|||
|
|
_webViewController?.addJavaScriptHandler(
|
|||
|
|
callbackName: 'fromWebApp',
|
|||
|
|
callback: (args) {
|
|||
|
|
final message = BridgeMessage.fromJson(args[0]);
|
|||
|
|
_handleIncomingMessage(message);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.3 Отправка сообщений в WebView
|
|||
|
|
```dart
|
|||
|
|
Future<void> _sendToWebView(BridgeMessage message) async {
|
|||
|
|
final jsCode = '''
|
|||
|
|
window.dispatchEvent(new CustomEvent("fromFlutterHost", {
|
|||
|
|
detail: ${jsonEncode(message.toJson())}
|
|||
|
|
}));
|
|||
|
|
''';
|
|||
|
|
await _webViewController?.evaluateJavascript(source: jsCode);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 3: JavaScript Bridge (1-2 дня)
|
|||
|
|
|
|||
|
|
#### 3.1 Создание bridge.js
|
|||
|
|
```javascript
|
|||
|
|
// Подписка на события от Flutter Host
|
|||
|
|
window.addEventListener("fromFlutterHost", (e) => {
|
|||
|
|
if (window.flutterApp && window.flutterApp.receiveFromHost) {
|
|||
|
|
window.flutterApp.receiveFromHost(e.detail);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Отправка сообщений в Flutter Host
|
|||
|
|
window.sendToFlutterHost = function(message) {
|
|||
|
|
if (window.flutter_inappwebview) {
|
|||
|
|
window.flutter_inappwebview.callHandler('fromWebApp', message);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2 Интеграция в HTML
|
|||
|
|
```html
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<script src="bridge.js"></script>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="flutter_target"></div>
|
|||
|
|
<script src="main.dart.js" type="application/javascript"></script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 4: Flutter Web сторона (2-3 дня)
|
|||
|
|
|
|||
|
|
#### 4.1 Web Bridge класс
|
|||
|
|
```dart
|
|||
|
|
class WebBridge {
|
|||
|
|
static final WebBridge _instance = WebBridge._internal();
|
|||
|
|
factory WebBridge() => _instance;
|
|||
|
|
WebBridge._internal();
|
|||
|
|
|
|||
|
|
final Map<String, Completer<BridgeMessage>> _pendingCallbacks = {};
|
|||
|
|
final StreamController<BridgeMessage> _messageStream = StreamController.broadcast();
|
|||
|
|
|
|||
|
|
void initialize() {
|
|||
|
|
// Регистрация глобального объекта
|
|||
|
|
setProperty(js.context, 'flutterApp', {
|
|||
|
|
'receiveFromHost': allowInterop(_handleMessageFromHost),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<BridgeMessage> sendMessage(BridgeMessage message) async {
|
|||
|
|
final completer = Completer<BridgeMessage>();
|
|||
|
|
if (message.id != null) {
|
|||
|
|
_pendingCallbacks[message.id!] = completer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
callMethod(js.context, 'sendToFlutterHost', [message.toJson()]);
|
|||
|
|
return completer.future;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 Обработка сообщений
|
|||
|
|
```dart
|
|||
|
|
void _handleMessageFromHost(dynamic data) {
|
|||
|
|
final message = BridgeMessage.fromJson(data);
|
|||
|
|
|
|||
|
|
// Проверяем, есть ли pending callback
|
|||
|
|
if (message.id != null && _pendingCallbacks.containsKey(message.id)) {
|
|||
|
|
_pendingCallbacks[message.id]!.complete(message);
|
|||
|
|
_pendingCallbacks.remove(message.id);
|
|||
|
|
} else {
|
|||
|
|
// Отправляем в стрим для event-notify сообщений
|
|||
|
|
_messageStream.add(message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 5: Основной API пакета (1-2 дня)
|
|||
|
|
|
|||
|
|
#### 5.1 BridgeWebView виджет
|
|||
|
|
```dart
|
|||
|
|
class BridgeWebView extends StatefulWidget {
|
|||
|
|
final String url;
|
|||
|
|
final BridgeConfig? config;
|
|||
|
|
final Function(BridgeMessage)? onMessage;
|
|||
|
|
final Widget? loadingWidget;
|
|||
|
|
|
|||
|
|
const BridgeWebView({
|
|||
|
|
required this.url,
|
|||
|
|
this.config,
|
|||
|
|
this.onMessage,
|
|||
|
|
this.loadingWidget,
|
|||
|
|
super.key,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
State<BridgeWebView> createState() => _BridgeWebViewState();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2 Основной экспорт
|
|||
|
|
```dart
|
|||
|
|
// flutter_webview_bridge.dart
|
|||
|
|
export 'src/bridge/webview_bridge.dart';
|
|||
|
|
export 'src/models/bridge_message.dart';
|
|||
|
|
export 'src/models/bridge_config.dart';
|
|||
|
|
export 'bridge_webview.dart';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 6: Утилиты и хелперы (1 день)
|
|||
|
|
|
|||
|
|
#### 6.1 Message Serializer
|
|||
|
|
```dart
|
|||
|
|
class MessageSerializer {
|
|||
|
|
static Map<String, dynamic> toJson(BridgeMessage message) {
|
|||
|
|
return {
|
|||
|
|
if (message.type != null) 'type': message.type,
|
|||
|
|
if (message.id != null) 'id': message.id,
|
|||
|
|
if (message.data != null) 'data': message.data,
|
|||
|
|
'timestamp': message.timestamp.toIso8601String(),
|
|||
|
|
'source': message.source,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static BridgeMessage fromJson(Map<String, dynamic> json) {
|
|||
|
|
return BridgeMessage(
|
|||
|
|
type: json['type'],
|
|||
|
|
id: json['id'],
|
|||
|
|
data: json['data'],
|
|||
|
|
timestamp: DateTime.parse(json['timestamp']),
|
|||
|
|
source: json['source'],
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.2 Callback Manager
|
|||
|
|
```dart
|
|||
|
|
class CallbackManager {
|
|||
|
|
final Map<String, Completer<BridgeMessage>> _callbacks = {};
|
|||
|
|
final Duration _timeout;
|
|||
|
|
|
|||
|
|
CallbackManager({this._timeout = const Duration(seconds: 30)});
|
|||
|
|
|
|||
|
|
String registerCallback(Completer<BridgeMessage> completer) {
|
|||
|
|
final id = const Uuid().v4();
|
|||
|
|
_callbacks[id] = completer;
|
|||
|
|
|
|||
|
|
// Автоматический таймаут
|
|||
|
|
Timer(_timeout, () {
|
|||
|
|
if (_callbacks.containsKey(id)) {
|
|||
|
|
_callbacks[id]!.completeError(
|
|||
|
|
BridgeException('Request timeout: $id')
|
|||
|
|
);
|
|||
|
|
_callbacks.remove(id);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return id;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void completeCallback(String id, BridgeMessage response) {
|
|||
|
|
if (_callbacks.containsKey(id)) {
|
|||
|
|
_callbacks[id]!.complete(response);
|
|||
|
|
_callbacks.remove(id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 7: Примеры использования (1-2 дня)
|
|||
|
|
|
|||
|
|
#### 7.1 Flutter Host пример
|
|||
|
|
```dart
|
|||
|
|
class MyHomePage extends StatefulWidget {
|
|||
|
|
@override
|
|||
|
|
_MyHomePageState createState() => _MyHomePageState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|||
|
|
BridgeWebViewController? _bridgeController;
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return Scaffold(
|
|||
|
|
appBar: AppBar(title: Text('Flutter WebView Bridge')),
|
|||
|
|
body: BridgeWebView(
|
|||
|
|
url: 'http://localhost:8080',
|
|||
|
|
onMessage: _handleMessage,
|
|||
|
|
onBridgeReady: (controller) {
|
|||
|
|
_bridgeController = controller;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _handleMessage(BridgeMessage message) {
|
|||
|
|
switch (message.type) {
|
|||
|
|
case 'getAppVersion':
|
|||
|
|
_sendResponse(message.id!, {'version': '1.0.0'});
|
|||
|
|
break;
|
|||
|
|
case 'vibrate':
|
|||
|
|
HapticFeedback.vibrate();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.2 Flutter Web пример
|
|||
|
|
```dart
|
|||
|
|
void main() {
|
|||
|
|
WebBridge().initialize();
|
|||
|
|
runApp(MyApp());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class MyApp extends StatelessWidget {
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return MaterialApp(
|
|||
|
|
home: MyHomePage(),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class MyHomePage extends StatefulWidget {
|
|||
|
|
@override
|
|||
|
|
_MyHomePageState createState() => _MyHomePageState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|||
|
|
final _bridge = WebBridge();
|
|||
|
|
|
|||
|
|
Future<void> _getAppVersion() async {
|
|||
|
|
try {
|
|||
|
|
final response = await _bridge.sendMessage(
|
|||
|
|
BridgeMessage(
|
|||
|
|
type: 'getAppVersion',
|
|||
|
|
id: const Uuid().v4(),
|
|||
|
|
source: 'web',
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
print('App version: ${response.data?['version']}');
|
|||
|
|
} catch (e) {
|
|||
|
|
print('Error: $e');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return Scaffold(
|
|||
|
|
appBar: AppBar(title: Text('Flutter Web App')),
|
|||
|
|
body: Center(
|
|||
|
|
child: ElevatedButton(
|
|||
|
|
onPressed: _getAppVersion,
|
|||
|
|
child: Text('Get App Version'),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Этап 8: Тестирование (2-3 дня)
|
|||
|
|
|
|||
|
|
#### 8.1 Unit тесты
|
|||
|
|
```dart
|
|||
|
|
// test/bridge_test.dart
|
|||
|
|
void main() {
|
|||
|
|
group('BridgeMessage', () {
|
|||
|
|
test('should serialize and deserialize correctly', () {
|
|||
|
|
final message = BridgeMessage(
|
|||
|
|
type: 'test',
|
|||
|
|
id: '123',
|
|||
|
|
data: {'key': 'value'},
|
|||
|
|
source: 'host',
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
final json = MessageSerializer.toJson(message);
|
|||
|
|
final deserialized = MessageSerializer.fromJson(json);
|
|||
|
|
|
|||
|
|
expect(deserialized.type, equals(message.type));
|
|||
|
|
expect(deserialized.id, equals(message.id));
|
|||
|
|
expect(deserialized.data, equals(message.data));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 8.2 Integration тесты
|
|||
|
|
- Тест полного цикла сообщений
|
|||
|
|
- Тест таймаутов
|
|||
|
|
- Тест обработки ошибок
|
|||
|
|
|
|||
|
|
### Этап 9: Документация (1-2 дня)
|
|||
|
|
|
|||
|
|
#### 9.1 README.md
|
|||
|
|
- Описание пакета
|
|||
|
|
- Установка и настройка
|
|||
|
|
- Примеры использования
|
|||
|
|
- API документация
|
|||
|
|
|
|||
|
|
#### 9.2 API документация
|
|||
|
|
- Документирование всех публичных классов и методов
|
|||
|
|
- Примеры кода
|
|||
|
|
- Troubleshooting
|
|||
|
|
|
|||
|
|
## 🚀 План релиза
|
|||
|
|
|
|||
|
|
### v0.1.0 - Alpha
|
|||
|
|
- Базовая функциональность
|
|||
|
|
- Request-response модель
|
|||
|
|
- Простые примеры
|
|||
|
|
|
|||
|
|
### v0.2.0 - Beta
|
|||
|
|
- Event-notify модель
|
|||
|
|
- Стримы для подписок
|
|||
|
|
- Улучшенная обработка ошибок
|
|||
|
|
|
|||
|
|
### v1.0.0 - Stable
|
|||
|
|
- Полная документация
|
|||
|
|
- Тесты покрытие >80%
|
|||
|
|
- Примеры для всех сценариев
|
|||
|
|
|
|||
|
|
## 📝 Чек-лист готовности
|
|||
|
|
|
|||
|
|
- [ ] Структура пакета создана
|
|||
|
|
- [ ] Основные модели реализованы
|
|||
|
|
- [ ] Flutter Host сторона работает
|
|||
|
|
- [ ] JavaScript bridge интегрирован
|
|||
|
|
- [ ] Flutter Web сторона работает
|
|||
|
|
- [ ] Примеры созданы и работают
|
|||
|
|
- [ ] Тесты написаны и проходят
|
|||
|
|
- [ ] Документация готова
|
|||
|
|
- [ ] pubspec.yaml настроен
|
|||
|
|
- [ ] README.md написан
|
|||
|
|
|
|||
|
|
## 🔧 Технические детали
|
|||
|
|
|
|||
|
|
### Зависимости
|
|||
|
|
- `flutter_inappwebview`: для WebView функциональности
|
|||
|
|
- `uuid`: для генерации уникальных ID
|
|||
|
|
- `js`: для взаимодействия с JavaScript в Flutter Web
|
|||
|
|
|
|||
|
|
### Поддерживаемые платформы
|
|||
|
|
- ✅ Android
|
|||
|
|
- ✅ iOS
|
|||
|
|
- ✅ Web (Flutter Web в WebView)
|
|||
|
|
- ❌ Desktop (не планируется)
|
|||
|
|
- ❌ Linux (не планируется)
|
|||
|
|
|
|||
|
|
### Производительность
|
|||
|
|
- Минимальная задержка сообщений
|
|||
|
|
- Автоматическая очистка callback'ов
|
|||
|
|
- Таймауты для предотвращения утечек памяти
|