Debug panel

Dependencies
  • This panel uses EnvironmentConfig, thus depending on the env.dart file.
debug-panel

Debug panel will be visible only if showDebugPanel is set to true for EnvironmentConfig in the env.dart file. You can change the color of the tag by setting the debugPanelColor variable for your EnvironmentConfig.

NOTE:
Remember showDebugPanel for production should always be false in the Environment configuration in the env.dart file.
  'production': defaultConfig.copyWith(
    ...
    showDebugPanel: false,
  ),

Make the panel visible on a specific screen

final _navigatorKey = GlobalKey<NavigatorState>();

class MyPage extends StatelessWidget {
  const MyPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DebugWidget(
        navigatorKey: _navigatorKey,
        child: Container(),
    );
  }
}

Make the panel visible on every screen using central implementation.

NOTE:
You have to write the below code in MaterialApp, and that will show the tag panel on each screen. You don't have to wrap any other screen/ widget, or you don't have to extend any screen/widget with DebugWidget.

In the file containing the material app paste this code after imports

final _navigatorKey = GlobalKey<NavigatorState>();

In the material app paste, this code and panel will be visible on all pages.

return MaterialApp(
  builder: (BuildContext context, Widget? child) {
    return DebugWidget(
      navigatorKey: _navigatorKey,
      child: child!,
    );
  },
);

Source Code

// *****************************************
// Debug panel

// If you change any code in this file you'll probably have to restart the app
// HotReload won't work because most of the variables are constants and are
// assigned with some values when material app is build.
// *****************************************

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

import '../app_theme.dart';
import '../env.dart';
import '../helpers/constants.dart';
import '../helpers/styles.dart';
import '../services/dynamic_links.dart';

const double constHandleWidth = 180.0; // tag handle width
const double constHandleHeight = 38.0; // tag handle height

@immutable
class DebugWidget extends StatefulWidget {
  const DebugWidget({
    Key? key,
    required this.navigatorKey,
    required this.child,
  }) : super(key: key);

  final GlobalKey<NavigatorState> navigatorKey;
  final Widget child;

  @override
  DebugWidgetState createState() => DebugWidgetState();

  static DebugWidgetState of(BuildContext context) {
    return context.findAncestorStateOfType<DebugWidgetState>()!;
  }
}

class DebugWidgetState extends State<DebugWidget> with SingleTickerProviderStateMixin {
  final _drawerKey = GlobalKey();
  final _focusScopeNode = FocusScopeNode();
  final _handleHeight = constHandleHeight;
  late AnimationController _controller;

  // To determine whether to show tag or not depending on env variable
  late EnvironmentConfig _environmentConfig;
  bool showDebugPanel = false;

  @override
  void initState() {
    super.initState();
    // get env controller and set variable showDebugPanel
    _environmentConfig = EnvironmentConfig.getEnvConfig();
    showDebugPanel = _environmentConfig.showDebugPanel;
    // initialise AnimationController
    _controller = AnimationController(
      duration: duration250milli,
      vsync: this,
    );
    // addStatusListener to focus and unfocus the panel shown
    _controller.addStatusListener(
      (AnimationStatus status) {
        if (status == AnimationStatus.dismissed) {
          _focusScopeNode.unfocus();
        }
      },
    );
  }

  NavigatorState get navigator => widget.navigatorKey.currentState!;

  // will open panel
  void open() => _controller.fling(velocity: 1.0);

  // will close panel
  void close() => _controller.fling(velocity: -1.0);

  // will open/ close panel based on if panel is half open or close
  void toggle() {
    if (_controller.value > 0.5) {
      close();
    } else {
      open();
    }
  }

