.NET 開発基盤部会 Wiki」は、「Open棟梁Project」,「OSSコンソーシアム .NET開発基盤部会」によって運営されています。

目次

概要

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

  • 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

  • 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

参考

  • Qiita

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

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

  • PKCE実装のため。

定義

  • 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?で実装する。

参考

準備: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

参考

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スキームの重複

  • 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)フィールドを含め生成した。
  • http/http.dartによる/userinfoへのアクセスだけエラーになったので、
    • ココだけHTTP化。
      Flutterのhttp/http.dartにプラットフォーム側の証明書検証の
      無効化が適用されない問題(仕組みがネイティブと異なる)。
  • BackendエンドポイントのみHTTP化した場合、
    authz、token、userinfoエンドポイントで、以下のエラーが出るので、
    この手法は使用不可能(ライブラリが引数のチェックをしている)。
    Caused by: java.lang.IllegalArgumentException: only https connections are permitted

参考

Flutterのサード・ステップ

参考


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2021-07-01 (木) 18:41:49 (31d)