leo_easy_ui_kit 0.7.6
leo_easy_ui_kit: ^0.7.6 copied to clipboard
Leo Easy UI Kit: effortless yet powerful Flutter UI components.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:leo_easy_ui_kit/leo_easy_ui_kit.dart';
void main() {
runApp(const LeoEasyUiExampleApp());
}
class LeoEasyUiExampleApp extends StatelessWidget {
const LeoEasyUiExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Leo Easy UI Kit - Comprehensive Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF6200EE),
brightness: Brightness.light,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF6200EE),
brightness: Brightness.dark,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// Form demo data
String? role = 'Developer';
List<String> selectedInterests = ['Flutter', 'UI/UX'];
String? experienceLevel = 'Intermediate';
String? employmentStatus = 'Employed';
bool receiveNotifications = true;
int? yearsExperience = 3;
DateTime? startDate = DateTime.now();
List<String> selectedSkills = const [];
String? country = 'United States';
String? selectedPerson;
List<String> uploadedFiles = const [];
List<String> profileImages = const [];
int onboardingStep = 0;
List<String> technologyTags = ['Flutter', 'Dart'];
// Managed form demo data (EasyFormController)
final EasyFormController _profileForm = EasyFormController(<String, Object?>{
'fullName': 'Alex Flutter',
'headline': 'Senior Flutter Developer',
});
// Table demo data
List<Project> selectedProjects = [];
int tablePageIndex = 0;
int tablePageSize = 3;
String projectStatusFilter = 'All';
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
_profileForm.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return EasyResponsiveScaffold(
appBar: AppBar(
elevation: 0,
title: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Leo Easy UI Kit',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Effortless Flutter UI Components',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
bottom: TabBar(
controller: _tabController,
isScrollable: true,
tabs: const [
Tab(icon: Icon(Icons.dynamic_form), text: 'Forms'),
Tab(icon: Icon(Icons.menu), text: 'Selectors'),
Tab(icon: Icon(Icons.table_chart), text: 'Data'),
Tab(icon: Icon(Icons.widgets), text: 'Basic'),
],
),
),
navigationRail: NavigationRail(
selectedIndex: _tabController.index,
onDestinationSelected: (index) {
setState(() {
_tabController.index = index;
});
},
labelType: NavigationRailLabelType.all,
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.dynamic_form),
label: Text('Forms'),
),
NavigationRailDestination(
icon: Icon(Icons.menu),
label: Text('Selectors'),
),
NavigationRailDestination(
icon: Icon(Icons.table_chart),
label: Text('Data'),
),
NavigationRailDestination(
icon: Icon(Icons.widgets),
label: Text('Basic'),
),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _tabController.index,
onDestinationSelected: (index) {
setState(() {
_tabController.index = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.dynamic_form),
label: 'Forms',
),
NavigationDestination(
icon: Icon(Icons.menu),
label: 'Selectors',
),
NavigationDestination(
icon: Icon(Icons.table_chart),
label: 'Data',
),
NavigationDestination(
icon: Icon(Icons.widgets),
label: 'Basic',
),
],
),
body: TabBarView(
controller: _tabController,
children: [
_buildFormsTab(),
_buildSelectorsTab(),
_buildDataTab(),
_buildBasicTab(),
],
),
);
}
Widget _buildFormsTab() {
return ListView(
padding: const EdgeInsets.all(24),
children: [
_SectionHeader(
icon: Icons.person,
title: 'Professional Profile Form',
subtitle: 'Showcase of various form components',
),
const SizedBox(height: 24),
_FormSection(
title: 'Onboarding Stepper',
description:
'Horizontal, tappable indicator for multi-step flows and wizards',
child: easyStepperOf<int>(
steps: const [
EasyStepConfig(id: 0, title: 'Profile', subtitle: 'Basic info'),
EasyStepConfig(id: 1, title: 'Skills', subtitle: 'Expertise'),
EasyStepConfig(id: 2, title: 'Review', subtitle: 'Summary'),
],
currentStep: onboardingStep,
onStepChanged: (step) => setState(() => onboardingStep = step),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Dropdown Selection',
description: 'Simple single-select dropdown',
child: easyDropdownOf<String>(
items: const ['Developer', 'Designer', 'Manager', 'Student'],
value: role,
onChanged: (value) => setState(() => role = value),
itemLabel: (v) => v,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Button Group (Radio Alternative)',
description: 'Visually rich single-choice selector',
child: easyButtonGroupOf<String>(
items: const ['Beginner', 'Intermediate', 'Advanced', 'Expert'],
value: experienceLevel,
onChanged: (value) => setState(() => experienceLevel = value),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Multi-Select Checkboxes',
description: 'Select multiple interests',
child: easyCheckboxOf<String>(
items: const [
'Flutter',
'UI/UX',
'Backend',
'DevOps',
'Mobile',
'Web'
],
values: selectedInterests,
onChangedMany: (values) =>
setState(() => selectedInterests = values),
multiSelect: true,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Radio Group',
description: 'Traditional radio button selection',
child: easyRadioOf<String>(
items: const ['Employed', 'Freelance', 'Student', 'Seeking'],
value: employmentStatus,
onChanged: (value) => setState(() => employmentStatus = value),
itemLabel: (v) => v,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Switch Toggle',
description: 'Binary on/off controls',
child: easySwitchOf<String>(
items: const ['Enable Notifications'],
value: receiveNotifications ? 'Enable Notifications' : null,
onChanged: (value) =>
setState(() => receiveNotifications = value != null),
itemLabel: (v) => v,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Numeric Input',
description: 'Type-safe integer field with validation',
child: easyIntFieldOf(
value: yearsExperience,
onChanged: (value) => setState(() => yearsExperience = value),
hintText: 'Years of experience',
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Date Picker',
description: 'Native date picker integration',
child: easyDateFieldOf(
value: startDate,
onChanged: (value) => setState(() => startDate = value),
itemLabel: (date) => '${date.year}-${date.month}-${date.day}',
firstDate: DateTime(2000),
lastDate: DateTime(2030),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'File Picker',
description:
'Bottom-sheet category picker wired to your own file API',
child: EasyFilePicker<String>(
label: 'Attach files',
allowMultiple: true,
values: uploadedFiles,
onChangedMany: (files) => setState(() => uploadedFiles = files),
onPick: (category, allowMultiple) async {
// Integrate with `file_picker` or any custom implementation.
await Future.delayed(const Duration(milliseconds: 300));
return [
'${category.label} sample ${uploadedFiles.length + 1}.txt',
];
},
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Image Picker',
description:
'Camera / gallery picker that delegates to your own image source',
child: EasyImagePicker<String>(
allowMultiple: true,
values: profileImages,
onChangedMany: (images) => setState(() => profileImages = images),
onPick: (source, allowMultiple) async {
// Integrate with `image_picker` or any other source.
await Future.delayed(const Duration(milliseconds: 300));
return [
'${source.label} image ${profileImages.length + 1}',
];
},
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Managed Form (EasyFormController)',
description:
'Name and headline fields bound to EasyFormController by name.',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
easyTextFormFieldOf<String>(
form: _profileForm,
name: 'fullName',
hintText: 'Full name',
),
const SizedBox(height: 12),
easyTextFormFieldOf<String>(
form: _profileForm,
name: 'headline',
hintText: 'Headline (e.g. Flutter developer)',
),
const SizedBox(height: 12),
Builder(
builder: (context) {
final values = _profileForm.rawValues;
return Text(
'Form values: '
"${values['fullName'] ?? ''} · "
"${values['headline'] ?? ''}",
style: Theme.of(context).textTheme.bodySmall,
);
},
),
],
),
),
const SizedBox(height: 24),
_buildSummaryCard(),
],
);
}
Widget _buildSelectorsTab() {
return ListView(
padding: const EdgeInsets.all(24),
children: [
_SectionHeader(
icon: Icons.touch_app,
title: 'Advanced Selectors',
subtitle: 'Chips, segmented controls, and modal selectors',
),
const SizedBox(height: 24),
_FormSection(
title: 'Choice Chips',
description: 'Tag-style multi-select with visual feedback',
child: easyChipsOf<String>(
items: const [
'TypeScript',
'Dart',
'Python',
'Rust',
'Go',
'Swift'
],
values: selectedSkills,
onChangedMany: (values) => setState(() => selectedSkills = values),
multiSelect: true,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Tag Input',
description: 'Free-form tags backed by strongly-typed values',
child: easyTagInputOf(
tags: technologyTags,
onChanged: (tags) => setState(() => technologyTags = tags),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Segmented Button',
description: 'Material 3 segmented control',
child: easySegmentedOf<String>(
items: const ['Day', 'Week', 'Month', 'Year'],
value: 'Week',
onChanged: (value) {},
itemLabel: (v) => v,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Bottom Sheet Selector',
description: 'Modal sheet for picking from a large list',
child: easyBottomSheetSelectOf<String>(
items: const [
'United States',
'Canada',
'United Kingdom',
'Germany',
'France',
'Japan',
'Australia',
'Brazil',
],
value: country,
onChanged: (value) => setState(() => country = value),
itemLabel: (v) => v,
sheetTitle: const Text('Select Country'),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Popup Dialog Selector',
description: 'Dialog-based selection with search',
child: easyPopupSelectOf<String>(
items: const [
'Light Theme',
'Dark Theme',
'System Default',
'High Contrast'
],
value: 'System Default',
onChanged: (value) {},
itemLabel: (v) => v,
title: const Text('Select Theme'),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Search with Autocomplete',
description: 'Real-time search and recommendation',
child: easySearchOf<String>(
items: const [
'Alice Johnson',
'Bob Smith',
'Charlie Brown',
'Diana Prince',
'Eve Davis',
'Frank Miller',
'Grace Hopper',
],
itemLabel: (v) => v,
onSelected: (value) {
setState(() => selectedPerson = value);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Selected: $value'),
behavior: SnackBarBehavior.floating,
),
);
},
hintText: 'Search for a person...',
),
),
],
);
}
Widget _buildDataTab() {
final filteredProjects = projectStatusFilter == 'All'
? _sampleProjects
: _sampleProjects
.where((p) => p.status == projectStatusFilter)
.toList(growable: false);
final start = tablePageIndex * tablePageSize;
final end = (start + tablePageSize).clamp(0, filteredProjects.length);
final paginatedProjects = filteredProjects.sublist(start, end);
return ListView(
padding: const EdgeInsets.all(24),
children: [
_SectionHeader(
icon: Icons.table_rows,
title: 'Data Tables & Lists',
subtitle: 'Display and select structured data',
),
const SizedBox(height: 24),
_FormSection(
title: 'Filter Bar',
description:
'Responsive container for filters (chips, buttons, search)',
child: easyFilterBarOf(
leading: const Icon(Icons.filter_list),
filters: [
easyButtonGroupOf<String>(
items: const ['All', 'Completed', 'In Progress', 'Planned'],
value: projectStatusFilter,
onChanged: (value) => setState(() {
projectStatusFilter = value ?? 'All';
tablePageIndex = 0;
}),
),
],
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Selectable Data Table',
description: 'Multi-select table with easy row selection',
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: easyTableOf<Project>(
items: paginatedProjects,
columns: const [
DataColumn(label: Text('Project')),
DataColumn(label: Text('Status')),
DataColumn(label: Text('Progress')),
],
cellBuilder: (context, project, colIndex, selected) {
switch (colIndex) {
case 0:
return Text(project.name);
case 1:
return _StatusBadge(project.status);
case 2:
return Text('${project.progress}%');
default:
return const Text('');
}
},
selectedValues: selectedProjects,
onChangedMany: (values) =>
setState(() => selectedProjects = values),
multiSelect: true,
),
),
),
const SizedBox(height: 16),
Text(
'Selected projects: ${selectedProjects.length}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
easyPaginationOf(
itemCount: filteredProjects.length,
pageIndex: tablePageIndex,
pageSize: tablePageSize,
onPageChanged: (index) => setState(() => tablePageIndex = index),
onPageSizeChanged: (size) => setState(() {
tablePageSize = size;
tablePageIndex = 0;
}),
),
const SizedBox(height: 24),
_FormSection(
title: 'Selectable List',
description: 'Simple list with item selection',
child: easyListOf<String>(
items: const ['Inbox', 'Starred', 'Sent', 'Drafts', 'Trash'],
value: 'Inbox',
onChanged: (value) {},
itemLabel: (v) => v,
divided: true,
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Menu (Dropdown Alternative)',
description: 'Popup menu for actions or selections',
child: easyMenuOf<String>(
items: const ['Profile', 'Settings', 'Help', 'Logout'],
value: null,
onChanged: (value) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Menu action: $value'),
behavior: SnackBarBehavior.floating,
),
);
},
itemLabel: (v) => v,
tooltip: 'Open menu',
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Grid View',
description: 'Card-style grid selection using easyGridOf',
child: SizedBox(
height: 220,
child: easyGridOf<Project>(
items: _sampleProjects,
values: selectedProjects,
onChangedMany: (values) =>
setState(() => selectedProjects = values),
multiSelect: true,
itemLabel: (p) => p.name,
crossAxisCount: 2,
childAspectRatio: 4 / 3,
),
),
),
const SizedBox(height: 24),
_FormSection(
title: 'Searchable Grid',
description: 'Search + grid of projects with custom tiles',
child: easySearchableGridOf<Project>(
items: _sampleProjects,
itemLabel: (p) => p.name,
itemBuilder: (context, project, selected) => Card(
elevation: selected ? 4 : 1,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
project.name,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
_StatusBadge(project.status),
const Spacer(),
Text('${project.progress}% complete'),
],
),
),
),
values: selectedProjects,
onChangedMany: (values) =>
setState(() => selectedProjects = values),
multiSelect: true,
hintText: 'Search projects...',
crossAxisCount: 2,
childAspectRatio: 4 / 3,
),
),
],
);
}
Widget _buildBasicTab() {
return EasyScroll(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionHeader(
icon: Icons.widgets,
title: 'Basic Building Blocks',
subtitle: 'Simplified wrappers for common widgets',
),
const SizedBox(height: 24),
_FormSection(
title: 'EasyText',
description: 'Simplified text styling',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const EasyText(
'Headline Text',
size: 24,
weight: FontWeight.bold,
color: Colors.deepPurple,
),
const SizedBox(height: 8),
EasyText(
'This is a body text with some custom styling like letter spacing and line height.',
size: 16,
color: Colors.grey[700],
height: 1.5,
letterSpacing: 0.5,
),
],
),
),
const SizedBox(height: 24),
_FormSection(
title: 'EasyContainer',
description: 'Simplified container with shadow and tap',
child: Row(
children: [
EasyContainer(
width: 100,
height: 100,
color: Colors.white,
borderRadius: BorderRadius.circular(16),
elevation: 4,
alignment: Alignment.center,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Container Tapped!'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const EasyText('Tap Me', weight: FontWeight.bold),
),
const SizedBox(width: 16),
EasyContainer(
width: 100,
height: 100,
color: Colors.blue.withValues(alpha: 0.1),
borderColor: Colors.blue,
borderWidth: 2,
shape: BoxShape.circle,
alignment: Alignment.center,
child: const Icon(Icons.star, color: Colors.blue, size: 32),
),
],
),
),
const SizedBox(height: 24),
_FormSection(
title: 'EasyImage',
description: 'Simplified image loading',
child: Row(
children: [
const EasyContainer(
width: 100,
height: 100,
borderRadius: BorderRadius.all(Radius.circular(12)),
clipBehavior: Clip.antiAlias,
child: EasyImage(
image: 'https://picsum.photos/200',
fit: BoxFit.cover,
isNetwork: true,
placeholder: Center(child: CircularProgressIndicator()),
errorWidget: Center(child: Icon(Icons.error)),
),
),
const SizedBox(width: 16),
EasyContainer(
width: 100,
height: 100,
shape: BoxShape.circle,
clipBehavior: Clip.antiAlias,
child: const EasyImage(
image: 'https://picsum.photos/201',
fit: BoxFit.cover,
isNetwork: true,
),
),
],
),
),
const SizedBox(height: 24),
_FormSection(
title: 'EasyScroll',
description: 'Simplified scroll view (used for this tab)',
child: EasyContainer(
height: 150,
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.all(12),
child: EasyScroll(
showScrollbar: true,
child: Column(
children: List.generate(
20,
(i) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text('Scroll Item $i'),
),
),
),
),
),
),
],
),
);
}
Widget _buildSummaryCard() {
return Card(
elevation: 0,
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
const SizedBox(width: 12),
Text(
'Profile Summary',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
const SizedBox(height: 16),
_SummaryRow('Role', role ?? 'Not selected'),
_SummaryRow(
'Experience Level',
experienceLevel ?? 'Not selected',
),
_SummaryRow(
'Interests',
selectedInterests.join(', ').ifEmpty,
),
_SummaryRow(
'Employment Status',
employmentStatus ?? 'Not selected',
),
_SummaryRow(
'Notifications',
receiveNotifications ? 'Enabled' : 'Disabled',
),
_SummaryRow(
'Years Experience',
yearsExperience?.toString() ?? 'Not specified',
),
_SummaryRow(
'Files',
uploadedFiles.isEmpty
? 'No files attached'
: '${uploadedFiles.length} file(s) attached',
),
_SummaryRow(
'Images',
profileImages.isEmpty
? 'No images selected'
: '${profileImages.length} image(s) selected',
),
],
),
),
);
}
}
// Helper Widgets
class _SectionHeader extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
const _SectionHeader({
required this.icon,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color
?.withValues(alpha: 0.6),
),
),
],
),
),
],
);
}
}
class _FormSection extends StatelessWidget {
final String title;
final String description;
final Widget child;
const _FormSection({
required this.title,
required this.description,
required this.child,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color
?.withValues(alpha: 0.6),
),
),
const SizedBox(height: 12),
child,
],
);
}
}
class _SummaryRow extends StatelessWidget {
final String label;
final String value;
const _SummaryRow(this.label, this.value);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 140,
child: Text(
'$label:',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.8),
),
),
),
Expanded(
child: Text(
value,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
],
),
);
}
}
class _StatusBadge extends StatelessWidget {
final String status;
const _StatusBadge(this.status);
@override
Widget build(BuildContext context) {
Color color;
switch (status) {
case 'Completed':
color = Colors.green;
break;
case 'In Progress':
color = Colors.orange;
break;
case 'Planned':
color = Colors.blue;
break;
default:
color = Colors.grey;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
status,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
// Data models
class Project {
final String name;
final String status;
final int progress;
Project(this.name, this.status, this.progress);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Project &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
}
final _sampleProjects = [
Project('Mobile App Redesign', 'In Progress', 65),
Project('API Integration', 'Completed', 100),
Project('User Dashboard', 'In Progress', 40),
Project('Analytics Module', 'Planned', 0),
Project('Payment Gateway', 'Completed', 100),
];
extension on String {
String get ifEmpty => isEmpty ? 'None' : this;
}