  @override
  Widget build(BuildContext context) {
    final double topMargin = MediaQuery.of(context).padding.top + defaultMargin;
    return showDebugPanel
        ? LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              final height = constraints.maxHeight - topMargin;
              final minFactor = (_handleHeight / height);
              return Stack(
                fit: StackFit.expand,
                children: [
                  widget.child,
                  Container(
                    margin: EdgeInsets.only(top: topMargin),
                    child: GestureDetector(
                      onVerticalDragDown: (DragDownDetails details) {
                        _controller.stop();
                      },
                      onVerticalDragUpdate: (DragUpdateDetails details) {
                        _controller.value += (-details.primaryDelta! / height);
                      },
                      onVerticalDragEnd: (DragEndDetails details) {
                        if (_controller.isDismissed) {
                          return;
                        }
                        if (details.primaryVelocity!.abs() >= 365.0) {
                          final visualVelocity = -details.primaryVelocity! / height;
                          _controller.fling(velocity: visualVelocity);
                        } else if (_controller.value < 0.5) {
                          close();
                        } else {
                          open();
                        }
                      },
                      onVerticalDragCancel: () {
                        if (_controller.isDismissed || _controller.isAnimating) {
                          return;
                        }
                        if (_controller.value < 0.5) {
                          close();
                        } else {
                          open();
                        }
                      },
                      excludeFromSemantics: true,
                      child: RepaintBoundary(
                        child: Align(
                          alignment: Alignment.bottomCenter,
                          child: AnimatedBuilder(
                            animation: _controller,
                            builder: (BuildContext context, Widget? child) {
                              return Align(
                                alignment: Alignment.topCenter,
                                heightFactor: _controller.value + minFactor,
                                child: child,
                              );
                            },
                            child: RepaintBoundary(
                              child: FocusScope(
                                key: _drawerKey,
                                node: _focusScopeNode,
                                child: _EnvPanel(
                                  handleHeight: _handleHeight,
                                  onHandlePressed: toggle,
                                  config: _environmentConfig,
                                  child: Builder(
                                    builder: (BuildContext context) {
                                      return Padding(
                                        padding: EdgeInsets.only(
                                          top: _handleHeight,
                                        ),
                                        child: Container(
                                          margin: allPadding24,
                                          child: SingleChildScrollView(
                                            physics: const BouncingScrollPhysics(),
                                            child: Column(
                                              children: [
                                                _ShowDetails(
                                                  contentHolder: PanelDataContentHolder(
                                                    content: {
                                                      'App Title':
                                                          Data(value: _environmentConfig.appTitle),
                                                      'App Title Short': Data(
                                                        value: _environmentConfig.appTitleShort,
                                                      ),
                                                      'Environment': Data(
                                                        value: _environmentConfig.envType,
                                                      ),
                                                      'Version':
                                                          Data(value: _environmentConfig.version),
                                                      'Build':
                                                          Data(value: _environmentConfig.build),
                                                      'Backend URL': Data(
                                                        value: _environmentConfig.backendUrl,
                                                      ),
                                                      'API URL':
                                                          Data(value: _environmentConfig.apiUrl),
                                                      'Request and Response Timeout': Data(
                                                        value:
                                                            '${_environmentConfig.timeoutLimit / 1000} Seconds',
                                                      ),
                                                      'Firebase Id': Data(
                                                        value: _environmentConfig.firebaseId,
                                                      ),
                                                      'API Logs Interceptor': Data(
                                                        value: _environmentConfig
                                                                .enableApiLogInterceptor
                                                            ? 'enabled'
                                                            : 'disabled',
                                                        color: _environmentConfig
                                                                .enableApiLogInterceptor
                                                            ? AppTheme.colors['success']
                                                            : AppTheme.colors['danger'],
                                                      ),
                                                      'Local Logs': Data(
                                                        value: _environmentConfig.enableLocalLogs
                                                            ? 'enabled'
                                                            : 'disabled',
                                                        color: _environmentConfig.enableLocalLogs
                                                            ? AppTheme.colors['success']
                                                            : AppTheme.colors['danger'],
                                                      ),
                                                      'Cloud Logs': Data(
                                                        value: _environmentConfig.enableCloudLogs
                                                            ? 'enabled'
                                                            : 'disabled',
                                                        color: _environmentConfig.enableCloudLogs
                                                            ? AppTheme.colors['success']
                                                            : AppTheme.colors['danger'],
                                                      ),
                                                      if (null !=
                                                          _environmentConfig.sentryConfig) ...{
                                                        'Sentry DSN': Data(
                                                          value:
                                                              _environmentConfig.sentryConfig!.dsn,
                                                        ),
                                                        'Sentry Traces Sample Rate': Data(
                                                          value: _environmentConfig
                                                              .sentryConfig!.tracesSampleRate
                                                              .toString(),
                                                        ),
                                                        'Sentry Auto App Start (Record Cold And Warm Start Time)':
                                                            Data(
                                                          value: _environmentConfig
                                                                  .sentryConfig!.autoAppStart
                                                              ? 'enabled'
                                                              : 'disabled',
                                                          color: _environmentConfig
                                                                  .sentryConfig!.autoAppStart
                                                              ? AppTheme.colors['success']
                                                              : AppTheme.colors['danger'],
                                                        ),
                                                        'Sentry User Interaction Tracing': Data(
                                                          value: _environmentConfig.sentryConfig!
                                                                  .enableUserInteractionTracing
                                                              ? 'enabled'
                                                              : 'disabled',
                                                          color: _environmentConfig.sentryConfig!
                                                                  .enableUserInteractionTracing
                                                              ? AppTheme.colors['success']
                                                              : AppTheme.colors['danger'],
                                                        ),
                                                        'Sentry Auto Performance Tracking': Data(
                                                          value: _environmentConfig.sentryConfig!
                                                                  .enableAutoPerformanceTracing
                                                              ? 'enabled'
                                                              : 'disabled',
                                                          color: _environmentConfig.sentryConfig!
                                                                  .enableAutoPerformanceTracing
                                                              ? AppTheme.colors['success']
                                                              : AppTheme.colors['danger'],
                                                        ),
                                                        'Sentry Assets Instrumentation': Data(
                                                          value: _environmentConfig.sentryConfig!
                                                                  .enableAssetsInstrumentation
                                                              ? 'enabled'
                                                              : 'disabled',
                                                          color: _environmentConfig.sentryConfig!
                                                                  .enableAssetsInstrumentation
                                                              ? AppTheme.colors['success']
                                                              : AppTheme.colors['danger'],
                                                        ),
                                                      },
                                                    },
                                                  ),
                                                ),
                                                verticalMargin24,
                                                const _StreamLinksSection(),
                                                verticalMargin24,
                                              ],
                                            ),
                                          ),
                                        ),
                                      );
                                    },
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ],
              );
            },
          )
        : widget.child;
  }
}

@immutable
class _EnvPanel extends StatelessWidget {
  const _EnvPanel({
    Key? key,
    required this.handleHeight,
    required this.onHandlePressed,
    required this.config,
    required this.child,
  }) : super(key: key);

  final double handleHeight;
  final VoidCallback onHandlePressed;
  final EnvironmentConfig config;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData(
        primaryColor: config.debugPanelColor,
        colorScheme: ColorScheme.fromSwatch(
          accentColor: config.debugPanelColor,
          brightness: Brightness.dark,
        ),
      ),
      child: Material(
        color: config.debugPanelColor,
        clipBehavior: Clip.antiAlias,
        shape: const _PanelBorder(),
        child: Stack(
          fit: StackFit.expand,
          children: [
            RepaintBoundary(
              child: Overlay(
                initialEntries: [
                  OverlayEntry(
                    maintainState: true,
                    builder: (BuildContext context) => child,
                  ),
                ],
              ),
            ),
            RepaintBoundary(
              child: Align(
                alignment: Alignment.topCenter,
                child: InkResponse(
                  onTap: onHandlePressed,
                  radius: constHandleWidth / 1.25,
                  child: RotatedBox(
                    quarterTurns: 0,
                    child: SizedBox(
                      width: constHandleWidth,
                      height: handleHeight,
                      child: FittedBox(
                        child: Padding(
                          padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10, top: 10),
                          child: Text(
                            '${config.envType} ${config.version}+${config.build}',
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _PanelBorder extends ShapeBorder {
  const _PanelBorder();

  static const double handleWidth = constHandleWidth;
  static const double handleHeight =
      constHandleHeight + 4; // if you want a small width line visible with tag remove + 4

  @override
  EdgeInsetsGeometry get dimensions => EdgeInsets.zero;

  @override
  ShapeBorder scale(double t) => const _PanelBorder();

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
    return Path()..addRect(rect);
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    const borderRadius = BorderRadius.all(Radius.circular(handleHeight / 2));

    final width = ((rect.width - handleWidth) / 2);
    final leftEnd = rect.left + width;
    final rightEnd = rect.right - width;
    return Path.combine(
      PathOperation.union,
      Path.combine(
        PathOperation.difference,
        Path()
          ..addRect(
            rect,
          ),
        Path()
          ..addRRect(
            borderRadius.toRRect(
              Rect.fromLTRB(
                rect.left - handleWidth,
                -handleHeight,
                leftEnd,
                handleHeight - 4.0,
              ),
            ),
          )
          ..addRRect(
            borderRadius.toRRect(
              Rect.fromLTRB(
                rightEnd,
                -handleHeight,
                rect.right + handleHeight,
                handleHeight - 4.0,
              ),
            ),
          )
          ..addRect(
            Rect.fromLTWH(
              leftEnd,
              0,
              handleWidth,
              handleHeight / 2,
            ),
          ),
      ),
      Path()
        ..addRRect(
          borderRadius.toRRect(
            Rect.fromLTWH(
              leftEnd,
              0,
              handleWidth,
              handleHeight,
            ),
          ),
        ),
    );
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
    //
  }
}

class _StreamLinksSection extends StatefulWidget {
  const _StreamLinksSection({Key? key}) : super(key: key);

  @override
  State<_StreamLinksSection> createState() => _StreamLinksSectionState();
}

class _StreamLinksSectionState extends State<_StreamLinksSection> {
  DeepLink? link;

  @override
  void initState() {
    super.initState();
    DynamicLinks.dynamicLinksStream.listen((DeepLink deeplink) {
      setState(() {
        link = deeplink;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return link == null
        ? emptyWidget
        : Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Dynamic Link'),
              verticalMargin8,
              _ShowDetails(contentHolder: PanelLinkContentHolder(content: link!)),
            ],
          );
  }
}

class _ShowDetails extends StatefulWidget {
  final PanelContentHolder contentHolder;

  const _ShowDetails({
    Key? key,
    required this.contentHolder,
  }) : super(key: key);

  @override
  State<_ShowDetails> createState() => _ShowDetailsState();
}

class _ShowDetailsState extends State<_ShowDetails> {
  @override
  Widget build(BuildContext context) {
    final PanelContentHolder contentHolder = widget.contentHolder;
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        if (contentHolder is PanelDataContentHolder)
          Builder(
            builder: (context) {
              final List<TableRow> rows = [];
              contentHolder.content.forEach(
                (key, data) {
                  rows.add(
                    TableRow(
                      children: [
                        Padding(
                          padding: allPadding8,
                          child: Text(key),
                        ),
                        Padding(
                          padding: allPadding8,
                          child: SelectableText(
                            data.value ?? '',
                            style: TextStyle(color: data.color ?? AppTheme.colors['warning']),
                          ),
                        ),
                      ],
                      decoration: BoxDecoration(
                        border: Border(
                          bottom: BorderSide(color: AppTheme.colors['white']!.withOpacity(0.55)),
                        ),
                      ),
                    ),
                  );
                },
              );
              return Table(
                children: rows,
              );
            },
          )
        else if (contentHolder is PanelLinkContentHolder) ...[
          SelectableText(
            contentHolder.content.encoded,
            style: TextStyles.regular3?.copyWith(color: AppTheme.colors['danger']),
            onTap: () => Clipboard.setData(ClipboardData(text: contentHolder.content.encoded)),
          ),
          SelectableText(
            contentHolder.content.decoded,
            style: TextStyles.regular3?.copyWith(color: AppTheme.colors['success']),
            onTap: () => Clipboard.setData(ClipboardData(text: contentHolder.content.decoded)),
          ),
        ]
      ],
    );
  }
}

abstract class PanelContentHolder {
  const PanelContentHolder();
}

class PanelDataContentHolder extends PanelContentHolder {
  final Map<String, Data> content;

  const PanelDataContentHolder({
    required this.content,
  });
}

class PanelLinkContentHolder extends PanelContentHolder {
  final DeepLink content;

  const PanelLinkContentHolder({
    required this.content,
  });
}

class Data {
  final String? value;
  final String? tooltip;
  final Color? color;

  const Data({
    this.value,
    this.tooltip,
    this.color,
  });
}