0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter で作成した iOS と Android アプリに CloudWatch RUM を導入する ②

0
Last updated at Posted at 2026-02-24

Flutter で作成した Android アプリに Amazon CloudWatch RUM を導入する

Flutter で作成した Android アプリに Amazon CloudWatch RUM(Real User Monitoring)を導入し、HTTP リクエスト、クラッシュ、画面遷移、セッションなどのテレメトリデータを収集する方法を解説します。

はじめに

CloudWatch RUM は、AWS Distro for OpenTelemetry(ADOT)Android SDK を利用することで、Android アプリのリアルユーザーモニタリングが可能です。

Flutter アプリでは、全ての画面が単一の MainActivity 上で動作するため、ネイティブ SDK の自動計装だけでは画面遷移や HTTP リクエストの計測が不十分です。本記事では以下の 2 つのブリッジを構築し、RUM コンソールの全タブにデータを反映させます。

  1. HTTP リクエスト: Dio の通信を MethodChannel 経由でネイティブ OkHttp に中継し、ByteBuddy 自動計装で RUM に記録
  2. 画面遷移: GoRouter の遷移を MethodChannel 経由でネイティブの VisibleScreenTracker に反映

本記事で実現すること

  • CloudWatch RUM App Monitor の作成と設定
  • ADOT Android SDK(Zero-Code Instrumentation)の導入
  • Flutter の HTTP リクエストを RUM の HTTP requests / Network errors に反映
  • Flutter の画面遷移を RUM の screen.name に反映
  • クラッシュレポートの収集
  • Trace Map でのバックエンド接続の可視化

前提条件

  • Flutter 3.x 以上
  • Android minSdk 26 以上(ADOT SDK の要件)
  • AWS アカウントと CloudWatch RUM へのアクセス権限

iOS 版について

iOS 版の導入手順は別記事「Flutter で作成した iOS と Android アプリに CloudWatch RUM を導入する ①」を参照してください。

アーキテクチャ

android-architecture.png

1. CloudWatch RUM App Monitor の作成

AWS マネジメントコンソールで App Monitor を作成します。

  1. CloudWatch コンソール → Application Signals → RUM → 「Add app monitor」
  2. 以下を設定:
    • App monitor name: your-app-android
    • Application type: Android
    • Active tracing: 有効(X-Ray 連携)

Screenshot 2026-02-24 at 11.47.50.png

作成後、App Monitor ID をメモしておきます。

2. リソースベースポリシーの設定

モバイルアプリからの未認証リクエストを許可するため、リソースベースポリシーを設定します。App Monitor 作成時に「Create public policy」を選択した場合はこの手順は不要です。

aws rum put-resource-policy \
  --name your-app-android \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "rum:PutRumEvents",
        "Resource": "arn:aws:rum:<REGION>:<ACCOUNT_ID>:appmonitor/your-app-android",
        "Principal": "*"
      }
    ]
  }' \
  --region <REGION>

注意: 本番環境では IP アドレス制限などの条件を追加することを推奨します。

3. Android ネイティブ側の設定

3.1 settings.gradle.kts — ByteBuddy プラグインの追加

android/settings.gradle.ktsplugins ブロックに ByteBuddy プラグインを追加します:

plugins {
    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
    id("com.android.application") version "8.11.1" apply false
    id("org.jetbrains.kotlin.android") version "2.2.20" apply false
    // ↓ 追加
    id("net.bytebuddy.byte-buddy-gradle-plugin") version "1.15.10" apply false
}

3.2 app/build.gradle.kts — 依存関係の追加

android/app/build.gradle.kts に以下の変更を加えます:

plugins ブロックに ByteBuddy を追加:

plugins {
    id("com.android.application")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
    // ↓ 追加
    id("net.bytebuddy.byte-buddy-gradle-plugin")
}

minSdk を 26 に変更(ADOT SDK の要件):

