Skip to content

DNET_FlutterSecondStep

nishi_74322014 edited this page Sep 11, 2026 · 1 revision

Flutterのセカンド・ステップ

概要

Flutter の step by step(其の二)。

手順1:プラットフォーム固有のカスタムコード

Platform Channelsには、3つのチャネルがある。

MethodChannelでバッテリーレベルを取得・表示

  • P/Invokeの非同期版のような仕組み。
  • MethodChannelで、Android(Kotlin or Java)iOS(Swift or Objective-C)に非同期に遷移。
    • MethodChannel "アプリパッケージ名/チャンネル名"でハンドラを登録
    • ハンドラ内を if(MethodCall.method == "xxxx") で分岐させ、ネイティブAPI呼出。

main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.dev/battery');

  // Get battery level.
  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

Widget build(BuildContext context) {
  ...
    ElevatedButton(
      child: const Text('BatteryLevel Button'),
      style: ElevatedButton.styleFrom(
        primary: Colors.orange,
        onPrimary: Colors.white,
      ),
      onPressed: _getBatteryLevel,
    )
  ...
}

Android(MethodChannel)

  • BatteryManager API
  • MainActivity.ktファイル
package com.opentouryo.flutter_template

import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

class MainActivity: FlutterActivity() {
    private val CHANNEL = "samples.flutter.dev/battery"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            // Note: this method is invoked on the main thread.
            // TODO
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }

        return batteryLevel
    }
}

iOS(MethodChannel)

  • device.batteryLevel API
  • AppDelegate.swiftファイル
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      // Handle battery messages.
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      self?.receiveBatteryLevel(result: result)
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery info unavailable",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}

EventChannelで...

プラットフォームとFlutter間で双方向でイベント配信を実現するためのAPI

MessageChannelで...

プラットフォームとFlutter間で双方向でメッセージ送受信するためのAPI

参考(手順1)

手順2:AppAuthで認証処理を実装してみる。

準備:カスタムURLスキーム系

定義

  • Android
    以下の定義を、src/main/AndroidManifest.xmlに追加する。

    • Deep Links

      <!-- Deep Links -->
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="hoge" />
      </intent-filter>
    • App Links

      <!-- App Links -->
      <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" android:host="example.com" />
      </intent-filter>
    • 参考

  • iOS

    • Custom URL schemes
    • Universal Links
  • Web
    なし。

実装

ロケーション・バー直では、動かないとのこと。
以下のHTMLを作成し、ソコから飛んでみる。

  • 送信

    • Deep Links

      <!DOCTYPE html>
      <html>
        <body>
          <a href="myapp://hoge/">App Aに飛ぶ</a>
        </body>
      </html>
    • App Links

      <!DOCTYPE html>
      <html>
        <body>
          <a href="http://example.com/">App Aに飛ぶ</a>
        </body>
      </html>
  • 受信

    • Android

      • Deep Links
      • App Links
    • iOS

      • Custom URL schemes
      • Universal Links
    • Web
      onGenerateRouteで実装する。

参考(カスタムURLスキーム系)

準備:Null Safetyを導入

サンプルコードが、Null Safetyだったため。

SDKのアップデート

>flutter channel
Flutter channels:
  master
  dev
  beta
* stable
>flutter upgrade
Flutter is already up to date on channel stable
Flutter 2.2.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 02c026b03c (2 days ago) • 2021-05-27 12:24:44 -0700
Engine • revision 0fdb562ac8
Tools • Dart 2.13.1
>flutter doctor
Waiting for another flutter command to release the startup lock...
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.2.1, on Microsoft Windows [Version 10.0.19041.985], locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    X Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Android Studio (version 4.1.0)
[√] VS Code, 64-bit edition (version 1.47.2)
[√] Connected device (3 available)

! Doctor found issues in 1 category.
>flutter doctor --android-licenses
All SDK package licenses accepted.======] 100% Computing updates...
>flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.2.1, on Microsoft Windows [Version 10.0.19041.985], locale ja-JP)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[√] Chrome - develop for the web
[√] Android Studio (version 4.1.0)
[√] VS Code, 64-bit edition (version 1.47.2)
[√] Connected device (3 available)

• No issues found!

SDKバージョンを上げる。

environment:
 sdk: ">=2.12.0-0 <3.0.0"

リビルドする。

(flutter pub getの代替)

コードの修正

  • 手動

    • 型 -> 型?

    • XX.YY -> XX?.YY
      null チェックをした上でアクセスする。

    • XXX -> XXX ?? ""
      nullの場合、空文字列を返す, etc.

    • XXX -> XXX!
      nullでないと断定して non-nullable な型にキャスト。

    • requiredを足したり、

    • デフォルト値を設定したり、

    • , etc.

  • 自動

    dart migrate
    

参考(Null Safety)

flutter_appauthを使った実装

