티스토리 뷰

Tip and Error

Flutter 웹뷰 뒤로가기 (w. Webview_flutter)

geonwoopaeng@gmail.com 2024. 3. 26. 22:50

Flutter 웹뷰 뒤로 가기 + Webview_flutter

웹에서 window.open을 이용해서 새창을 띄웁니다.
그렇게 되면 기존에 띄어져 있는 웹이 변경됩니다.

Android, iOS 뒤로가기를 이용해서 문제를 해결하였습니다.

이전 상황을 간단하게 설명하면 웹뷰로 만들어진 앱에서 새로운 페이지로 이동을 시키려고 했습니다.
그래서 Modal을 만들어 이용하고 특정 url을 중간에 가로채서 버그들을 수정했습니다.
이렇게 처리를 하게되면 Flutter 코드를 계속적으로 봐야 하는 귀찮음이 많았습니다.
(그리고 Flutter 개발자가 없다...)

이로인해 Flutter을 간단하게만 볼 수 있고 수정은 최대한 안 하는 방식으로 생각한 방법입니다.

Android와 iOS 둘다 뒤로 가기의 형태가 다릅니다.

Android: 뒤로가기 버튼

iOS: 좌우로 드래그

그래서 각각 적용을 해줘야 합니다.

기존 틀

우선 설명에 앞서 기존 틀에대해 설명을 하겠습니다.

webview_flutter을 사용하여 웹뷰를 만들었습니다.
실행할 url에 parameter을 붙여서 기기에 대한 정보를 전달하였습니다.

그래서 FutureBuilder 와 같은 위젯이 사용되었으니 고려하며 보시기 바랍니다.

Android

Android에서는 뒤로 가기 버튼이 존재합니다.

이 버튼을 코드 없이 사용하면 앱이 꺼져버리는 현상이 일어납니다.
그래서 WillPopScope을 이용해야 합니다.

  // 전 화면으로 이동하는 함수
  Future<bool> goBack(BuildContext context) async {
    if (controller == null) {
        return true;
    }

    if (await controller.canGoBack()) {
        controller!.goBack();
        return Future.value(false);
    } else {
        return Future.value(true);
    }
  }

  // android 뒤로가기 handling 하는 함수
  Widget androidBackHandling(BuildContext context) {
    return WillPopScope(
        onWillPop: () => goBack(context),
        child: Scaffold(
            body: WebViewWidget(
                controller: controller,
            )
        )
    );
  }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

iOS

iOS에서는 좌 -> 우로 드래그하여 뒤로 가게 됩니다.

여기서도 코드를 추가하지 않으면 뒤로 가기 드래그가 작동하지 않습니다.
그래서 onHorizontalDragEnd을 이용해야 합니다.
+ 추가로 Safari 에서는 사용자 Click Event 에서만 window.open 을 통한 팝업을 허용하고 있습니다.

  // 전 화면으로 이동하는 함수
  Future<bool> goBack(BuildContext context) async {
    if (controller == null) {
        return true;
    }

    if (await controller.canGoBack()) {
        controller!.goBack();
        return Future.value(false);
    } else {
        return Future.value(true);
    }
  }

  // ios 뒤로가기 handling 하는 함수
  Widget iosBackHandling(BuildContext context) {
    return GestureDetector(
        onHorizontalDragEnd: (details) {
            // onHorizontalDragEnd: drag 이벤트가 마무리 될 때 적용
            if (details.velocity.pixelsPerSecond.dx > 50) {
                // details.velocity.pixelsPerSecond.dx > 50 : 드래그 속도가 오른쪽 방향으로 50 픽셀 이상인 경우
                goBack(context);
            }
        },
        child: Scaffold(
            body: WebViewWidget(
                controller: controller
            )
        )
    );
  }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Build에 붙이기

주요 함수

...
/*
* WidgetsBindingObserver: 앱 생명주기 이벤트를 감지하기 위한 인터페이스
* */
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  late WebViewController controller;
  late Future<String> webview_device_info_url;

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

  // State 객체가 최초 생성될 때 호출되는 메소드 (한번만 호출)
  // 초기화 하는 부분 주로 사용
  @override
  void initState() {
    super.initState();
    webview_device_info_url = getWebviewDeviceInfoUrl();
  }

  // 전 화면으로 이동하는 함수
  Future<bool> goBack(BuildContext context) async {
    ...
  }

  // android 뒤로가기 handling 하는 함수
  Widget androidBackHandling(BuildContext context) {
    ...
  }

  // ios 뒤로가기 handling 하는 함수
  Widget iosBackHandling(BuildContext context) {
    ...
  }

  @override
  Widget build(BuildContext context) {
    // FutureBuilder: 비동기 처리하는 데이터를 처리한 후 위젯을 반환할 때 사용하는 위젯
    return FutureBuilder<String>(
        future: webview_device_info_url,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            //webview 기본 설정 부분
            controller = WebViewController()
              ..setJavaScriptMode(JavaScriptMode.unrestricted)
              ..loadRequest(Uri.parse(snapshot.data!));

            return Platform.isAndroid
                ? androidBackHandling(context)
                : iosBackHandling(context);
          } else {
            return Scaffold(
                body: Center(
                  child: CircularProgressIndicator(),
                )
            );
          }
        }
    );
  }
}

 

