🐛

Flutter開発を便利にするdebug_dotパッケージを作った

に公開

Flutter 開発をしていると、デバッグ機能やテスト用のメニューにアクセスするのが面倒なことがよくあります。そこで、画面上にフローティングするボタンを表示して、簡単にデバッグメニューにアクセスできるパッケージ debug_dot を作成しました。

以下のような場面で便利です。

  • API 設定の切り替え
  • テストデータの注入
  • UI の状態切り替え
  • ログの表示・クリア
  • 設定値の一時変更

debug_dot とは

主な機能

  • フローティングボタン: 画面上をドラッグで移動できるバグアイコン
  • カスタマイズ可能なデバッグメニュー: 独自のメニュー項目とアクションを追加可能
  • 簡単な統合: アプリをDebugDotウィジェットでラップするだけ
アプリ デバッグメニュー
ホーム画面 デバッグメニュー

使い方

基本的な使用方法

アプリをDebugDotウィジェットでラップするだけです。

import 'package:debug_dot/debug_dot.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(
    const DebugDot(
      menus: [
        YourDebugMenuA(),
        YourDebugMenuB(),
      ],
      child: MyApp(),
    ),
  );
}

カスタムデバッグメニューの作成

DebugMenuクラスを継承してカスタムメニュー項目を作成できます:

class SnackBarDebugMenu extends DebugMenu {
  
  String get title => 'SnackBarを表示';

  
  IconData? get icon => Icons.message;

  const SnackBarDebugMenu();

  
  Route? onTap(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('デバッグメッセージ!')),
    );
    return null; // ナビゲーションしないアクションの場合はnullを返す
  }
}

class DebugPageMenu extends DebugMenu {
  
  String get title => 'デバッグ設定';

  
  IconData? get icon => Icons.settings;

  const DebugPageMenu();

  
  Route? onTap(BuildContext context) {
    return MaterialPageRoute(
      builder: (context) => DebugSettingsPage(),
    );
  }
}

プロダクションビルドでの除外

開発専用の機能なので、プロダクションビルドでは条件付きで除外することを推奨しています。

Widget buildApp() {
  if (kDebugMode) {
    return DebugDot(
      menus: [...],
      child: MyApp(),
    );
  }
  return MyApp();
}

Dart コンパイラはkDebugModeのようなコンパイル時定数での条件分岐に対して、アグレッシブなツリーシェイクを実行します。リリースビルド(flutter build --release)では:

  1. kDebugModefalseとなるため、if ブロック内のコードが到達不可能と判断される
  2. 到達不可能なコード(DebugDotウィジェットとその依存関係)がバンドルから完全に除去される
  3. debug_dotパッケージ自体も APK/IPA に含まれなくなる

この最適化により、プロダクションビルドのファイルサイズやパフォーマンスに影響を与えることなく、開発時の利便性を享受できます。

余談

オーバーレイ

debug_dot は Overlay を使用してフローティング UI を実現しています。メインアプリの上に独立したレイヤーとして動作するため、既存のアプリ本体に影響を与えません。

class DebugDotState extends State<DebugDot> {
  final _overlayKey = GlobalKey<OverlayState>();
  OverlayEntry? _debugDotEntry;

  void showDebugDot() {
    final entry = OverlayEntry(
      builder: (_) => DebugDotView(
        menus: widget.menus,
        initialPosition: widget.initialPosition,
        padding: widget.padding
      ),
    );
    _overlayState().insert(entry);
    _debugDotEntry = entry;
  }
}

ドラッグ機能

ドラッグ操作では、気持ちよく操作できるように無駄にこだわりました。

物理ベースアニメーション

ドラッグ終了時には、終了時点のベクトルを元に、自然な慣性と物理法則に基づいたアニメーションで画面端にスナップします。

final spring = SpringDescription(mass: 0.5, stiffness: 1500, damping: 40);
_animationController.animateWith(SpringSimulation(spring, 0, 1, initialVelocity));

スムーズクランプ

通常の境界制限では、画面端で急にドラッグが止まってしまい気持ちよくありません。対数関数を使った「スムーズクランプ」を実装しています。これにより画面端を越えてドラッグした場合も、徐々に抵抗感が増すような動きを実現しています。

double _smoothAxis(double v, double minVal, double maxVal) {
  if (v < minVal) {
    return minVal - log(-_k * (v - minVal) + 1) / _k;
  } else if (v > maxVal) {
    return maxVal + log(_k * (v - maxVal) + 1) / _k;
  } else {
    return v;
  }
}

軽量な虫アイコン

虫アイコンは、画像ファイルではなくCustomPainterを使って描画しています。
画像ファイルをバンドルに含める必要がなくアプリサイズが増加せず、リリースビルドで確実にツリーシェイクされるのが嬉しいポイントです。

class BugPainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    // 胴体
    canvas.drawOval(
      Rect.fromCenter(center: center, width: bodyWidth, height: bodyHeight),
      paint,
    );

    // 頭部
    canvas.drawCircle(headCenter, headSize / 2, paint);

    // 触角と脚をCanvasで描画...
  }
}

リンク

GitHubで編集を提案

Discussion