[Flutter] Flutter WebView Two Way Communication

Parrottkim·2023년 11월 6일
0

시작하기에 앞서

Flutter로 개발하다보면 결제나 지도와 같이 Javascript로 작성된 웹 페이지와 데이터를 주고 받아야 하는 경우가 생깁니다. 이번 포스트에서는 Flutter 모바일과 웹 모두에서 웹 페이지 간 통신 방법에 대해 알아보겠습니다.

패키지 추가

dependencies:
  flutter:
    sdk: flutter

...

webview_flutter: ^4.4.2

먼저, 모바일에서 WebView를 사용하기 위한 pubspec.yamlwebview_flutter 패키지 종속성을 추가합니다.

HTML 파일

flutter:

  ...

  assets:
    - assets/view.html

Flutter에서 html 파일을 사용하기 위해 pubspec.yaml에 해당 문구를 추가합니다.

<!DOCTYPE html>
<html>

<head>
  <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
  <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      height: 100%;
      width: 100%;
      padding: 20px;
      padding-top: 36px;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .input-container {
      position: relative;
      max-width: 500px;
      width: 100%;
    }

    .text-input {
      width: 100%;
      height: 36px;
      padding: 10px;
      border: 1px solid #ccc;
      border-radius: 5px;
      font-size: 16px;
    }

    .button {
      width: 100%;
      height: 36px;
      margin-top: 16px;
      background-color: #007BFF;
      color: #fff;
      border: none;
      border-radius: 5px;
      font-size: 16px;
      cursor: pointer;
    }

    .button:hover {
      background-color: #0056b3;
    }
  </style>
</head>

<body>
  <div class="input-contianer">
    <input type="text" class="text-input" id="text-input" placeholder=" ">
    <button class="button">전송</button>
  </div>
</body>

</html>

assets/lib 경로에 텍스트 필드와 버튼으로 구성된 view.html을 생성합니다.

Flutter 구현

먼저 lib/widget에 2가지 파일을 만들어야합니다.

하나는 모바일에서 사용할 input_webview_mobile.dart, 하나는 웹에서 사용할 input_webview_web.dart입니다.

파일을 2가지로 나누어 모바일과 웹 위젯을 따로 만드는 이유는, webview_flutter 패키지가 웹 환경을 지원하지 않아 웹에서는 HtmlElementView으로 구현해야하기 때문입니다.

input_webview_mobile.dart

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

class InputWebView extends StatefulWidget {
  const InputWebView({super.key});

  
  State<InputWebView> createState() => _InputWebViewState();
}

class _InputWebViewState extends State<InputWebView> {
  late final WebViewController controller;

  
  void initState() {
    super.initState();

    controller = WebViewController()
      ..loadFlutterAsset('assets/view.html');
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('이름 입력하기')),
      body: WebViewWidget(
        controller: controller,
      ),
    );
  }
}

input_webview_web.dart

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

import 'package:flutter/material.dart';

class InputWebView extends StatefulWidget {
  const InputWebView({super.key});

  
  State<InputWebView> createState() => _InputWebViewState();
}

class _InputWebViewState extends State<InputWebView> {
  
  void initState() {
    super.initState();

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(
      'input-webview',
      (int viewId) => html.IFrameElement()
        ..style.width = '100%'
        ..style.height = '100%'
        ..src = 'assets/view.html'
        ..style.border = 'none',
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('이름 입력하기')),
      body: HtmlElementView(
        viewType: 'input-webview',
      ),
    );
  }
}

두 위젯 모두 클래스명을 InputWebView으로 만들고, 조건부 import를 이용해 플랫폼에 따라 위젯을 분기해줘야 합니다. 이 방법은 후술하겠습니다.

완성한 모바일, 웹 각각의 화면은 다음과 같습니다.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_webview_communication/widget/input_webview_mobile.dart'
    if (dart.library.html) 'package:flutter_webview_communication/widget/input_webview_web.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => _showWebView(context),
          child: Text('이름 입력하기'),
        ),
      ),
    );
  }

  Future<void> _showWebView(BuildContext context) {
    return Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => InputWebView()));
  }
}
import 'package:flutter_webview_communication/widget/input_webview_mobile.dart'
    if (dart.library.html) 'package:flutter_webview_communication/widget/input_webview_web.dart';