defaultConfig {
    applicationId = "com.example.your_app"
    minSdk = 26  // ← flutter.minSdkVersion から変更
    targetSdk = flutter.targetSdkVersion
    versionCode = flutter.versionCode
    versionName = flutter.versionName
}

dependencies ブロックに ADOT SDK、OkHttp、ByteBuddy 計装を追加:

dependencies {
    // ADOT Android SDK(Zero-Code Instrumentation)
    implementation("software.amazon.opentelemetry.android:agent:1.0.0")

    // OkHttp(Flutter の HTTP リクエストを中継するため)
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    // ByteBuddy による OkHttp/HttpURLConnection 自動計装
    byteBuddy("io.opentelemetry.android.instrumentation:okhttp3-agent:0.15.0-alpha")
    byteBuddy("io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.15.0-alpha")
}

3.3 aws_config.json — ADOT 設定ファイル

android/app/src/main/res/raw/aws_config.json を作成します:

{
  "aws": {
    "region": "<REGION>",
    "rumAppMonitorId": "<YOUR_APP_MONITOR_ID>"
  },
  "otelResourceAttributes": {
    "service.name": "your-app-android",
    "service.version": "1.0.0"
  }
}

ADOT Agent はアプリ起動時にこのファイルを自動的に読み込み、テレメトリ収集を開始します。コードの変更は不要です(Zero-Code Instrumentation)。

3.4 AndroidManifest.xml — Application クラスの登録

android/app/src/main/AndroidManifest.xml<application> タグに android:name を追加します:

<application
    android:label="your_app"
    android:name=".YourApplication"
    android:icon="@mipmap/ic_launcher">

3.5 Application クラス — 画面名の上書き

Flutter アプリでは全ての画面が MainActivity 上で動作するため、ADOT の自動計装では screen.name が常に MainActivity になります。これを Flutter の画面名で上書きするため、VisibleScreenTracker の内部状態をリフレクションで更新します。

android/app/src/main/kotlin/<package>/YourApplication.kt:

package com.example.your_app

import io.flutter.app.FlutterApplication
import java.util.concurrent.atomic.AtomicReference

class YourApplication : FlutterApplication() {
    companion object {
        @Volatile
        private var lastResumedActivityRef: AtomicReference<String>? = null

        fun updateScreenName(name: String) {
            lastResumedActivityRef?.set(name)
        }
    }

    override fun onCreate() {
        super.onCreate()
        // ADOT Agent は ContentProvider 経由で onCreate() より前に初期化済み。
        // リフレクションで VisibleScreenTracker にアクセスし、
        // screen.name を Flutter の画面名で上書き可能にする。
        try {
            val servicesClass = Class.forName(
                "io.opentelemetry.android.internal.services.Services"
            )
            val getMethod = servicesClass.getMethod(
                "get", android.app.Application::class.java
            )
            val services = getMethod.invoke(null, this)
            val trackerMethod = servicesClass.getMethod("getVisibleScreenTracker")
            val tracker = trackerMethod.invoke(services)
            val field = tracker.javaClass.getDeclaredField("lastResumedActivity")
            field.isAccessible = true
            @Suppress("UNCHECKED_CAST")
            lastResumedActivityRef = field.get(tracker) as? AtomicReference<String>
        } catch (_: Exception) {}
    }
}

なぜリフレクションが必要か: VisibleScreenTrackerinternal パッケージに属しており、直接アクセスできません。また、ADOT の OpenTelemetryRumClientConfig には SpanProcessor を追加する API がないため、リフレクションで lastResumedActivityAtomicReference を取得し、Flutter の画面名で上書きします。

3.6 MainActivity — Platform Channel ハンドラ

android/app/src/main/kotlin/<package>/MainActivity.kt に、HTTP リクエスト中継と画面遷移通知の Platform Channel を実装します:

