Widgets
Debug panel
Dependencies
- This panel uses EnvironmentConfig, thus depending on the env.dart file.
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,
});
}