- WebView 라이브러리 (v5) :
flutter_inappwebview
- 화면 활성화 여부 판단 라이브러리 :
visibility_detector
yaml
// pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^5.4.4+3
visibility_detector: ^0.4.0+2
1단계 : 블리피 런처 위젯 추가
Flutter 프로젝트 내에 블리피 런처를 구동하기 위해서는 WebView를 사용하는 위젯을 생성해야 합니다.
다음 가이드를 따라 진행해주시기 바랍니다.
1. dart 파일 생성
기본적으로 lib
폴더 경로에 블리피 런처가 구동될 dart
파일을 생성합니다.
가이드에서는 screens
폴더를 생성 후 bleepy_screen.dart
파일을 생성하였습니다.
2. Navigator 추가
블리피 런처 페이지 위젯으로 이동하기 위한 라우터를 추가합니다.
3. WebView에 런처 URL 로드
위젯 페이지 내 InAppWebView
를 추가하고 {런처 URL}을 요청합니다.
dart
class _BleepyLauncherScreenState extends State<BleepyLauncherScreen> {
InAppWebViewController? webViewController;
@override
Widget build(BuildContext context) {
final GlobalKey webViewKey = GlobalKey();
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(),
android: AndroidInAppWebViewOptions(),
ios: IOSInAppWebViewOptions(),
);
return InAppWebView(
key: webViewKey,
initialUrlRequest:
URLRequest(url: Uri.parse({런처 URL})),
initialOptions: options,
onWebViewCreated: (controller) {
webViewController = controller;
}
)
}
}
2단계 : 위젯 백그라운드 처리 추가
블리피 런처를 WebView 내에 구동하면 자바스크립트 로직이 핑 전송 API를 지속적 호출합니다.
따라서 위젯이 백그라운드로 이동 되었으면 inactive
상태로 처리가 되어야 합니다.
그러나 런처 위젯에서 다른 위젯으로 Navigator
를 통한 라우팅 시 예외적인 케이스가 존재합니다.
- AOS -
flutter_inappwebview
상태로 변경되지 않습니다. - IOS -
flutter_inappwebview
상태로 변경됩니다.
AOS의 경우 라우팅 시 inactive
상태로 바뀌지 않는 이슈가 있어 직접 컨트롤이 필요합니다.
다음 가이드를 따라 진행해주시기 바랍니다.
1. LifeCycle 감지 설정 추가
위젯의 LifeCycle 감지를 위해 with WidgetsBindingObserver
를 사용하도록 생성합니다.
initState
에서 addObserver
를 호출합니다.dispose
에서 removeObserver
를 호출합니다.
해당 처리들은 VisibilityDetector
라이브러리가 정상 동작하기 위해서 필요합니다.
dart
class _BleepyLauncherScreenState extends State<BleepyLauncherScreen>
with WidgetsBindingObserver{
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
2. VisibilityDetector 라이브러리 추가
VisibilityDetector
라이브러리는 현재 화면이 활성화되었는지 여부를 판단할 수 있는 라이브러리 입니다.
화면의 비활성화/활성화 여부에 따라 아래와 같은 처리가 필요합니다.
VisibilityDetector
라이브러리는 아래 옵션 표를 참고합니다.
Interface function | Type | Description |
---|
key | key | key 값으로 필수 입력 |
onVisibilityChanged | void Function(VisibilityInfo) | 화면의 활성화 여부 변경될 때 호출되는 콜백 메소드 VisibilityInfo 내 visibleFraction 값으로 활성화 판단 double 형으로 전달되며 1.0 은 활성화, 0.0 은 비활성화 |
dart
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: const Key("banner-screen-visibility-detector"),
onVisibilityChanged: (visibilityInfo) {
var visibleFlag = visibilityInfo.visibleFraction.toInt();
if(Platform.isAndroid) {
if(visibleFlag == 1){
webViewController?.android.resume();
} else {
webViewController?.android.pause();
}
}
},
child: SafeArea(...)
);
}
3단계 : Javascript Handler 추가
블리피 런처와 Flutter 어플리케이션 간의 통신을 수행하기 위해 Javascript Handler
추가가 필요합니다.
자바스크립트 핸들러 작성 시 handlerName 값은 꼭 BlpLauncher
로 작성해야 합니다.
블리피 런처로부터 전달 받는 이벤트는 아래 2가지 케이스가 있습니다.
handlerName | Type | Description |
---|
BlpLauncher | closeLauncher | 블리피 런처의 Back 버튼 UI 클릭 시 (뒤로가기 처리) |
BlpLauncher | moveActivity | 광고 플로팅 배너 클릭 시 화면 이동 url 데이터가 넘어옴(커스텀 스키마 형태) |
dart
InAppWebView(
key: webViewKey,
initialUrlRequest:
URLRequest(url: Uri.parse(widget.launcherUrl)),
initialOptions: options,
onWebViewCreated: (controller) {
webViewController = controller;
controller.addJavaScriptHandler(handlerName: 'BlpLauncher', callback: (message) {
final data = jsonDecode(message[0]);
final key = data['type'];
switch (key) {
case "closeLauncher":
closeLauncher();
break;
case "moveActivity":
final url = data["url"];
moveCustomSchema(url!);
break;
default:
break;
}
});
}
)
1. closeLauncher()
블리피 런처의 Back 버튼 UI 클릭 시 Flutter에서는 뒤로가기 처리를 수행합니다.
dart
void closeLauncher() {
Navigator.of(context).pop();
}
전체 소스 코드
dart
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'package:bleepy_flutter_example/screens/banner_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:visibility_detector/visibility_detector.dart';
class CustomScheme {
final String url;
CustomScheme({required this.url});
}
class BleepyLauncherScreen extends StatefulWidget {
final String launcherUrl;
const BleepyLauncherScreen({super.key, required this.launcherUrl});
@override
State<BleepyLauncherScreen> createState() => _BleepyLauncherScreenState();
}
class _BleepyLauncherScreenState extends State<BleepyLauncherScreen> with{
InAppWebViewController? webViewController;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
final GlobalKey webViewKey = GlobalKey();
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(),
android: AndroidInAppWebViewOptions(),
ios: IOSInAppWebViewOptions(),
);
return VisibilityDetector(
key: const Key("banner-screen-visibility-detector"),
onVisibilityChanged: (visibilityInfo) {
var visibleFlag = visibilityInfo.visibleFraction.toInt();
if(Platform.isAndroid) {
if(visibleFlag == 1){
webViewController?.android.resume();
} else {
webViewController?.android.pause();
}
}
},
child: SafeArea(
bottom: false,
child: InAppWebView(
key: webViewKey,
initialUrlRequest:
URLRequest(url: Uri.parse(widget.launcherUrl)),
initialOptions: options,
onWebViewCreated: (controller) {
webViewController = controller;
controller.addJavaScriptHandler(handlerName: 'BlpLauncher', callback: (message) {
final data = jsonDecode(message[0]);
final key = data['type'];
switch (key) {
case "closeLauncher":
closeLauncher();
break;
case "moveActivity":
final url = data["url"];
moveCustomSchema(url!);
break;
default:
break;
}
});
},
onLoadStart: (controller, url) {},
onLoadStop: (controller, url) async {},
onLoadError: (controller, url, code, message) {},
onProgressChanged: (controller, progress) {},
onUpdateVisitedHistory: (controller, url, androidIsReload) {},
onConsoleMessage: (controller, consoleMessage) {
debugPrint(consoleMessage as String?);
},
)
)
);
}
void closeLauncher() {
Navigator.of(context).pop();
}
String ensureTrailingSlash(String url) {
if (!url.endsWith('/')) {
late String targetUrl;
targetUrl = "$url/";
return targetUrl;
}
return url;
}
void moveCustomSchema(String url) {
final RegExp pattern =
RegExp(r'^bleepy://([a-z0-9-_.]*)', caseSensitive: false);
final RegExpMatch? match = pattern.firstMatch(url);
if (match == null) return;
final String? type = match.group(1);
final Uri uriData = Uri.parse(url);
final Map<String, String> params = uriData.queryParameters;
if (params['url'] == null) return;
final CustomScheme data = CustomScheme(url: params['url']!);
String dataUrl = data.url.trim();
String targetUrl = ensureTrailingSlash(dataUrl);
switch (type) {
case "banner":
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => BannerScreen(url: targetUrl),
transitionDuration: const Duration(seconds: 0),
)
);
break;
}
}
}