flutter_ui_guard 0.0.3
flutter_ui_guard: ^0.0.3 copied to clipboard
A Flutter static analysis tool that detects UI performance issues, deep widget nesting, and missing best practices in widget trees.
import 'package:flutter/material.dart';
import 'package:flutter_ui_guard/flutter_ui_guard.dart';
void main() {
runApp(const FlutterGuardDemoApp());
}
class FlutterGuardDemoApp extends StatelessWidget {
const FlutterGuardDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Guard Demo',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GuardReport? _currentReport;
String _selectedExample = 'Basic Example';
final Map<String, Widget> _examples = {
'Basic Example': const BasicExampleWidget(),
'Deep Nesting': const DeepNestingWidget(),
'Heavy Build': const HeavyBuildWidget(),
'Container Issues': const ContainerIssuesWidget(),
'Good Practices': const GoodPracticesWidget(),
};
void _analyzeWidget(String exampleName) {
final widget = _examples[exampleName]!;
final report = FlutterGuard.analyze(
rootWidget: widget,
config: const GuardConfig(verbose: false),
);
setState(() {
_currentReport = report;
_selectedExample = exampleName;
});
}
@override
void initState() {
super.initState();
_analyzeWidget('Basic Example');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: const Text('đĄī¸ Flutter Guard Demo'),
elevation: 2,
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWideScreen = constraints.maxWidth > 800;
if (isWideScreen) {
return _buildWideLayout();
} else {
return _buildNarrowLayout();
}
},
),
);
}
Widget _buildWideLayout() {
return Row(
children: [
// Left sidebar - Example selector
Container(
width: 250,
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(right: BorderSide(color: Colors.grey[300]!)),
),
child: _buildSidebar(),
),
// Main content area
Expanded(child: _buildMainContent()),
],
);
}
Widget _buildNarrowLayout() {
return Column(
children: [
// Example selector dropdown
Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[100],
child: DropdownButton<String>(
value: _selectedExample,
isExpanded: true,
items: _examples.keys.map((name) {
return DropdownMenuItem(value: name, child: Text(name));
}).toList(),
onChanged: (value) {
if (value != null) _analyzeWidget(value);
},
),
),
// Main content
Expanded(child: _buildMainContent()),
],
);
}
Widget _buildSidebar() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.blue[50],
child: const Text(
'Select Example',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Expanded(
child: ListView(
children: _examples.keys.map((name) {
final isSelected = name == _selectedExample;
return ListTile(
selected: isSelected,
selectedTileColor: Colors.blue[100],
title: Text(name),
trailing: isSelected
? const Icon(Icons.check, color: Colors.blue)
: null,
onTap: () => _analyzeWidget(name),
);
}).toList(),
),
),
],
);
}
Widget _buildMainContent() {
return Column(
children: [
// Widget preview
Expanded(
flex: 2,
child: Container(
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.green[50],
child: Row(
children: [
const Icon(Icons.preview, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text(
'Preview: $_selectedExample',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _examples[_selectedExample],
),
),
),
],
),
),
),
// Analysis report
Expanded(
flex: 3,
child: Container(
color: Colors.grey[50],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.orange[50],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.analytics, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Text(
'Analysis Report',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
if (_currentReport != null) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildStatChip(
'Widgets',
_currentReport!.totalWidgets.toString(),
Colors.blue,
),
_buildStatChip(
'Issues',
_currentReport!.issues.length.toString(),
_currentReport!.issues.isEmpty
? Colors.green
: Colors.red,
),
_buildStatChip(
'Depth',
_currentReport!.maxDepth.toString(),
Colors.purple,
),
],
),
],
],
),
),
Expanded(
child: _currentReport == null
? const Center(child: CircularProgressIndicator())
: _buildReportView(_currentReport!),
),
],
),
),
),
],
);
}
Widget _buildStatChip(String label, String value, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: color.withValues(alpha: .7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
color: color.withValues(alpha: .9),
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildReportView(GuardReport report) {
if (report.issues.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, size: 64, color: Colors.green[400]),
const SizedBox(height: 16),
const Text(
'â
No Issues Found!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Your widget tree looks great!',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: report.issues.length,
itemBuilder: (context, index) {
final issue = report.issues[index];
return _buildIssueCard(issue);
},
);
}
Widget _buildIssueCard(GuardIssue issue) {
final severityData = _getSeverityData(issue.severity);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: severityData['color'].withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
severityData['icon'],
style: const TextStyle(fontSize: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
issue.widgetType,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
issue.message,
style: TextStyle(color: Colors.grey[700], fontSize: 14),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: severityData['color'].withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: severityData['color'].withValues(alpha: 0.3),
),
),
child: Text(
severityData['label'],
style: TextStyle(
color: severityData['color'],
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
if (issue.suggestion != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue[200]!),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.lightbulb, color: Colors.orange, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
issue.suggestion!,
style: TextStyle(color: Colors.blue[900], fontSize: 13),
),
),
],
),
),
],
],
),
),
);
}
Map<String, dynamic> _getSeverityData(IssueSeverity severity) {
switch (severity) {
case IssueSeverity.info:
return {'icon': 'âšī¸', 'label': 'INFO', 'color': Colors.blue};
case IssueSeverity.warning:
return {'icon': 'â ī¸', 'label': 'WARNING', 'color': Colors.orange};
case IssueSeverity.error:
return {'icon': 'â', 'label': 'ERROR', 'color': Colors.red};
case IssueSeverity.critical:
return {'icon': 'đ´', 'label': 'CRITICAL', 'color': Colors.purple};
}
}
}
// Example Widgets
class BasicExampleWidget extends StatelessWidget {
const BasicExampleWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Hello Flutter Guard'), // Missing const
const SizedBox(height: 8),
const Text('This is const'), // Good!
const SizedBox(height: 8),
Text('Another text'), // Missing const
],
);
}
}
class DeepNestingWidget extends StatelessWidget {
const DeepNestingWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Column(
children: [
Column(
children: [
Column(
children: [
Column(
children: [
Column(
children: [
Container(
padding: const EdgeInsets.all(8),
color: Colors.red[100],
child: const Text('Too Deep!'),
),
],
),
],
),
],
),
],
),
],
),
],
),
],
);
}
}
class HeavyBuildWidget extends StatelessWidget {
const HeavyBuildWidget({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
12,
(i) => Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Text('Item $i'),
),
),
);
}
}
class ContainerIssuesWidget extends StatelessWidget {
const ContainerIssuesWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(), // Empty container
const SizedBox(height: 8),
Container(
width: 100,
height: 50,
color: Colors.green[200],
), // Should use SizedBox
const SizedBox(height: 8),
Opacity(
opacity: 0.5,
child: Container(
padding: const EdgeInsets.all(16),
color: Colors.orange[200],
child: const Text('Expensive Opacity'),
),
),
],
);
}
}
class GoodPracticesWidget extends StatelessWidget {
const GoodPracticesWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text('All const!'), // Still missing const here for demo
SizedBox(height: 8),
Icon(Icons.check_circle, color: Colors.green),
SizedBox(height: 8),
Padding(padding: EdgeInsets.all(16), child: Text('Good practices')),
],
);
}
}