배경:

테스트폰을 18.2 버전으로 업데이트 한 뒤, 잊고있었다가 똑같은 코드로 xcode로 앱을 실행시켰는데 흰 화면이 나타남.

 

증상:

플러터 기존 앱 코드를 실행했을 때, 안드로이드는 정상작동.

하지만 iOS 기기에서만 웹뷰가 처음 로드될 때 흰 화면이 나타남.

앱 실행 - 홈탭의 웹뷰가 onWebViewCreated 까지만 실행 - 흰 화면 - 다른 탭은 정상 연결 

 

로그:

Warning: -[BETextInput attributedMarkedText] is unimplemented

nw_application_id_create_self NECP_CLIENT_ACTION_GET_SIGNED_CLIENT_ID [80: Authentication error]

Failed to resolve host network app id

Invalidating grant <invalid NS/CF object> failed

Error acquiring assertion: <Error Domain=RBSServiceErrorDomain Code=1 "((target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.rendering AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.networking AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.webcontent))" UserInfo={NSLocalizedFailureReason=((target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.rendering AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.networking AND target is not running or doesn't have entitlement com.apple.developer.web-browser-engine.webcontent))}>

 

이런 경고 로그가 자꾸 나타남..

결과적으론 저 로그는 별 의미가 없긴했지만.

 

히스토리:

 

  1. Info.plist 수정
  2. Entitlements 수정
  3. Singletone 수정
  4. 웹뷰 로드 타이밍 수정
  5. Url 수정 테스트
  6. Domain exception 추가
  7. Capability 추가 수정
  8. InAppWebView 설정 수정
  9. Xcode 업데이트
  10. Firebase app name 일치 테스트
  11. Flutter clear, pub get 시도
  12. iOS 캐시 삭제 재시작 시도
  13. 웹뷰 init 완료된 직접 url 연결
  14. Flutter 업데이트

위와 같은 삽질을 했다.. 

기존에 작성한 글은 그대로 남겨둔다.

 

2025 Feb 10 

: 구글링해서 에러를 해결하기 위한 단서들을 찾았지만 별 의미 없었음

 

1. 

https://github.com/apache/cordova-ios/issues/1440

 

IOS report "Failed to resolve host network app id" error, Android is working properly · Issue #1440 · apache/cordova-ios

Bug Report Problem The Android platform app is working normally, and then we added the iOS platform. When running in the emulator, a white screen appears and the console reports "Failed to resolve ...

github.com

 

 

2.

https://developer.apple.com/forums/thread/762223?answerId=812340022#812340022

 

WKWebView can't connect to externa… | Apple Developer Forums

Finally solved: it turned out I just needed to set the 'customUserAgent' property of the WKWebView. It's not clear why this wasn't required in iOS versions prior to iOS 17.5, but in any case the behavior seems to be working correctly again with this fix!

developer.apple.com

 

InAppWebView 설정에서 다음 코드 추가

userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/605.1.15",

 

 

그런데 2번은 내겐 아무 의미가 없었다.

아직 해결하지 못해서 기록차원에서 작성

 

하.. 안드로이드는 참 잘되는데... 아이폰.....

 

 

---------

 

2025 Feb 11

: 드디어 원인을 발견했다.

iOS를 18.2 로 업데이트 해서 이전 버전의 Flutter 혹은 패키지들과 호환 오류가 발생함.

 

원인을 발견한 계기는 다음 코드를 실행.

...

onWebViewCreated: (controller) {
    // 여기에서 강제로 url 로드
    controller.loadUrl(
        urlRequest: URLRequest(url: WebUri("https://www.google.co.kr"))
    );
 },
 
 ...

 

 

이렇게 했더니 웹뷰에 탭 이벤트가 안 먹혔다.

화면은 나타나지만 탭이 안 되는 상황... 그렇게 검색했더니 드디어 정확한 원인을 확인했다..!

 

https://github.com/pichillilorenzo/flutter_inappwebview/issues/2415

 

Tap interactions not working on iOS 18.2 · Issue #2415 · pichillilorenzo/flutter_inappwebview

Is there an existing issue for this? I have searched the existing issues Current Behavior Once I interact with any widget in the application that isn't the web view, the webview stops recognizing a...

github.com

 

InAppWebView 제작자의 예제 코드:

https://github.com/flutter/flutter/issues/159911#issuecomment-2539973403

 

[CP][For 3.28 and NOT 3.27!!!][ios][platform_view] workaround for non-tappable webview · Issue #159911 · flutter/flutter

Issue Link flutter/engine#57030 Target beta Cherry pick PR Link flutter/engine#57032 Changelog Description Fix an issue on iOS 18.2 where web view's link is not tappable. Impacted Users all end cus...

github.com

 

 

해결방법 :

 

1. 흰화면 오류 해결 / whiteScreen :

 

만약 웹뷰 설정시 다음 값을 사용하고있는지 확인하자.

useShouldOverrideUrlLoading: true,

 

 

값이 true 일 때 웹뷰 구현코드에 "shouldOverrideUrlLoading"이 작성되어있어야 했다.

 

@override
Widget build(BuildContext context) {
return Scaffold(
        appBar: AppBar(title: const Text("Example")),
        body: SafeArea(
        child: Column(children: <Widget>[
        Expanded(
                child: Stack(
                  children: [
                    InAppWebView(
                      key: webViewKey,
                      initialUrlRequest:
                      URLRequest(url: WebUri("https://flutter.dev/")),
                      initialSettings:
                      InAppWebViewSettings(isInspectable: kDebugMode),
                      onWebViewCreated: (controller) {
                        webViewController = controller;
                      },
                      onLoadStart: (controller, url) {
                        setState(() {
                          this.url = url.toString();
                          urlController.text = this.url;
                        });
                      },
                      onLoadStop: (controller, url) async {
                        setState(() {
                          this.url = url.toString();
                          urlController.text = this.url;
                        });
                      },
                      // 여기서부터 필요한 코드
                      shouldOverrideUrlLoading: (controller, navigationAction) {
                        return NavigationActionPolicy.ALLOW;
                      },
                      ...

 

만약 사용중이라면?

저 코드때문에 iOS 기기에서 흰화면 오류가 발생한다.

 

저 코드가 원인이었던 것!!!!!!

 

하지만 나는 android에서는 저 코드가 필요했기때문에 다음과 같이 수정했다.

 

shouldOverrideUrlLoading: Platform.isAndroid ?
    (controller, navigationAction) async {
  var uri = navigationAction.request.url!;
  // 기존 코드
  return NavigationActionPolicy.ALLOW;
} : null,

 

이러면 문제 해결 !

 

 

iOS 업데이트 할때마다 참 즐겁다..

 

 

 

2. 웹뷰에서 터치 오류 해결:

 

- Flutter Upgrade 3.27.4

https://flutter-ko.dev/development/tools/sdk/releases

 

Flutter SDK archive

All current Flutter SDK releases: stable, beta, and main.

docs.flutter.dev

 

- Xcode Upgrade 16.2

 

 

Xcode 16.2 버전부터 iOS 18.2 버전 케어하니까 업데이트를 해주자..

 

FCM 구현은 잘 적힌 글이 많으니 생략하겠습니다.

 

- 구현하고싶은 알림 화면 ( Heads-up ) push notification image

 

구현하는데 애먹은 부분은 안드로이드에서 FCM 을 받았을때

앱이 종료/비활성화 일때 다음과 같이 배너 알림 (혹은 heads-up 알림) 띄우기였습니다.

 

결론부터 말하자면 해결책은 백엔드에 있다!!

 

 

다음은 구글에서 안내하는 서버 알림 예제입니다.

https://firebase.google.com/docs/cloud-messaging/migrate-v1?_gl=1*14deht6*_up*MQ..*_ga*MjA1NTg3NzcwNS4xNzI4MzU0MjAz*_ga_CW55HF8NVT*MTcyODM1NDIwMy4xLjAuMTcyODM1NDIyMC4wLjAuMA..

 

기존 HTTP에서 HTTP v1로 마이그레이션  |  Firebase Cloud Messaging

의견 보내기 기존 HTTP에서 HTTP v1로 마이그레이션 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. FCM의 기존 HTTP API를 사용하는 앱은 이 가이드의 안내에 따라

firebase.google.com

 

 

{
  "message": {
    "topic": "news",
    "notification": {
      "title": "Breaking News",
      "body": "New news story available."
    },
    "data": {
      "story_id": "story_12345"
    },
    "android": {
      "notification": {
        "click_action": "TOP_STORY_ACTIVITY",
        "body": "Check out the Top Story"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "category" : "NEW_MESSAGE_CATEGORY"
        }
      }
    }
  }
}

 

위와 같이 예제를 따라 서버단에서 FCM 에 전송할 json을 구현했다고 가정하면,

flutter 단에서는 해당 데이터를 받아 패키지나 firebase 함수를 통해 메시지를 화면에 띄워줍니다.

 

 

 

Flutter단에서 필요한 작업 :

 

1. AndroidManifest.xml 

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="high_importance_channel"
    />

 

 

2. FlutterLocalNotificationPlugin 설정 

    final NotificationDetails notiDetail = const NotificationDetails(
      android: AndroidNotificationDetails(
        'high_importance_channel',
        'High Importance Notifications',
        importance: Importance.high,
        priority: Priority.high,
        playSound: true,
        enableVibration: true,
        icon: "mipmap/m_logo",
      ),
      iOS: DarwinNotificationDetails(
        presentAlert: true,
        presentBadge: true,
        presentSound: true,
        presentBanner: true,
      ),
    );

      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
          ?.createNotificationChannel(
          const AndroidNotificationChannel(
            'high_importance_channel', // id
            'High Importance Notifications', // title
            description:
            'This channel is used for important notifications.', // description
            importance: Importance.high,
            showBadge: true,
          )
      );

 

 

백엔드에서 해주어야할 작업:

 

{
  "message": {
    "topic": "news",
    "notification": {
      "title": "Breaking News",
      "body": "New news story available."
    },
    "data": {
      "story_id": "story_12345"
    },
    "android": {
    "priority": "high", // 코드 추가
      "notification": {
      	"channel_id": "high_importance_channel", // 코드 추가
        "click_action": "TOP_STORY_ACTIVITY",
        "body": "Check out the Top Story"
      }
    },
    "apns": {
      "payload": {
        "aps": {
          "category" : "NEW_MESSAGE_CATEGORY"
        }
      }
    }
  }
}

 

 

'// 코드 추가' 주석을 달아놓은 줄을 추가해주면 된다.

 

메시지를 전달할때 안드로이드에서 priority와 channel_id 를 추가하는 것이 중요!

 

InAppWebView 6.0 버전 기준으로 작성했습니다.

 

web에서 mailto: 로 만들어둔 링크를 터치하면 아무 반응이 없다면 

inAppWebView 에서 따로 처리해주어야합니다.

 

1. 안드로이드 설정

AndroidManifest.xml  에 다음과 같이 코드를 추가합니다.

이때, 꼭!!!!! application 밖에, uses-permission 과 같은 뎁스에서 작성해주셔야합니다.

저는 manifest 하단부분에 작성했습니다.

    <queries>
        <intent>
            <action android:name="android.intent.action.SENDTO" />
            <data android:scheme="mailto" />
        </intent>
    </queries>

 

 

2. iOS 설정

info.plist 에 다음 코드를 추가합니다.

    <key>LSApplicationQueriesSchemes</key>
    <array>
      <string>mailto</string>
    </array>

 

3.

inAppWebView 위젯 Widget build(BuildContext context) 

내부에서 다음과 같이 코드를 작성해주면 됩니다.

                  shouldOverrideUrlLoading: (controller, navigationAction) async {
                    var uri = navigationAction.request.url!;
                    if (uri.scheme == 'mailto') {
                      // Properly encode the mailto URL
                      final encodedSubject = Uri.encodeComponent(uri.queryParameters['subject'] ?? '');
                      final encodedBody = Uri.encodeComponent(uri.queryParameters['body'] ?? '');
                      final mailtoUri = Uri.parse('mailto:${uri.path}?subject=$encodedSubject&body=$encodedBody');

                      try {
                        if (await canLaunchUrl(mailtoUri)) {
                          await launchUrl(mailtoUri);
                        } else {
                          print("Could not launch $mailtoUri");
                        }
                      } catch (e) {
                        print("Error launching mail app: $e");
                      }
                      return NavigationActionPolicy.CANCEL;
                    }
                    return NavigationActionPolicy.ALLOW;
                  },

 

 

이렇게 설정해두면 링크 클릭시 메일 앱으로 연동됩니다.

 

iOS 에서는 잘 되는데 안드로이드에서는 인앱웹뷰를 통해 만든 웹뷰 화면에서 다운로드 링크를 눌렀을 경우,

아무 일도 일어나지 않는다.

 

그래서 찾은 가장 쉬운 해결방법!

 

1. 패키지를 다운받는다. 

그런데 InAppWebView를 사용하려면 받아두었을지도?

 

https://pub.dev/packages/url_launcher

 

url_launcher | Flutter package

Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.

pub.dev

 

 

2. AndroidManifest.xml에 다음 코드 추가

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

 

 

 

3. 다운로드 요청을 잡아서 수동으로 처리


child: InAppWebView(
                  key: webViewKey,
                  initialUrlRequest: URLRequest(url: WebUri(initUrl)),
                  onWebViewCreated: (controller) {
                    webController = controller;
                  },
                  
                  ...
                  
                  // 하기 코드 추가

                  onDownloadStartRequest: (controller, downloadRequest) async {
                    String downloadUrl = downloadRequest.url.toString();

                    if (Platform.isAndroid) {
                      if (await canLaunchUrl(WebUri(downloadUrl))) {
                        await launchUrl(WebUri(downloadUrl));
                      } else {
                        throw 'Could not launch $downloadUrl';
                      }
                    }

                  },
                  
                  ...

 

 

이렇게 처리하면 번거롭게 다른 패키지들을 설치하지 않아도 다운로드가 된다.

 

 

Flutter 에서 안드로이드 고유 번호를 확인하는 방법!

이걸 확인하기 위해서 안드로이드 native와 채널로 소통하여 받아오도록 구현했다.

 

 

1. project로 폴더를 확인할 때,

/android/app/src/main/jotlin/com/[패키지폴더]/app/MainActivity.kt

 

class MainActivity: FlutterActivity() {
    private val CHANNEL = "device_info_channel"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Android SSAID
        flutterEngine?.let {
            MethodChannel(it.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
                if (call.method == "getAndroidID") {
                    val androidID = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
                    result.success(androidID)
                } else {
                    result.notImplemented()
                }
            }
        }
    }
}

 

 

이렇게 네이티브단에서 설정을 통해 android ID 값을 받는다.

 

 

2. Flutter 

  static Future<String?> getAndroidID() async {
    const MethodChannel _channel = MethodChannel('device_info_channel');
    final String? androidId = await _channel.invokeMethod('getAndroidID');
    return androidId;
  }

 

 

플러터는 원하는 곳에서 받아오는 함수를 작성하여 호출 해서 사용한다.

 

고난과 역경의 시간이었다.

 

Flutter - iOS device app badge

 

플러터로 개발하는 아이폰 앱 배지에 알림 숫자를 컨트롤 하기 위해서는 수동으로 swift를 수정해주어야한다.

안타까운 플러터 개발자들을 위해 왜 xcode로 직접 코드를 짜야하는 배경을 설명하자면,

 

1. FlutterLocalNotificaionPlugin package

: 이 패키지에 badgeNumber가 있지만 전혀 소용이 없다.

final NotificationDetails notiDetail = const NotificationDetails(
  android: AndroidNotificationDetails(
    'high_importance_channel',
    'High Importance Notifications',
    importance: Importance.high,
    priority: Priority.high,
    playSound: true,
    enableVibration: true,
    icon: "mipmap/m_logo",
  ),
  iOS: DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
    presentBanner: true,
    badgeNumber: 3
  ),
);

 

이에 관련한 개발자의 답변:

 

https://github.com/MaikuB/flutter_local_notifications/issues/81

 

Support for notification badges on iOS and Android? · Issue #81 · MaikuB/flutter_local_notifications

Do you plan to support app icon notification badges for iOS and Android? :) Or maybe I just overlooked the functionality in the current plugin. Anyhow, would love to see the feature built in. My us...

github.com

 

 

2. FlutterAppBadger package

: discontinued. 더이상의 업데이트가 안되어있어서 최근 개발을 진행하는 프로젝트에는 소용이없다.

 

3. 서버단에서 해결

https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification

ios badge는 서버단에서 해결하면 아주... 쉽게 해결된다.

 

24/09/02 수정 - 코드는 앱이 백그라운드일때만 작동됩니다.

앱이 종료되었을땐 실행이 안됩니다. 

 

포스트를 삭제하려다가 빅데이터를 위해 남겨두기로 결정.

앱 종료시 실행이 안되고 버그처럼 나타나서 저는 서버에서 핸들링하기로 결정했습니다.

 

기존 포스트 내용은 접어두었습니다.

 

더보기

위와 같은 배경으로 나는 swift를 통해 iOS badge count 개발을 진행해야했다.

 

AppDelegate.swift 파일을 수정하면된다.

FCM message 를 전달받았을때 native 에서 받은 메시지를 바로 반영해준다.

 

import Flutter
import UIKit
import Firebase
import UserNotifications
import os.log

@main
@objc class AppDelegate: FlutterAppDelegate {

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                if granted {
                    os_log("iOS: Notification permission granted.", type: .info)
                } else if let error = error {
                    os_log("iOS: Notification permission error: %@", type: .error, error.localizedDescription)
                }
            }
        }
        
        application.registerForRemoteNotifications()
        
        FirebaseApp.configure()
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // This method will be called when the app receives a notification in background or terminated state
    override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        os_log("### iOS: Received a remote notification", type: .info)
        
        // Handle the notification data (e.g., for Firebase Analytics)
        Messaging.messaging().appDidReceiveMessage(userInfo)
        
        // Increment the badge count and update the app icon
        if let badgeCount = userInfo["badge"] as? Int {
            setBadgeCount(badgeCount, application: application)
        } else {
            // Increment the badge count locally if "badge" not provided in payload
            let currentBadgeCount = UserDefaults.standard.integer(forKey: "badgeCount")
            let newBadgeCount = currentBadgeCount + 1
            UserDefaults.standard.set(newBadgeCount, forKey: "badgeCount")
            setBadgeCount(newBadgeCount, application: application)
        }
        
        // Call the completion handler to let the system know the fetch is complete
        completionHandler(.newData)
    }
    
    // method to set the badge count
    func setBadgeCount(_ count: Int, application: UIApplication) {
        os_log("### iOS: Setting badge count to %d", type: .info, count)
        if #available(iOS 16.0, *) {
            UNUserNotificationCenter.current().setBadgeCount(count) { error in
                if let error = error {
                    os_log("Failed to set badge count: %@", type: .error, error.localizedDescription)
                } else {
                    os_log("Badge count set to %d", type: .info, count)
                }
            }
        } else {
            application.applicationIconBadgeNumber = count
            os_log("Badge count set to %d using deprecated method", type: .info, count)
        }
    }
    
    // 앱이 실행될때 쌓아둔 알림 카운트를 모두 초기화
    override func applicationDidBecomeActive(_ application: UIApplication) {
        UserDefaults.standard.set(0, forKey: "badgeCount")
        setBadgeCount(0, application: application)
    }
}

 

 

os_log 는 콘솔로 앱이 background 상태일때 로그를 찍기위해 사용했으니 필요 없다면 생략하면 된다.

 

그리고 더 중요한 점,

 

서버단에서 FCM payload를 수정해주어야한다.

 

{
  "message": {
    "topic": "테스트토픽",
    "data": {
        "title": "TEST 3",
        "body": "data - body message"
    },
    "apns": {
      "payload": {
        "aps": {
          "alert": {
            "title": "Breaking News ",
            "body": "New news story available."
          },
          "content-available": 1,
          "interruption-level": "active" 
        }
      }
    }
  }
}

 

 

나는 notification을 사용하지 않아 빼두었지만, 필요하다면 추가해서 사용하면 된다.

중요한 점은 apns 안의 구조이므로 참조해서 사용하시길...

 

 

flutter 앱에서 SafeArea 배경화면이 흰색이라 아이콘이 보이지 않을때,

background를 색칠해줄 수 있다.

 

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black, //이 부분
      body: SafeArea(
        child: Container(
          decoration: const BoxDecoration(
            image: const DecorationImage(
              image: AssetImage('assets/images/my_bg.png'),
              fit: BoxFit.cover,
            ),
          ),
          child: ...

 

ios에 Splash image 하나를 추가했을 뿐인데 갑자기 

아이폰에서 Splash Screen 에 멈춰서 아무 동작이 안되기 시작했다...

 

지난번에 이 오류를 고치지 못해서 꽤나 애먹었는데 해결책을 찾아 공유한다.

 

https://github.com/jonbhanson/flutter_native_splash/issues/577#issuecomment-1841518637

 

flutter iOS app is stuck on launchscreen when installed on real device · Issue #577 · jonbhanson/flutter_native_splash

Describe the bug I have this flutter app where I've set up a flutter_native_splash screen with the package that is named the same. I've used the command and the description from the installation gu...

github.com

 

Thanks a lot, https://github.com/gikwegbu !!

 

 

플러터 앱 실행이 잘 되다가 갑자기 iOS만 하얀 화면 혹은 스플래시 화면에 멈춰있는 경우,

 

token = await _firebaseMessaging.getToken();

 

이 녀석이 에러의 원인이었다.

ios 에서는 token이 아니라 APNS Token을 받아오는데 그걸 처리해주지 않아서 무한 로딩에 걸리는 것이다...

 

 

 

flutter No file or variants found for asset  에러

플러터에서 이미지 로딩이 안 될 경우

 

flutter:
  assets:
    - assets/images/

 

 

pubspec.yaml 파일에 assets 폴더를 인식할 수 있도록 작성.

그 때 모든 이미지 파일을 나열해서 작성하지 않고 그냥 이미지 폴더까지 등록해도 인식 가능

 

위와 같이 수정 후,

프로젝트 디렉토리에서 flutter clean 해준다.

 

만약 ios도 함께 개발중이라면 project directory 에서 flutter clean 완료된 후 flutter build ios도 실행

% flutter clean 
% flutter build ios

 

JS window.open() 명령어로 되어있는 웹을 flutter webview에서 실행시키기 위해 얼마나 오랜 기간을 삽질해왔는지 ㅠㅠㅠ

 

!!!!!!!! 드디어 해결책을 찾았다!!!! 

그래서 고생할 지도 모를 지구 어딘가의 flutter 개발자를 위해 글을 남긴다. 

 

새 창을 띄우기 위해서는 flutter webview 라이브러리가 아닌 InAppWebView를 사용해야한다.

그런데 마침 이번에 6.0으로 업데이트를 했다길래 냉큼 새 버전으로 탑승

 

https://inappwebview.dev/docs/intro

 

Getting Started | InAppWebView

Installation

inappwebview.dev:443

 

 

설치방법 생략

 

그리고 대망의 window.open() flutter app 에서 실행하기!!!!!!!!!

라이브러리 기여자들이 굉장히 예제를 성실하게 남겨주었다.

 

예제 참조:

https://github.com/pichillilorenzo/flutter_inappwebview_examples/blob/main/popup_window/lib/main.dart

 

flutter_inappwebview_examples/popup_window/lib/main.dart at main · pichillilorenzo/flutter_inappwebview_examples

A collection of flutter_inappwebview project examples - pichillilorenzo/flutter_inappwebview_examples

github.com

 

이것만있으면 뚝딱이다.

 

하지만 사람의 욕심은 끝이 없다. 아니 새 창을 띄웠으면 닫아야죠... 뒤로가기 버튼을 아무리 눌러도 안되는데 이거 머임ㅠ

안드로이드 뒤로가기 버튼을 백날 누르고 goBack() 함수를 불러도 먹히지 않았다.

그렇게 검색을 해서 찾아보면 willPopScope가 나온다.

 

However, But, Although . . .

willPopScope is deprecated. 

 

참고자료 : 

 

https://api.flutter.dev/flutter/widgets/WillPopScope-class.html

 

WillPopScope class - widgets library - Dart API

Registers a callback to veto attempts by the user to dismiss the enclosing ModalRoute. See also: Inheritance Annotations @Deprecated('Use PopScope instead. ' 'This feature was deprecated after v3.12.0-1.0.pre.') Constructors WillPopScope({Key? key, require

api.flutter.dev

 

그래서 친절하게 migration guide도 준비되어있다.

https://docs.flutter.dev/release/breaking-changes/android-predictive-back#migration-guide

 

Android predictive back

The ability to control back navigation at the time that a back gesture is received has been replaced with an ahead-of-time navigation API in order to support...

docs.flutter.dev

 

 

 

 

 

 

그래서 위의 레퍼런스를 참조해서 해결한 전체 코드:

 

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

//InAppWebViewController Singleton
class WebControllerSingleton {
  WebControllerSingleton._privateConstructor();
  static final WebControllerSingleton _instance = WebControllerSingleton._privateConstructor();
  factory WebControllerSingleton() {
    return _instance;
  }

  InAppWebViewController? _webController;
  InAppWebViewController? get webController => _webController;
  set webController(InAppWebViewController? controller) {
    _webController = controller;
  }
}

//main webView Screen
class InAppWebViewScreen extends StatefulWidget {
  const InAppWebViewScreen({Key? key}):super(key:key);

  @override
  State<InAppWebViewScreen> createState() => _InAppWebViewScreenState();
}

class _InAppWebViewScreenState extends State<InAppWebViewScreen> {
  final GlobalKey webViewKey = GlobalKey();

  final urlController = TextEditingController();
  final String loginUrl = "https://www.naver.com";
  String url = "";

  PullToRefreshController? pullToRefreshController;
  double progress = 0;

  InAppWebViewSettings settings = InAppWebViewSettings(
    isInspectable: kDebugMode,
    javaScriptEnabled: true,
    javaScriptCanOpenWindowsAutomatically: true,
    iframeAllowFullscreen: true,
    useOnDownloadStart: true,
    useOnLoadResource: true,
    clearCache: true,
    allowFileAccessFromFileURLs: true,
    allowUniversalAccessFromFileURLs: true,
  );

  @override
  void initState() {
    super.initState();

    pullToRefreshController = kIsWeb
        ? null
        : PullToRefreshController(
      settings: PullToRefreshSettings(
        color: Colors.black,
      ),
      onRefresh: () async {
        if (defaultTargetPlatform == TargetPlatform.android) {
          WebControllerSingleton().webController?.reload();
        } else if (defaultTargetPlatform == TargetPlatform.iOS) {
          WebControllerSingleton().webController?.loadUrl(
            urlRequest:
            URLRequest(
                url: await WebControllerSingleton().webController?.getUrl()),
          );
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvoked: (didPop) async {
        print("------ try go back");
        await WebControllerSingleton().webController?.goBack();
      },
      child: Scaffold(
        body: Column(
          children: <Widget>[
            Expanded(
              child: InAppWebView(
                key: webViewKey,
                initialUrlRequest: URLRequest(url: WebUri(loginUrl)),
                onWebViewCreated: (controller) {
                //init controller
                  WebControllerSingleton().webController = controller;
                },
                initialSettings: settings,
                pullToRefreshController: pullToRefreshController,
                onCreateWindow: (controller, createWindowAction) async {
                  showDialog(
                    context: context,
                    builder: (context) {
                      return WindowPopup(createWindowAction: createWindowAction);
                    },
                  );
                  return true;
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

}

//popup window class
class WindowPopup extends StatefulWidget {
  final CreateWindowAction createWindowAction;

  const WindowPopup({Key? key, required this.createWindowAction})
      : super(key: key);

  @override
  State<WindowPopup> createState() => _WindowPopupState();
}

class _WindowPopupState extends State<WindowPopup> {
  String title = '';

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      content: SizedBox(
        width: double.maxFinite,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(mainAxisSize: MainAxisSize.max, children: [
              Expanded(
                child:
                Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
              ),
              IconButton(
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  icon: const Icon(Icons.close))
            ]),
            Expanded(
              child: InAppWebView(
                gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
                  Factory<OneSequenceGestureRecognizer>(
                        () => EagerGestureRecognizer(),
                  ),
                },
                windowId: widget.createWindowAction.windowId,
                onTitleChanged: (controller, title) {
                  setState(() {
                    this.title = title ?? '';
                  });
                },
                onCloseWindow: (controller) {
                  Navigator.pop(context);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

나는 webViewController를 제어하기 위해 Singleton으로 만들어버렸다. 사실 onCreateWindow에서 새로 호출하는 AlertDialog를 Scaffold로 띄워보려고 시도했던 흔적이기도함.. 안드에서는 전체화면으로 잘 나오는데 ios에서는 정말 팝업으로 작게 나타나서 해결 시도중입니다. 해결책 아시는분 제발 글 써주세요.

 

+ Recent posts