전체 코드

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:uuid/uuid.dart';
import 'package:shared_preferences/shared_preferences.dart';

// 웹을 변경할 시 변경해야할 url 입니다.
// 실행되는 url은 webview_url + parameter(device info) 입니다.
// parameter(device info)을 합치는 곳은 getWebviewDeviceInfoUrl()에 있습니다.
final String webview_url = "http://paeng.com/";

/*
* main 함수는 App을 시작할 때 처음 실행되는 함수
* */
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); //앱 실행할 준비가 완료될 때까지 기다린다.
  runApp(const MyApp());
}

/*
 * StatelessWidget: 상태가 없는 위젯
 * 한번 그려진 후 상태가 변경 X -> UI를 빠르게 그린다.
 */
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '팽',
      home: const MyHomePage(),
    );
  }
}

/*
 * StatefulWidget: 상태를 가지고 있는 위젯
 * 데이터 변경에 따라 UI 업데이트 할 수 있다.
 */
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

/*
* WidgetsBindingObserver: 앱 생명주기 이벤트를 감지하기 위한 인터페이스
* */
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  late WebViewController controller;
  late Future<String> webview_device_info_url;

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

  // State 객체가 최초 생성될 때 호출되는 메소드 (한번만 호출)
  // 초기화 하는 부분 주로 사용
  @override
  void initState() {
    super.initState();
    webview_device_info_url = getWebviewDeviceInfoUrl();
  }

  // 전 화면으로 이동하는 함수
  Future<bool> goBack(BuildContext context) async {
    if (controller == null) {
        return true;
    }

    if (await controller.canGoBack()) {
        controller!.goBack();
        return Future.value(false);
    } else {
        return Future.value(true);
    }
  }

  // android 뒤로가기 handling 하는 함수
  Widget androidBackHandling(BuildContext context) {
    return WillPopScope(
        onWillPop: () => goBack(context),
        child: Scaffold(
            body: WebViewWidget(
                controller: controller,
            )
        )
    );
  }

  // ios 뒤로가기 handling 하는 함수
  Widget iosBackHandling(BuildContext context) {
    return GestureDetector(
        onHorizontalDragEnd: (details) {
            // onHorizontalDragEnd: drag 이벤트가 마무리 될 때 적용
            if (details.velocity.pixelsPerSecond.dx > 50) {
                // details.velocity.pixelsPerSecond.dx > 50 : 드래그 속도가 오른쪽 방향으로 50 픽셀 이상인 경우
                goBack(context);
            }
        },
        child: Scaffold(
            body: WebViewWidget(
                controller: controller
            )
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    // FutureBuilder: 비동기 처리하는 데이터를 처리한 후 위젯을 반환할 때 사용하는 위젯
    return FutureBuilder<String>(
        future: webview_device_info_url,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            //webview 기본 설정 부분
            controller = WebViewController()
              ..setJavaScriptMode(JavaScriptMode.unrestricted)
              ..loadRequest(Uri.parse(snapshot.data!));

            return Platform.isAndroid
                ? androidBackHandling(context)
                : iosBackHandling(context);
          } else {
            return Scaffold(
                body: Center(
                  child: CircularProgressIndicator(),
                )
            );
          }
        }
    );
  }

  Future<String> getWebviewDeviceInfoUrl() async {
    ...
    // get DEVICE_UDID, OS_VERSION, DEVICE_MODEL and set
    return webview_url + '?DEVICE_UDID=$DEVICE_UDID&DEVICE_VERSION=$OS_VERSION&DEVICE_MODEL=$DEVICE_MODEL';
  }
}

 

마치며

이번 해결책을 찾아가면서 정답은 하나가 아니라고 생각이 되었습니다.
오히려 차선책으로 선택한 선택이 최고의 선택이 되기도 하고 선택한 것을 최고로 만들면 되겠다라는 생각도 하게 되었습니다.
좋은 방향성을 가진 개발자이자 사람이 되기 위해 노력해야 겠습니다. :) 

긴 글 읽어주셔서 감사합니다. !!

반응형
공지사항
최근에 올라온 글