프로그래밍/flutter

[flutter] native(앱) 의 웹뷰(vue3) 양방향 통신 방법 webview_flutter 사용

소행성왕자 2023. 8. 8. 15:29

webview_flutter : 안드로이드에서는 정상 작동하지만 iOS 에서는 alert 을 비롯한 알수없게 작동안된다.

=> InAppWebViewController 로 변경

 

목적 : flutter 와 웹뷰(vue3) 의 양방향 통신방법에 대해서 알아본다.

전제 조건 : vue3 에서는 쉐워드워커, 워커를 사용하기 때문에 워커에서는 flutter 의 웹뷰로 의 양방향 통신은 불가능하다.

프로세스 흐름 - input (request)

  1. ui 에서 Tr 클릭 (n_flutter.vue)
  2. Tr input 객체를 만들어 Worker 로 전송 $Worker.send(data); (n_flutter.vue)
  3. js/index.js 의 send 함수에서 웹인지 앱인지 체크 (/js/index.js)
  4. inputHexString 을 만들어 JSBridge 채널을 통해 flutter 웹뷰의 onMessageReceived 전송 (main.dart : JsBridge.postMessage(JSON.stringify(message)); ) 
  5. JsBridge 로 전달받은 inputHexString 을 decode 한 후 연결된 소켓이 전송 ( SocketManager.dart : List<int> bytes = HEX.decode(message); )

프로세스 흐름 - output (response)

  1. mymq 에서 응답 데이터를 flutter 소켓 리시버에서 받음 (SocketManager.dart : Future<void> _listenToSocket() async)
  2. 바이너리 int 데이터(List<int> data)를 hexString 으로 변환 ( _hexString = HEX.encode(data) )
  3. 어떤 플랫폼인지 알기 위해 action 에 flutter 와 응답받은 resHexSting 을 객체로 변환 ( String jsonString = json.encode(jsonObject) )
  4. 변화된 jsonString 을 웹뷰(vue3)로 넘겨준다 ( _controller.runJavaScript("flutter2web('$jsonString')"); )
  5. vue3 에서는 flutter 에서 전송된 데이터를 받기 위한 flutter2web 함수가 정의 되어 있음 ( js/flutter.js )
  6. Worker 의 reveiveFluterApp 함수로 전달 (js/flutter.js : $Worker.reveiveFluterApp(data); )
  7. app 으로 받은 hexString 데이터 를 output 객체와 value binding 을 위해 워커로 전송  (reviceApp 메소드)
  8. 워커의 reviceApp 에서는 인자로 받은 hexString 을 blob 으로 변환하여 recivetHex(헥사스트링), reciveString(사람이 확인할수 있는 데이터) 데이터를 만들어 UI 로 전송 (js/worker/webSocket_Worker.js)
  9. this.worker.swk.onmessage  워커로부터 받은 데이터를 분석하여 emitter 로 전송 ( js/index.js )

위에 흐름에서도 보면 flutter 와 웹뷰의 통신 데이터는 hexString 으로 전송되고 전송 받는다.

main.dart

/*
 * https://pub.dev/packages/webview_flutter
 * https://kbwplace.tistory.com/176
 *
 */