IdentityServer4でGetting Started.

  • Exampleのコードをコピペする。

  • redirect_uriが、

    'io.identityserver.demo:/oauthredirect'
    

    ...と、Private-Use URI Scheme Redirection になってるので、

    • Android

      • build.gradleファイルに、

        ...
        android {
            ...
            defaultConfig {
                ...
                manifestPlaceholders = [
                        'appAuthRedirectScheme': 'io.identityserver.demo'
                ]
            }
        }
      • AndroidManifest.xmlファイルに、
        (API 30以上(Android 11以降))

        <manifest>
          <queries>
              <intent>
                  <action android:name="android.intent.action.VIEW" />
                  <category android:name="android.intent.category.BROWSABLE" />
                  <data android:scheme="https" />
              </intent>
              <intent>
                  <action android:name="android.intent.action.VIEW" />
                  <category android:name="android.intent.category.APP_BROWSER" />
                  <data android:scheme="https" />
              </intent>
          </queries>
    • iOS
      Info.plistに、

      <key>CFBundleURLTypes</key>
      <array>
          <dict>
              <key>CFBundleTypeRole</key>
              <string>Editor</string>
              <key>CFBundleURLSchemes</key>
              <array>
                  <string>io.identityserver.demo</string>
              </array>
          </dict>
      </array>

      と指定する必要がある。

カスタムURLスキームの重複

準備:カスタムURLスキーム系を参照)

  • Android
    以下の両方の設定が有効だった。

    • AndroidManifest.xml
    • build.gradle
  • iOS
    未検証

HTTPS問題を解消する。

  • 汎用認証サイトでテストで検証する場合、

  • 以下の何れかの方法でHTTPS問題を解決する。

    • 方法1:
      リンク先の方法で、自己署名証明書の問題を解決する。
      (プラットフォーム側の証明書検証を無効化する)

    • 方法2:
      BackendエンドポイントのみHTTPS → HTTP化して問題を解決する。

汎用認証サイトでテスト

  • コードを変更する。

    • Exampleのコード
      エンドポイントのURLを変更
    • build.gradle(Android)、Info.plist(iOS)
      カスタムURLスキームを変更
  • IISのSSL設定を行い、全エンドポイントをHTTPS化した場合、

    • 以下のエラーが出るので、先ず、CNを一致させる必要がある。

      javax.net.ssl.SSLPeerUnverifiedException: Hostname 192.168.0.x not verified:
      certificate: sha1/****************************
      DN: CN=localhost
      subjectAltNames: [localhost]
      
    • 必要に応じて、DDNSを併用する
      (IPアドレスやNETBIOS名でやった場合、どうなるか?は確認していない)。

    • Chromeで、NET::ERR_CERT_COMMON_NAME_INVALIDが出るので、
      SAN(Subject Alternative Name)フィールドを含め生成(IISのSSL設定の該当節を参照)した。

    • http/http.dartによる/userinfoへのアクセスだけエラーになったので、

  • BackendエンドポイントのみHTTP化した場合、
    authz、token、userinfoエンドポイントで、以下のエラーが出るので、
    この手法は使用不可能(ライブラリが引数のチェックをしている)。

    Caused by: java.lang.IllegalArgumentException: only https connections are permitted
    

参考(flutter_appauth)

参考

Flutterの参考

  • Flutterの参考(Flutterの該当節を参照)

Flutterのサード・ステップ

移行メモ

  • 元 Wiki では見出しそのものが他ページ・同ページ内へのリンクになっていた箇所 (「手順2:AppAuthで認証処理を実装してみる。」「カスタムURLスキームの重複」 「Flutterの参考」「Flutterのサード・ステップ」)は、GitHub Wiki では 見出しからアンカが生成されるため、見出しをプレーン・テキストとし、 リンクは直下の本文に置いた。
  • 同名の見出し(「Android」「iOS」「参考」)が複数あり GitHub Wiki でアンカが衝突するため、括弧で文脈を補って一意にした。
  • PukiWiki のページ内アンカ(#xxxxxxxx)は GitHub Wiki では再現できないため、 同一ページ内のアンカは見出しから生成されるアンカに張り替え、 他ページのアンカを指すリンクは「〜(ページ名 の該当節を参照)」の形に置き換えた。
  • マイクロソフト系技術情報 Wiki(techinfoofmicrosofttech.osscons.jp)への URL リンクは、移行済みの AppAuth / IISのSSL設定 / HttpClientの類の使い方 に張り替えた。
  • 元 Wiki の行頭空白によるプレフォーマット・ブロックは、 フェンス付きコードブロックにした。
  • リンク先が未移行のページは、移行先のファイル名を `DNET_○○.md` の形で示し TODO.md に登録した。

Tags: 移行, Flutter, Platform Channels, MethodChannel, Null Safety, AppAuth, flutter_appauth, PKCE, Deep Links, App Links

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally