> For the complete documentation index, see [llms.txt](https://bleepy.gitbook.io/bleepy-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bleepy.gitbook.io/bleepy-developers/game-promotion/flutter.md).

# Flutter

<figure><img src="/files/U6sDn6w4dwsmJjHU9976" alt=""><figcaption></figcaption></figure>

***

Flutter 가이드 문서는 아래 2가지 라이브러리를 필수로 사용합니다.

* WebView 패키지 (v6) : <mark style="color:blue;">`flutter_inappwebview`</mark>
  * v5를 사용하고 계신 클라이언트는 [<mark style="color:orange;">\[Migration Guide\]</mark>](https://inappwebview.dev/docs/migration-guide/) 링크를 참고하여 마이그레이션을 권장드립니다.
* 화면 활성화 여부 판단 패키지 : <mark style="color:blue;">`visibility_detector`</mark>
  * 런처 화면이 백그라운드 모드로 전환될 때 WebView의 <mark style="color:blue;">`pause`</mark> , <mark style="color:blue;">`resume`</mark> 상태 등을 체크하기 위해  필요합니다.

```yaml
// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  flutter_inappwebview: ^6.0.0
  visibility_detector: ^0.4.0+2
```

***

## 1단계: 런처 위젯 추가

인앱 게임을 런처에 구동하기 위해서는 WebView를 사용하는 위젯을 생성해야 합니다. 다음 가이드를 따라 진행해주시기 바랍니다.

### 1. dart 파일 생성

기본적으로 <mark style="color:blue;">`lib`</mark> 폴더 경로에 런처가 구동될 <mark style="color:blue;">`dart`</mark> 파일을 생성합니다.\
가이드에서는 <mark style="color:blue;">`screens`</mark> 폴더를 생성 후 <mark style="color:blue;">`bleepy_screen.dart`</mark> 파일을 생성하였습니다.

### 2. Navigator 추가

런처 페이지 위젯으로 이동하기 위한 router를 추가합니다.

### 3. InAppWebViewSettings 옵션 추가

WebView에 사용될 옵션 값을 정의합니다.

### 4. WebView에 런처 URL 로드

위젯 페이지 내 <mark style="color:blue;">`InAppWebView`</mark> 를 추가하고 <mark style="color:blue;">`런처 호출 URL`</mark>을 요청합니다.

```dart
// lib/screens/bleepy_screen.dart
class _BleepyLauncherScreenState extends State<BleepyLauncherScreen> {
  InAppWebViewController? webViewController;

  @override
  Widget build(BuildContext context) {
    final GlobalKey webViewKey = GlobalKey();

    // WebView 옵션
    InAppWebViewSettings options = InAppWebViewSettings(
      ...
      javaScriptEnabled: true, // javascript 실행 여부
      useHybridComposition: true, // hybrid 사용을 위한 android 웹뷰 최적화
      clearCache: true,
      ...
    );

    return InAppWebView(
      key: webViewKey,
      initialUrlRequest:
      URLRequest(url: WebUri({런처 URL})),
      initialSettings: options,
      onWebViewCreated: (controller) {
        webViewController = controller;
      }
    )
  }
}
```

***

## 2단계: 위젯 백그라운드 처리 추가

WebView 내에 런처를 실행하면 자바스크립트 로직이 핑 전송 API를 주기적으로 호출합니다. 따라서 위젯이 백그라운드 모드로 이동되면 <mark style="color:blue;">`inactive`</mark> 상태로 처리되어야 정상입니다. \
그러나 런처 위젯에서 다른 위젯으로 <mark style="color:blue;">`Navigator`</mark> 를 통한 라우팅 시 플랫폼에 따라 예외적인 케이스가 존재합니다.

<mark style="color:blue;">`AOS`</mark>의 경우 라우팅 시 <mark style="color:blue;">`inactive`</mark> 상태로 변경되지 않는 이슈가 있어 직접 컨트롤이 필요합니다. \
다음 가이드를 따라 코드를 추가해 주세요.

### 1. LiftCycle 감지 설정 추가

위젯의 LifeCycle 감지를 위해 <mark style="color:blue;">`WidgetsBindingObserver`</mark> 를 사용합니다.

* <mark style="color:blue;">`initState`</mark> 에서 <mark style="color:blue;">`addObserver`</mark> 를 호출
* <mark style="color:blue;">`dispose`</mark> 에서 <mark style="color:blue;">`removeObserver`</mark> 를 호출

해당 과정은 <mark style="color:blue;">`VisibilityDetector`</mark> 라이브러리의 정상 동작을 위해 선행되어야 합니다.

```dart
// lib/screens/bleepy_screen.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 라이브러리 추가

<mark style="color:blue;">`VisibilityDetector`</mark> 라이브러리를 통해 현재 화면의 활성화 여부 판단이 필요합니다.\
화면의 활성화/비활성화 여부에 따라 WebView에 아래와 같은 처리가 필요합니다.

* 화면 비활성화 시
  * WebView <mark style="color:blue;">`pause`</mark>
* 화면 활성화 시
  * WebView <mark style="color:blue;">`resume`</mark>

<mark style="color:blue;">`VisibilityDetector`</mark> 라이브러리는 아래 옵션 표를 참고합니다.

<table><thead><tr><th width="243">Interface function</th><th width="242">Type</th><th>Description</th></tr></thead><tbody><tr><td><mark style="color:orange;"><code>key</code></mark></td><td>key</td><td>필수 입력 값</td></tr><tr><td><mark style="color:orange;"><code>onVisibilityChanged</code></mark></td><td>void Function(VisibilityInfo)</td><td><p>visibilityInfo 내 visibleFraction으로 활성화 여부 판단</p><ul><li><p><mark style="color:blue;"><code>double</code></mark> 형</p><ul><li><mark style="color:blue;"><code>1.0</code></mark> - 활성화</li><li><mark style="color:blue;"><code>0.0</code></mark> - 비활성화</li></ul></li></ul></td></tr></tbody></table>

```dart
// lib/screens/bleepy_screen.dart
@override
Widget build(BuildContext context) {
  ...

  // VisibilityDetector 추가
  return VisibilityDetector(
    key: const Key("banner-screen-visibility-detector"),
    onVisibilityChanged: (visibilityInfo) {
      // visibility change event
      var visibleFlag = visibilityInfo.visibleFraction.toInt();

      if(visibleFlag == 1){
        // 화면 보여질 때 실행
        webViewController?.resume();
      } else {
        // 화면 안 보여질 때 실행
        webViewController?.pause();
      }
    },
    child: SafeArea(...)
  );
}
```

***

## 3단계: Javascript Handler 추가

런처와 Flutter간의 통신을 수행하기 위해 <mark style="color:blue;">`Javascript Handler`</mark> 추가가 필요합니다. \
자바스크립트 핸들러 작성 시 handlerName 값은 <mark style="color:blue;">`BlpLauncher`</mark> 로 작성해야 합니다.\
다음은 런처에서 전달하는 이벤트 케이스 입니다.

* handlerName: <mark style="color:blue;">`BlpLauncher`</mark>

| Type                                                      | Description                       |
| --------------------------------------------------------- | --------------------------------- |
| <mark style="color:orange;">`closeLauncher`</mark>        | 블리피 런처의 Back 버튼 UI 클릭 시 (뒤로가기 처리) |
| <mark style="color:orange;">`launcherLoaded`</mark>       | 블리피 런처 로드가 완료된 시점에 호출             |
| <mark style="color:orange;">`timerMissionComplete`</mark> | 타이머 미션이 완료 되었을 때 호출               |
| <mark style="color:orange;">`giftReceived`</mark>         | 육성완료 시 호출                         |

```dart
// lib/screens/bleepy_screen.dart
InAppWebView(
  key: webViewKey,
  initialUrlRequest:
  URLRequest(url: WebUri(widget.launcherUrl)),
  initialSettings: 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 "launcherLoaded":
          launcherLoaded();
          break;
        case "timerMissionComplete":
          timerMissionComplete();
          break;
        case "giftReceived":
          giftReceived();
          break;

        default:
          break;
      }
    });
  }
)
```

<pre class="language-dart"><code class="lang-dart">// lib/screens/bleepy_screen.dart

<strong>// 런처 종료
</strong>void closeLauncher() {
  Navigator.of(context).pop();
}

// 런처 로드 완료
void closeLauncher() {
  // 런처의 로드가 완료된 후 추가적인 처리 필요 시 사용
}

// 타이머 미션 완료
void closeLauncher() {
  // 타이머 미션 완료 후 추가적인 처리 필요 시 사용
}

// 육성완료
void closeLauncher() {
  // 육성완료 후 추가적인 처리 필요 시 사용
}
</code></pre>

***

## 4단계:  런처 스크린 세로모드 고정 로직 추가

게임은 세로모드 해상도에 최적화 되어 스크린의 세로모드 고정이 필요합니다.

1. <mark style="color:blue;">`service.dart`</mark> 패키지를 추가합니다.&#x20;
2. <mark style="color:blue;">`SystemChrome.setPreferredOrientations`</mark> 메서드를 통해 세로 방향으로 고정합니다.

```dart
// lib/screens/bleepy_screen.dart
// 화면 고정을 위해 상단에 다음 패키지를 추가
import 'package:flutter/services.dart';

@override
Widget build(BuildContext context) {
  // ...

  // 세로 위아래 방향 고정
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
  
  return VisibilityDetector(
    ...
    child: SafeArea(...)
    );
  }
```

***

## 5단계: 화면 전환 설정

### 1. shouldOverrideUrlLoading 사용

<mark style="color:blue;">`inAppWebView`</mark> 에서 제공되는 <mark style="color:blue;">`shouldOverrideUrlLoading`</mark> 를 사용하여 딥링크 정보를 받을 수 있습니다. URI 정보를 받아 Screen 분기 처리를 진행합니다.

* <mark style="color:blue;">`URI`</mark> 정보 - <mark style="color:blue;">`navigationAction.request.url`</mark>
  * <mark style="color:orange;">`scheme`</mark> - 딥링크 스키마
  * <mark style="color:orange;">`host`</mark> - 딥링크 호스트
  * <mark style="color:orange;">`queryParameters`</mark> - 딥링크 쿼리스트링

```dart
// WebView 선언 부분 코드 추가
InAppWebView(
  ...
  shouldOverrideUrlLoading: (controller, navigationAction) async {
    // uri 정보
    var uri = navigationAction.request.url!;
    
    // Deep Link scheme 값으로 조건문 추가
    if (uri.scheme == {scheme}) {
      // Deep Link host 값으로 조건문 추가
      if (uri.host == {host}) {
        // Query string 값을 전달하기 위해 arguments 추가
        Navigator.pushNamed(context, '/{host}', arguments: uri.queryParameters);
        return NavigationActionPolicy.CANCEL;
      }
    }
    return NavigationActionPolicy.ALLOW;
  },
  ...
)
```

{% hint style="info" %}
개발에 대한 추가 설명이 더 필요하신가요?

"[<mark style="color:orange;">\[Client Admin\]</mark>](https://client-admin.bleepy.io/login) 로그인 → 오른쪽 하단 채널톡 위젯" 클릭 후 개발 카테고리에 문의 남겨주시면 기술 개발팀에서 확인 후 연락드리겠습니다.
{% endhint %}