import 'dart:convert';
import 'dart:developer';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'SocketManager.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {

  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(

        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  /*
   * tcp/ip 소켓 connect / send / close Class
   */
  final socketManager = SocketManager();

  /*
   * 웹뷰 컨트롤러 설정
    JavaScriptMode.disabled: WebView에서 JavaScript를 비활성화합니다. 웹 페이지의 JavaScript 코드가 실행되지 않습니다.
    JavaScriptMode.unrestricted: WebView에서 JavaScript를 활성화합니다. 웹 페이지의 JavaScript 코드가 실행되며, JavaScript로 인터랙티브한 동작이 가능해집니다.
    setBackgroundColor(const Color(0x00000000)): 이 부분은 WebView의 배경 색상을 설정하는 것입니다. 여기서 Color(0x00000000)은 투명한 배경색을 나타냅니다. 즉, WebView가 투명한 배경을 가지게 됩니다.
   */
  late final controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setBackgroundColor(const Color(0x00000000))
    ..setNavigationDelegate(
      NavigationDelegate(
        onProgress: (int progress) {
          // update loading bar.
        },
        onPageStarted: (String url) {},
        onPageFinished: (String url) {},
        onWebResourceError: (WebResourceError error) {},
        onNavigationRequest: (NavigationRequest request) {
          if (request.url.startsWith('https://www.youtube.com/')) {
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },
      ),
    )

    /*
      addJavaScriptChannel('JsBridge', onMessageReceived: (JavaScriptMessage message) { ... }): 이 부분은 WebView에 JavaScript 채널을 추가하는 것입니다.
      채널은 JavaScript와 Flutter 사이의 통신을 가능하게 해주는 메커니즘입니다.
      여기서 'JsBridge'는 채널의 이름을 나타냅니다.
      이 이름을 기반으로 JavaScript에서 해당 채널을 호출하고, Flutter에서 해당 채널로 메시지를 수신할 수 있습니다.
      onMessageReceived: (JavaScriptMessage message) { ... }:
      이 부분은 JavaScript에서 Flutter로 메시지를 전달할 때 호출되는 콜백 함수를 정의하는 것입니다.
      JavaScriptMessage는 flutter_inappwebview 라이브러리에서 제공되는 클래스로서, JavaScript에서 전달된 메시지를 Flutter에서 받을 때 사용합니다.
      따라서 addJavaScriptChannel을 사용하여 'JsBridge'라는 이름의 채널을 추가하고, 해당 채널로 메시지가 전달되면 onMessageReceived 콜백 함수가 호출됩니다.
      이때, message 매개변수를 통해 JavaScript에서 전달된 메시지를 받을 수 있습니다.
      실제 웹페이지의 javascript 확인해보면 JsBridge 로 된 메시지가 있습니다.
     */
    ..addJavaScriptChannel('JsBridge', onMessageReceived: (JavaScriptMessage message) {
      /*
        javascript send() 클릭시 여기로 전송된다 message 는 json 형태
       */
      _web2flutter(message);
    })
    ..loadRequest(
      Uri.parse('http://192.168.0.6:5173/')
    );




  /*
   * 사용자 정의 함수
   */

  _reload() {
    controller.reload();
  }

  _web2flutter(JavaScriptMessage message) async {
    /*
     * json 형태를 객체로 변경
     */
    Map<String, dynamic> parsedJson = jsonDecode(message.message);

    var action = parsedJson['action'];
    var inputHexString = parsedJson['hexString'];

    var receivedHexString = '';

    switch(action) {
      case 'connect':
        connect();
        break;
      case 'sendTr' :
        sendTr(inputHexString);
        break;
    }
  }

  dd(String str) {
    if (kDebugMode) {
      print(str);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView 11'),
      ),
      body: WebViewWidget(
        controller: controller,
      ),
    );
  }

  @override
  void reassemble() {
    super.reassemble();
    _reload();
  }

  /*
   StatefulWidget이 생성될 때 한 번 호출되는 생명주기 메서드로,
   해당 위젯이 생성되었을 때 초기화 작업을 수행하는데 사용됩니다. 따라서 이곳에 초기 실행해야 할 로직을 넣으면 됩니다.
   */
  @override
  void initState() {
    super.initState();

    // 이곳에 초기 실행해야 할 로직을 추가합니다.
    socketManager.connectToSocket(controller);    // 소켓 접속한다.

  }

  Future<void> connect() async {
    await socketManager.connectToSocket(controller);    // 소켓 접속한다.
    var receivedConnected = 'connected : ${socketManager.connected}';
    print(receivedConnected);
    controller.runJavaScript("flutter2web('$receivedConnected')");
  }

  Future<void> sendTr(sendHexString) async {
    socketManager.sendTr(sendHexString);  // 접속된 소켓에 데이터를 전송한다. input
  }
}

SocketManager.dart

import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:hex/hex.dart';
import 'package:webview_flutter/src/webview_controller.dart';

class SocketManager {
  late Socket _socket;
  bool _connected = false;
  late String _hexString = '';
  late WebViewController _controller;


  static const String host = "13.125.57.";  // mymq
  static const int port = 900;


  Future<void> connectToSocket(WebViewController controller) async {
    try {
      _socket = await Socket.connect(host, port);
      print('Socket connected');

      _controller = controller;
      _connected = true;

      _listenToSocket();
    } catch (e) {
      print('Failed to connect to socket: $e');
      _connected = false;
    }
  }


