Flutter WebView 이미지 업로드 (feat.Base64)

강정우·2023년 10월 14일
0

Flutter&Dart

목록 보기
67/87
post-thumbnail

구현 개념

  • Flutter에서 웹뷰로 이미지 업로드하는 방법은 일반적으로 웹 뷰에 포함된 HTML 파일 내에 Flutter의 이미지 등록 함수를 window 함수로 연결하고, Flutter의 WebView 클래스에서 이를 사용하여 이미지 파일 선택 다이얼로그를 띄우고 선택한 이미지 파일을 반환 받아서 네이티브와 통신하는 것이다.

  • 예를 들어, Flutter에서 WebView 위젯을 구성할 때, onWebViewCreated 콜백을 사용하여 웹뷰 객체를 생성하고,
    이 객체를 사용하여 JavaScript와 통신할 수 있다.

  • 이것은 WebView에서 window.flutter_inappwebview.callHandler(handlerName, args) 메소드를 사용하여 호출 할 수 있다.

  • 그리고 네이티브 측에서는 InAppWebView.onWebViewCreated 이벤트 핸들러에서 WebView의 초기화가 완료되었을 때, WebViewController 객체를 얻어서 JavaScript에서 호출되는 함수를 구현하고, 이를 사용하여 네이티브 측으로 데이터를 전달할 수 있다.

구현 코드 (html)

  • 그럼이제 위 로직을 코드로 구현해보자.
    우선 webview 단의 html 코드를 우선 작성해보자.
<template>
	<button @click="gettingImageData">Getting Img data</button>
    <img :src="pickedImg"/>
</template>
<script setup>
const pickedImg = ref();

// 핸드폰 이미지 select 기능을 사용하기위해 
const gettingImageData = () => {
  window.flutterGetImage.postMessage('getImageData');
};

// onMounted로 플러터에서 넘어오는 base64 데이터 -> blob 데이터로 변환
// 추후 flutter 에서 getImageData를 보내면 window함수로 받아오는데 이게 동작한다.
onMounted(() => {
  window.getImageData = function (base64Data) {
    const binaryString = atob(base64Data);
    const uint8Array = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      uint8Array[i] = binaryString.charCodeAt(i);
    }
    const blob = new Blob([uint8Array], { type: "image/jpeg" });
    pickedImg.value = URL.createObjectURL(blob);
  };
});
</script>

base 64란

  • 그럼 여기서 잠깐 base 64에 대해 알아보자.
    Base 64 란 8비트 2진 데이터를 (플랫폼의) 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 가리키는 개념이다.

  • Base 64 는 데이터를 64진법 으로 나타낸다.
    이를 0부터 63까지 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ 으로 나타낸다.

데이터를 Base64로 바꾸는 과정

  1. 24비트 버퍼에 위쪽(MSB)부터 1바이트 (8비트)씩 3바이트를 채운다.
  2. 3바이트 보다 미만이라면, 버퍼의 남은 부분은 0으로 채워넣는다.
  3. 버퍼의 위쪽부터 6비트씩 잘라 그 값을 읽어, Base 64 의 값으려 변경한다.
  4. 버퍼의 남은 부분을 0으로 채운 값을 1바이트당 = 코드로 변경한다.

Javascript 에서의 Base 64 변환

  • WindowOrWorkerGlobalScope 에 base64 utility method 로 btoa 과 atob 가 구현되어 있다.

btoa(DOMString data): DOMString;

  • btoa() 은 입력 문자열을 Base 64 으로 표현되는 문자열을 반환한다.
    만약 입력 문자열에 유니 코드 같은 btoa 에서 이해할 수 없는 문자열이 들어오면 InvalidCharacterError 가 발생한다.

atob(DOMString data): ByteString;

  • atob() 은 인코딩된 Base 64 문자열을 디코드한다.
    만약 입력 문자열에 Base 64 에 포함되지 않는 문자 (A-Z,a-z,0–9+/ 이외) 가 입력되면 DOMException 이 발생한다.

  • 예를 들면