package com.example.your_app

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.opentelemetry.api.common.Attributes
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import software.amazon.opentelemetry.android.OpenTelemetryRumClient
import software.amazon.opentelemetry.android.features.span
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class MainActivity : FlutterActivity() {
    private val httpClient = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
    private val executor = Executors.newCachedThreadPool()

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // HTTP 中継チャネル: Dart → OkHttp → ByteBuddy 自動計装
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.yourapp/http"
        ).setMethodCallHandler { call, result ->
            if (call.method == "request") {
                val method = call.argument<String>("method") ?: "GET"
                val url = call.argument<String>("url") ?: ""
                val headers = call.argument<Map<String, String>>("headers")
                    ?: emptyMap()
                val body = call.argument<String>("body")

                executor.execute {
                    try {
                        val rb = Request.Builder().url(url)
                        headers.forEach { (k, v) -> rb.addHeader(k, v) }
                        val reqBody = body?.toRequestBody(
                            "application/json".toMediaTypeOrNull()
                        )
                        when (method.uppercase()) {
                            "GET" -> rb.get()
                            "POST" -> rb.post(
                                reqBody ?: "".toRequestBody(null)
                            )
                            "PUT" -> rb.put(
                                reqBody ?: "".toRequestBody(null)
                            )
                            "PATCH" -> rb.patch(
                                reqBody ?: "".toRequestBody(null)
                            )
                            "DELETE" -> if (reqBody != null)
                                rb.delete(reqBody) else rb.delete()
                        }
                        val resp = httpClient.newCall(rb.build()).execute()
                        val respBody = resp.body?.string() ?: ""
                        val respHeaders = mutableMapOf<String, String>()
                        resp.headers.forEach { (k, v) -> respHeaders[k] = v }
                        runOnUiThread {
                            result.success(mapOf(
                                "statusCode" to resp.code,
                                "body" to respBody,
                                "headers" to respHeaders
                            ))
                        }
                    } catch (e: Exception) {
                        runOnUiThread {
                            result.error("HTTP_ERROR", e.message, null)
                        }
                    }
                }
            } else {
                result.notImplemented()
            }
        }

        // OTel チャネル: 画面遷移通知
        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.yourapp/otel"
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "viewChanged" -> {
                    val name = call.argument<String>("name") ?: "unknown"
                    YourApplication.updateScreenName(name)
                    OpenTelemetryRumClient.span(
                        name = "screen.view",
                        attributes = Attributes.builder()
                            .put("screen.name", name)
                            .build()
                    ) {}
                    result.success(null)
                }
                else -> result.notImplemented()
            }
        }
    }
}

なぜ OkHttp で中継するのか

Flutter の Dio は Dart VM 内の HTTP スタックを使用するため、ADOT の ByteBuddy 自動計装(OkHttp / HttpURLConnection 対象)では捕捉できません。

カスタムスパンで HTTP 属性を手動設定する方法も試みましたが、CloudWatch RUM コンソールの HTTP requests セクションや Trace Map には反映されませんでした。RUM が認識するのは ByteBuddy 自動計装が生成する正規のスパンのみです。

そのため、Dart 側の HTTP リクエストを Platform Channel 経由でネイティブの OkHttp に中継し、ByteBuddy の自動計装に拾わせるアプローチを採用しています。

http-request-flow.png

4. Dart 側の実装

4.1 NativeHttpClientAdapter — OkHttp 中継アダプター

Dio の HttpClientAdapter を差し替えて、Android では全ての HTTP リクエストをネイティブ OkHttp 経由で実行します。

lib/core/network/native_http_client_adapter.dart:

import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';