다음 부분에서 input_webview_mobile.dart import하고, 만약 dart.library.html을 사용하는 경우 input_webview_web.dart을 import하게 됩니다.

kIsWeb만으로 분기하기에는 여러가지 문제점이 있기 때문에 다음과 같이 조건부 import를 이용해서 분기해줍니다.

자세한 내용은 다음 링크를 확인해주세요.

Javascript → Flutter

먼저 각 InputWebView에 Javascript Callback을 추가하고, view.html에서 버튼을 눌렀을 때 이벤트를 등록하는 방식으로 구현합니다.

input_webview_mobile.dart


void initState() {
  super.initState();

  controller = WebViewController()
  
    ... (중략)
    
    ..addJavaScriptChannel('messageHandler', onMessageReceived: (message) {
      showDialog(
	    context: context,
	    builder: (context) => AlertDialog(title: Text(message.message)),
      );
    })
    ..loadFlutterAsset('assets/view.html');
}
..addJavaScriptChannel('messageHandler', onMessageReceived: (message) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(title: Text(message.message)),
  );
})

다음 문구를 통해 WebViewControllerJavascriptChannel을 추가합니다. messageHandler라는 JavascriptChannel을 만들고, messageHandler 채널을 통해 메시지를 수신하면 Callback이 호출됩니다.

Callback이 호출되면 해당 메시지를 AlertDialog로 표시합니다.

view.html

<script>
  const button = document.querySelector("button");
  const input = document.querySelector("input");

  button.addEventListener("click", (event) => {
    messageHandler.postMessage(input.value);
  });
</script>

</body> 뒤에 다음의 스크립트를 작성합니다. 버튼이 클릭되면, postMessage를 사용해 messageHandler JavascriptChannel에 메세지를 전송합니다.

input_webview_web.dart


void initState() {
  super.initState();

  ... (중략)

  html.window.onMessage.listen((event) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(title: Text(event.data)),
    );
  });
}

view.html에서 postMessage가 호출될 때 실행되는 이벤트 Callback을 작성하고 Callback이 호출되면 해당 메시지를 AlertDialog로 표시합니다.

view.html

<script>
  const button = document.querySelector("button");
  const input = document.querySelector("input");

  button.addEventListener("click", (event) => {
    window.parent.postMessage(input.value, '*');
  });
</script>

브라우저의 부모 창에 postMessage를 사용해 메시지를 전송합니다.

Flutter → Javascript

view.html 페이지 로드가 끝났을 때, 각 InputWebView에서 postMessage를 보내는 방식으로 구현합니다.

input_webview_mobile.html


void initState() {
  super.initState();

  controller = WebViewController()

    ... (중략)
			
    ..setNavigationDelegate(
        NavigationDelegate(
            onPageFinished: (value) => {
                  controller.runJavaScript(
                      'messageHandler.postMessage(\'hello\', \'*\')')
                }),
      );
  }

NavigationDelegate라는 콜백 함수 델리게이트를 이용해 웹 페이지의 상태가 로드가 완료된 상태이면, controller.runJavaScript('messageHandler.postMessage(\'hello\', \'*\')')으로 웹 페이지에 메시지를 전달합니다.

view.html

messageHandler.addEventListener('message', (event) => {
  alert(event.data);
});

messageHandler라는 JavascriptChannel으로 전달된 메시지가 있을 때, 이벤트가 발생되어 해당 메시지를 경고 대화 상자로 표시합니다.

input_webview_web.dart


void initState() {
  super.initState();

  ... (중략)

  html.window.postMessage('hello', '*');
}

브라우저에 postMessage를 이용해 메시지를 전달합니다.

view.html

window.parent.addEventListener('message', (event) => {
  console.log(event);
  alert(event.data);
});

부모 창에서 전달된 메시지가 있을 때, 이벤트가 발생되어 해당 메시지를 경고 대화 상자로 표시합니다.

이제, Flutter와 웹 페이지 사이 양방향 통신이 가능한 앱이 완성되었습니다.

Github Repository

profile
Flutter developer

0개의 댓글