mnemo_cards/games/flutter_webview_bridge_plan.md
2025-11-11 02:55:41 +03:00

14 KiB
Raw Permalink Blame History

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                     # Тесты

📋 Структура сообщений

Модель сообщения

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 Создание структуры пакета

flutter create --template=package flutter_webview_bridge

1.2 Основные модели

  • BridgeMessage - модель сообщения
  • BridgeConfig - конфигурация моста
  • BridgeException - исключения моста

1.3 Зависимости в pubspec.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 контроллер

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

void _setupJavaScriptHandler() {
  _webViewController?.addJavaScriptHandler(
    callbackName: 'fromWebApp',
    callback: (args) {
      final message = BridgeMessage.fromJson(args[0]);
      _handleIncomingMessage(message);
    },
  );
}

2.3 Отправка сообщений в WebView

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

// Подписка на события от 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

<!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 класс

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 Обработка сообщений

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 виджет

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 Основной экспорт

// 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

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

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 пример

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 пример

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 тесты

// 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'ов
  • Таймауты для предотвращения утечек памяти