class NativeHttpClientAdapter implements HttpClientAdapter {
  static const _channel = MethodChannel('com.yourapp/http');

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future<void>? cancelFuture,
  ) async {
    String? body;
    if (requestStream != null) {
      final chunks = <int>[];
      await for (final chunk in requestStream) {
        chunks.addAll(chunk);
      }
      if (chunks.isNotEmpty) {
        body = utf8.decode(chunks);
      }
    }

    final headers = <String, String>{};
    options.headers.forEach((k, v) {
      if (v != null) headers[k] = v.toString();
    });

    try {
      final response = await _channel.invokeMethod<Map>('request', {
        'method': options.method,
        'url': options.uri.toString(),
        'headers': headers,
        if (body != null) 'body': body,
      });

      final statusCode = response!['statusCode'] as int;
      final responseBody = response['body'] as String? ?? '';
      final responseHeaders = <String, List<String>>{};
      if (response['headers'] is Map) {
        (response['headers'] as Map).forEach((k, v) {
          responseHeaders[k.toString()] = [v.toString()];
        });
      }

      return ResponseBody.fromString(
        responseBody,
        statusCode,
        headers: responseHeaders,
      );
    } on PlatformException catch (e) {
      throw DioException(
        requestOptions: options,
        type: DioExceptionType.connectionError,
        message: e.message,
      );
    }
  }

  @override
  void close({bool force = false}) {}
}

4.2 DioClient — Android 時にアダプターを差し替え

lib/core/network/dio_client.dart で、Android の場合のみ NativeHttpClientAdapter を使用します:

import 'dart:io';
import 'package:dio/dio.dart';
import 'native_http_client_adapter.dart';

class DioClient {
  late final Dio _dio;

  DioClient() {
    _dio = Dio(baseOptions);
    // Android では OkHttp 経由で ADOT 自動計装を有効化
    if (Platform.isAndroid) {
      _dio.httpClientAdapter = NativeHttpClientAdapter();
    }
    _setupInterceptors();
  }

  Dio get instance => _dio;

  void _setupInterceptors() {
    // OtelHttpInterceptor は不要(OkHttp が自動でトレースヘッダーを付与)
    _dio.interceptors.add(LoggingInterceptor());
    _dio.interceptors.add(ConnectivityInterceptor());
    _dio.interceptors.add(AuthInterceptor());
    _dio.interceptors.add(ErrorInterceptor());
  }
}

重要: Android では OtelHttpInterceptor(Dart 側でトレースヘッダーを付与するインターセプター)は不要です。ByteBuddy が OkHttp のリクエストに自動で traceparent ヘッダーを付与します。iOS では引き続き OtelHttpInterceptor を使用してください。

4.3 OtelNavigatorObserver — 画面遷移の通知

lib/core/observability/otel_navigator_observer.dart:

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

class OtelNavigatorObserver extends NavigatorObserver {
  static const _channel = MethodChannel('com.yourapp/otel');

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    _reportRoute(route);
  }

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    if (newRoute != null) _reportRoute(newRoute);
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (previousRoute != null) _reportRoute(previousRoute);
  }

  void _reportRoute(Route<dynamic> route) {
    final name = route.settings.name ?? 'unknown';
    _channel.invokeMethod('viewChanged', {'name': name}).catchError((_) {});
  }
}

GoRouter に Observer を追加:

GoRouter(
  initialLocation: '/',
  observers: [OtelNavigatorObserver()],
  routes: [ /* ... */ ],
);

5. ビルドと動作確認

5.1 ビルド

flutter clean
flutter pub get
flutter build apk --debug

注意: JDK 25 以降を使用している場合、Kotlin コンパイラが Java バージョンを解析できずビルドに失敗します。Android Studio 付属の JDK 21 を使用してください:

export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
flutter build apk --debug

5.2 エミュレータでの動作確認

# エミュレータを起動(DNS を指定しないと RUM エンドポイントに接続できない場合がある)
emulator -avd <AVD_NAME> -dns-server 8.8.8.8

# APK をインストール
adb install -r build/app/outputs/flutter-apk/app-debug.apk

# アプリを起動
adb shell am start -n <PACKAGE>/<PACKAGE>.MainActivity

5.3 ADOT Agent の初期化確認

logcat で ADOT Agent の初期化ログを確認します:

adb logcat | grep -i "OpenTelemetryRum"

以下のようなログが表示されれば、Agent は正常に初期化されています:

D OpenTelemetryRum: onAvailable: currentNetwork=CurrentNetwork(state=TRANSPORT_WIFI, ...)
D OpenTelemetryRum: Requested cache size: %s, folder size: 10000000

5.4 HTTP スパンの確認

OkHttp の自動計装が動作しているか確認します:

adb logcat | grep -i "HttpExporter"

HttpExporter のエラーが表示されなければ、スパンは正常に送信されています。エラーが表示される場合は、エミュレータのネットワーク接続を確認してください。

5.5 CloudWatch Logs での確認

RUM に送信されたスパンは CloudWatch Logs でも確認できます:

aws logs filter-log-events \
  --log-group-name /aws/vendedlogs/RUMService_<APP_MONITOR_NAME><APP_MONITOR_ID_PREFIX> \
  --filter-pattern "GET" \
  --region <REGION> \
  --log-stream-names /aws/rum/otel-spans

ByteBuddy 自動計装が生成したスパンは scope.nameio.opentelemetry.okhttp-3.0 になります:

{
  "scope": { "name": "io.opentelemetry.okhttp-3.0" },
  "name": "GET",
  "kind": "CLIENT",
  "attributes": {
    "http.request.method": "GET",
    "http.response.status_code": 200,
    "url.full": "https://example.com/api/v1/data",
    "server.address": "example.com",
    "server.port": 443,
    "network.peer.address": "1.2.3.4"
  },
  "status": { "code": "UNSET" }
}

6. CloudWatch RUM コンソールでの確認

6.1 Performance タブ

アプリ起動時間(Cold Start)、画面描画時間(Time to First Draw)が表示されます。

Screenshot 2026-02-24 at 11.51.54.png

Screenshot 2026-02-24 at 11.52.08.png

Screenshot 2026-02-24 at 11.52.20.png

6.2 Errors タブ — HTTP requests

HTTP リクエストのレイテンシとエラー率が表示されます。4xx/5xx レスポンスは自動的に HTTP error として記録されます。

Screenshot 2026-02-24 at 11.53.06.png

6.3 Errors タブ — Crashes

アプリクラッシュ(未処理例外)が記録されます。クラッシュデータはディスクバッファに保存され、次回起動時に送信されます。

Screenshot 2026-02-24 at 11.53.31.png

Screenshot 2026-02-24 at 11.53.42.png

6.4 Sessions タブ

ユーザーセッションの一覧が表示されます。各セッションの画面遷移、HTTP リクエスト、エラーを時系列で確認できます。

Screenshot 2026-02-24 at 11.54.06.png

6.5 Trace Map

pictures-androidpictures-api-fargate-otel (ECS Fargate) のように、アプリからバックエンドへの接続が可視化されます。

Screenshot 2026-02-24 at 11.54.30.png

7. Flutter 固有の課題と対策

7.1 画面名が MainActivity になる問題

Flutter アプリでは Android の Activity が MainActivity 1 つだけのため、ADOT の自動計装では全てのスパンの screen.nameMainActivity になります。

対策: OtelNavigatorObserver で Dart 側の画面遷移を検知し、PicturesApplication.updateScreenName()VisibleScreenTrackerlastResumedActivity を Flutter の画面名(home, pictureDetail 等)に上書きします。

ただし、アプリ起動時の AppStartCreatedtime_to_first_draw スパンは Flutter エンジン初期化前に生成されるため、MainActivity のままです。

Screenshot 2026-02-24 at 11.55.51.png

7.2 HTTP リクエストが RUM に反映されない問題

Flutter の Dio は Dart VM 内の HTTP スタックを使用するため、ByteBuddy の OkHttp/HttpURLConnection 自動計装では捕捉できません。

カスタムスパンで http.request.methodserver.address 等の属性を手動設定する方法も試みましたが、RUM コンソールの HTTP requests セクションや Trace Map には反映されませんでした。

対策: NativeHttpClientAdapter で Dio の HTTP リクエストを Platform Channel 経由でネイティブの OkHttp に中継します。OkHttp のリクエストは ByteBuddy が自動計装するため、RUM が認識する正規のスパンが生成されます。