  Future<void> _listenToSocket() async {
    // 데이터를 받을 버퍼를 초기화
    List<int> buffer = [];
    _socket.listen(
      (List<int> data) async {
        // 받은 데이터를 버퍼에 추가
        buffer.addAll(data);

        // 전체 데이터 길이 정보가 도착한 경우
        if (buffer.length >= 4) {
          /**
           * 전체 데이터 길이를 얻음
           * buffer[0], buffer[1], buffer[2], buffer[3] 각각은 1바이트 크기의 정수 값입니다. buffer 배열의 첫 4바이트를 나타냅니다.
              << 연산자는 비트를 왼쪽으로 이동시키는 연산입니다. buffer[0] << 24는 buffer[0]의 비트를 왼쪽으로 24비트 이동시킨 값을 의미합니다.
              마찬가지로 buffer[1] << 16, buffer[2] << 8, buffer[3]은 각각 비트를 왼쪽으로 16, 8, 0비트 이동시킨 값을 나타냅니다.
              | 연산자는 비트별 OR 연산을 수행합니다. 위의 네 개의 이동 연산을 수행한 결과를 비트별 OR 연산하여 하나의 정수 값으로 합쳐줍니다.
           */
          int length = (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];

          // 버퍼에 모든 데이터가 도착한 경우
          if (buffer.length >= length + 4) {
            // 데이터를 추출하고 사용
            List<int> receivedData = buffer.sublist(0, length + 4);
            _hexString = HEX.encode(receivedData);
            print('flutter 에서 데이터 수신');
            print(_hexString);

            // 사용한 데이터는 버퍼에서 삭제
            buffer.removeRange(0, length + 4);

            // 비동기 작업을 수행하고자 하는 경우, await 키워드로 Future를 기다릴 수 있습니다.
            await someAsyncTask();
            // 비동기 작업 후 추가적인 코드
          }
        }
      },
      onError: (e) {
        print('Socket error: $e');
        _disconnect();
      },
      onDone: () {
        print('Socket disconnected');
        _disconnect();
      },
      cancelOnError: false,
    );
  }


/*
  Future<void> _listenToSocket() async {
    _socket.listen(
          (List<int> data) async {
            // 데이터 수신 처리
            _hexString = HEX.encode(data);
            print('flutter 에서 데이터 수신');
            print(_hexString);

        // 비동기 작업을 수행하고자 하는 경우, await 키워드로 Future를 기다릴 수 있습니다.
        await someAsyncTask();
        // 비동기 작업 후 추가적인 코드
      },
      onError: (e) {
        print('Socket error: $e');
        _disconnect();
      },
      onDone: () {
        print('Socket disconnected');
        _disconnect();
      },
      cancelOnError: false,
    );
  }*/



  Future<void> someAsyncTask() async {
    // 비동기 작업을 수행하는 함수
    Map<String, dynamic> jsonObject = {
      'action': 'flutter',
      'resHexString': _hexString,
    };

    String jsonString = json.encode(jsonObject);
    _controller.runJavaScript("flutter2web('$jsonString')");

  }


  Future<void> sendData(String message) async {
    if (!_connected) {
      print('Socket is not connected');
      return;
    }

    try {
      _socket.write(message);
      await _socket.flush();
      print('Sent: $message');
    } catch (e) {
      print('Failed to send data: $e');
      _disconnect();
    }
  }

  Future<void> sendTr(String message) async {
    if (!_connected) {
      print('Socket is not connected');
      return;
    }

    List<int> bytes = HEX.decode(message);

    try {
      _socket.add(bytes);
      await _socket.flush();
      print('Sent: $message');
    } catch (e) {
      print('Failed to send data: $e');
      _disconnect();
    }
  }

  void _disconnect() {
    _socket?.close();
    _connected = false;
  }

  String get hexString {
    return _hexString;
  }

  bool get connected {
    return _connected;
  }
}

html javascript

<button type="button" @click="connect()">connect</button>


const connect =_=>{
    Common.debugApp('connecting....');
    const message = {
        key: 'value',
        action: 'connect',
        hexString: 'aa',
    };
    JsBridge.postMessage(JSON.stringify(message));
};

JS -> flutter 데이터 전송

webview_flutter 사용시

    JsBridge.postMessage(JSON.stringify(message)); 

InAppWebViewController 사용시 

    window.flutter_inappwebview.callHandler('JsChannel', JSON.stringify(message));

 

InAppWebView 사용한 웹뷰