window.btoa('Man');
// TWFu
window.btoa('TWFu');
// Man
window.btoa('\u');
// Uncaught SyntaxError: Invalid Unicode escape sequence
window.atob('🙂');
// Uncaught DOMException: Failed to execute 'atob' on 'Window'
  • 이때 유니코드를 Base 64 로 변환해야할 경우, 일반적으로 InvalidCharacterError 예외 가 발생 한다.
    하지만 유니코드를Uniform Resource Identifier (URI) 컴포넌트로 변경하고 다시 Base 64 변경한다면, 가능하다.
    encodeURIComponent() 와 decodeURIComponent 를 이용하여, 유니코드 를 URI 으로 변경하여 사용할 수 있다.
var uni = '🙂';
var data = encodeURIComponent(uni);
// %F0%9F%99%82
var encode = window.btoa(data);
// JUYwJTlGJTk5JTgy
var decode = window.atob(encode);
// %F0%9F%99%82
decodeURIComponent(decode);
// 🙂

구현 코드 (flutter)

  • 그럼이제 native 코드를 작성해보자. 우선 imagePicker를 다운받아주자.

동작과정

  • WebView에서 window.fileChooser 채널(여기서는 window.flutterGetImage.postMessage('getImageData');)을 사용하여 Flutter의 ImagePicker(여기서는 flutterGetImage)를 호출한다.
class HomeScreen extends StatefulWidget {
  final String? token;

  const HomeScreen({required this.token, Key? key}) : super(key: key);

  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  WebViewController? webViewController;
  Set<JavascriptChannel>? channel;
  final homeUrl = '본인 주소';
  int index = 0;
  String? _token;

  
  void initState() {
    super.initState();
    _token = widget.token;
  }

  
  void dispose() {
    super.dispose();
  }

  
  Widget build(BuildContext context) {

    void _getImage() async {
      final ImagePicker _picker = ImagePicker();
      final imageFile = await _picker.pickImage(source: ImageSource.gallery);

      if (imageFile != null) {
        final Uint8List bytes = await imageFile.readAsBytes();
        final String base64 = base64Encode(bytes);
        webViewController!.runJavascript('window.getImageData("$base64")');
      }
    }

    return DefaultLayout(
      child: FutureBuilder(
          future: checkPermission(),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
            if (snapshot.data == '위치 권한이 허가되었습니다.') {
              return StreamBuilder<Position>(
                stream: Geolocator.getPositionStream(),
                builder: (context, snapshot) {
                  if(snapshot.hasData) {}
                  return WebView(
                    initialUrl: homeUrl,
                    onWebViewCreated: (WebViewController webViewController) {
                      this.webViewController = webViewController;
                    },
                    javascriptMode: JavascriptMode.unrestricted,
                    javascriptChannels: <JavascriptChannel>{
                      JavascriptChannel(
                        name: 'flutterGetImage',
                        onMessageReceived: (JavascriptMessage message) {
                          _getImage();
                        },
                      ),
                    },
                  );
                },
              );
            }
            return const Center(child: Text('연결이 끊겼습니다.'));
          }),
    );
  }
}

Future checkPermission() async {
  final isLocationEnabled = await Geolocator.isLocationServiceEnabled();

  if (!isLocationEnabled) {
    return '위치 서비스를 활성화 해주세요.';
  }

  LocationPermission checkPermission = await Geolocator.checkPermission();

  if (checkPermission == LocationPermission.denied) {
    checkPermission = await Geolocator.requestPermission();

    if (checkPermission == LocationPermission.denied) {
      return '위치 권한을 허가해주세요.';
    }
  }

  if (checkPermission == LocationPermission.deniedForever) {
    return '앱의 위치 권한을 세팅에서 허가해주세요.';
  }

  return '위치 권한이 허가되었습니다.';
}

reference

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글