7.3 エミュレータの DNS 解決失敗

Android エミュレータでは、dataplane.rum.<region>.amazonaws.com の DNS 解決に失敗することがあります。

対策: エミュレータ起動時に DNS サーバーを明示的に指定します:

emulator -avd <AVD_NAME> -dns-server 8.8.8.8

また、Private DNS が有効な場合は無効にします:

adb shell settings put global private_dns_mode off

8. 自動計測される項目

ADOT Android SDK が自動的に計測する項目です。Flutter 側の実装は不要です。

項目 説明
アプリ起動 Cold/Warm Start の検出と計測
Activity ライフサイクル Created, Started, Resumed 等のイベント
Time to First Draw 画面の初回描画までの時間
クラッシュ 未処理例外の検出(次回起動時に送信)
ANR Application Not Responding の検出
Slow Rendering フレームドロップの検出
HTTP リクエスト OkHttp/HttpURLConnection の自動計装(ByteBuddy)
セッション ユーザーセッションの開始/終了
デバイス情報 モデル、OS バージョン、地域情報

9. iOS 版との違い

項目 Android iOS
SDK ADOT Android SDK (agent:1.0.0) ADOT Swift SDK (aws-otel-swift)
パッケージ管理 Gradle (Maven Central) Swift Package Manager
初期化 ContentProvider で自動(Zero-Code) aws_config.json + SPM で自動
HTTP 計装 ByteBuddy で OkHttp を自動計装 カスタムスパン(手動)
HTTP の RUM 反映 ✅ HTTP requests / Trace Map に反映 ⚠️ カスタムスパンのため一部制約あり
画面名の上書き リフレクションで VisibleScreenTracker を更新 AwsScreenManagerProvider の API を使用
クラッシュ検出 UncaughtExceptionHandler PLCrashReporter

最大の違いは HTTP リクエストの計装方式です。Android では ByteBuddy による OkHttp 自動計装が利用できるため、NativeHttpClientAdapter で OkHttp に中継することで RUM の HTTP requests セクションと Trace Map に正しく反映されます。iOS ではこの方式が使えないため、カスタムスパンでの対応となります。そのため、iOSでは Trace Map を ECS まで繋げることができませんでした。

まとめ

Flutter Android アプリに CloudWatch RUM を導入するには、以下の 3 つのブリッジが必要です:

  1. HTTP リクエスト: NativeHttpClientAdapter → MethodChannel → OkHttp → ByteBuddy 自動計装
  2. 画面遷移: OtelNavigatorObserver → MethodChannel → VisibleScreenTracker 更新
  3. クラッシュ/ANR/起動時間: ADOT Agent が自動計測(追加実装不要)

これにより、CloudWatch RUM の Performance、Errors(HTTP requests / Crashes)、Sessions、Trace Map の全てのセクションで Flutter アプリのテレメトリデータを確認できるようになります。


変更ファイル一覧

ファイル 説明
android/settings.gradle.kts ByteBuddy プラグイン追加
android/app/build.gradle.kts ADOT SDK、OkHttp、ByteBuddy 依存関係追加、minSdk 26
android/app/src/main/res/raw/aws_config.json ADOT 設定ファイル(リージョン、App Monitor ID)
android/app/src/main/AndroidManifest.xml Application クラスの登録
android/app/src/main/kotlin/.../YourApplication.kt VisibleScreenTracker のリフレクション更新
android/app/src/main/kotlin/.../MainActivity.kt OkHttp HTTP 中継、画面遷移通知の Platform Channel
lib/core/network/native_http_client_adapter.dart OkHttp 中継用 Dio アダプター
lib/core/network/dio_client.dart Android 時に NativeHttpClientAdapter を使用
lib/core/observability/otel_navigator_observer.dart 画面遷移を通知する NavigatorObserver
lib/routing/app_router.dart GoRouter に Observer を追加

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?