How to Refactor a FlutKit Screen for Production Use
FlutKit demo screens intentionally prioritize clarity and immediacy over architectural purity. Refactoring is an optional, deliberate step when moving toward real-world applications.
When Should You Refactor?
You should consider refactoring a FlutKit screen when:
- The screen will be reused or extended
- The screen consumes real API data
- Business logic starts to grow
- Multiple developers work on the same feature
If the screen is used only as a visual reference, refactoring is not required.
Step 1 — Choose the Demo Screen to Refactor
Start by selecting one specific demo screen, not the entire app.
Example:
- Demo screen: Analytics Dashboard
- File location:
lib/demo/dashboard/dashboard_analytics_screen.dartWithin this screen, we will refactor one concrete component:
Live Users Heat Map
Find LiveUsersHeatMap() class.
This scoped approach avoids unnecessary complexity and keeps refactoring manageable.
Step 2 — Identify What Is Mixed in the Demo File
In the original FlutKit demo implementation, the following concerns live in a single Dart file:
- UI widgets
- Data models
- Mock data
- Visualization configuration
This is intentional in FlutKit demos.
Example (original demo code):
CountryUserDatamodelliveUserDatamock listLiveUsersHeatMapwidget- Map configuration logic
/// LIVE USERS HEAT MAP ///
// Country user data model
class CountryUserData {
final String country;
final int users;
CountryUserData(this.country, this.users);
}
// Mock up data
final List<CountryUserData> liveUserData = [
CountryUserData('United States', 2450),
CountryUserData('India', 1820),
CountryUserData('Indonesia', 1325),
CountryUserData('Brazil', 980),
CountryUserData('Germany', 720),
CountryUserData('Australia', 610),
CountryUserData('Canada', 540),
CountryUserData('France', 500),
CountryUserData('China', 380),
CountryUserData('Nigeria', 240),
];
// UI View
class LiveUsersHeatMap extends StatefulWidget {
const LiveUsersHeatMap({super.key});
@override
State<LiveUsersHeatMap> createState() => _LiveUsersHeatMapState();
}
class _LiveUsersHeatMapState extends State<LiveUsersHeatMap> {
late MapShapeSource _mapSource;
@override
void initState() {
super.initState();
_mapSource = MapShapeSource.asset(
'assets/maps/world_map.json',
shapeDataField: 'name',
dataCount: liveUserData.length,
primaryValueMapper: (index) => liveUserData[index].country,
shapeColorValueMapper: (index) => liveUserData[index].users,
shapeColorMappers: [
MapColorMapper(
from: 0,
to: 300,
color: kSecondaryColor.withValues(alpha: 0.4),
text: '<300',
),
MapColorMapper(
from: 301,
to: 700,
color: kSecondaryColor.withValues(alpha: 0.6),
text: '300-700',
),
MapColorMapper(
from: 701,
to: 1500,
color: kSecondaryColor.withValues(alpha: 0.8),
text: '700-1500',
),
MapColorMapper(
from: 1501,
to: 3000,
color: kSecondaryColor,
text: '>1500',
),
],
);
}
@override
Widget build(BuildContext context) {
final themeData = Theme.of(context);
return Card(
child: Column(
children: [
// header
CardHeader(
kText: 'Live Users',
kWidget: Padding(
padding: EdgeInsetsDirectional.only(end: kDefaultPadding / 2),
child: CustomIconButton(
icon: Icons.info_outline,
onTap: () {},
iconColor: themeData.colorScheme.onSurface,
shape: ButtonShape.circle,
tooltipMessage:
'Displays the locations of users who are currently \nconnected and interacting live, worldwide.',
),
),
),
// heat map
Padding(
padding: EdgeInsets.all(kDefaultPadding),
child: SizedBox(
height: 480,
child: SfMaps(
layers: [
MapShapeLayer(
source: _mapSource,
legend: MapLegend.bar(
MapElement.shape,
position: MapLegendPosition.bottom,
segmentSize: Size(60, 12),
labelsPlacement: MapLegendLabelsPlacement.betweenItems,
padding: EdgeInsets.only(
top: kDefaultPadding,
),
),
tooltipSettings: MapTooltipSettings(
strokeColor: themeData.colorScheme.outline,
color: themeData.colorScheme.inverseSurface,
strokeWidth: 0.4,
),
shapeTooltipBuilder: (BuildContext context, int index) {
final data = liveUserData[index];
final formattedUsers =
NumberFormat.decimalPattern().format(data.users);
return Container(
padding: EdgeInsets.all(kDefaultPadding / 2),
decoration: BoxDecoration(
color: themeData.colorScheme.inverseSurface,
),
child: Text(
'${data.country}\n$formattedUsers live users',
style: TextStyle(
color: themeData.colorScheme.onInverseSurface,
fontSize: kBodySmall,
),
),
);
},
),
],
),
),
),
],
),
);
}
}
Our goal is not to over-engineer, but to separate responsibilities cleanly.
Step 3 — Decide the Target Structure (Minimal Production Split)
For production use, we recommend a lightweight separation using feature-based boundaries.
Target feature structure:
features/
└─ analytics/
├─ data/
│ ├─ models/
│ └─ datasources/
├─ logic/
└─ ui/
├─ widgets/
└─ screens/You do not need Clean Architecture or complex layers unless your project requires it.
Step 4 — Extract the Data Model
Move the model into the feature’s data layer.
File:
features/analytics/data/models/country_user_data.dartclass CountryUserData {
final String country;
final int users;
CountryUserData(this.country, this.users);
}This allows reuse and future extension (e.g., API mapping).
Step 5 — Move Mock Data into a Data Source
Mock data should live outside UI files.
File:
features/analytics/data/datasources/live_user_mock_data.dartimport '../models/country_user_data.dart';
final List<CountryUserData> liveUserMockData = [
CountryUserData('United States', 2450),
CountryUserData('India', 1820),
CountryUserData('Indonesia', 1325),
CountryUserData('Brazil', 980),
CountryUserData('Germany', 720),
CountryUserData('Australia', 610),
CountryUserData('Canada', 540),
CountryUserData('France', 500),
CountryUserData('China', 380),
CountryUserData('Nigeria', 240),
];Later, this file can be replaced with an API or repository implementation.
Step 6 — Refactor the UI Widget (Minimal Change)
Move the widget into the feature UI layer.
File:
features/analytics/ui/widgets/live_users_heat_map.dartKey changes:
- Import model and data source
- Keep UI logic intact
- Avoid introducing state management prematurely
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_maps/maps.dart';
import '../../data/datasources/live_user_mock_data.dart';
import '../../data/models/country_user_data.dart';
class LiveUsersHeatMap extends StatefulWidget {
const LiveUsersHeatMap({super.key});
@override
State<LiveUsersHeatMap> createState() => _LiveUsersHeatMapState();
}
class _LiveUsersHeatMapState extends State<LiveUsersHeatMap> {
late MapShapeSource _mapSource;
@override
void initState() {
super.initState();
_mapSource = MapShapeSource.asset(
'assets/maps/world_map.json',
shapeDataField: 'name',
dataCount: liveUserMockData.length,
primaryValueMapper: (index) => liveUserMockData[index].country,
shapeColorValueMapper: (index) => liveUserMockData[index].users,
shapeColorMappers: _buildColorMappers(),
);
}
List<MapColorMapper> _buildColorMappers() {
return const [
MapColorMapper(from: 0, to: 300, color: Colors.blueGrey, text: '<300'),
MapColorMapper(from: 301, to: 700, color: Colors.blueGrey, text: '300-700'),
MapColorMapper(from: 701, to: 1500, color: Colors.blueGrey, text: '700-1500'),
MapColorMapper(from: 1501, to: 3000, color: Colors.blueGrey, text: '>1500'),
];
}
@override
Widget build(BuildContext context) {
final themeData = Theme.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 480,
child: SfMaps(
layers: [
MapShapeLayer(
source: _mapSource,
tooltipSettings: MapTooltipSettings(
color: themeData.colorScheme.inverseSurface,
),
shapeTooltipBuilder: (context, index) {
final CountryUserData data = liveUserMockData[index];
final formattedUsers =
NumberFormat.decimalPattern().format(data.users);
return Text(
'${data.country}\n$formattedUsers live users',
style: TextStyle(
color: themeData.colorScheme.onInverseSurface,
),
);
},
),
],
),
),
),
);
}
}Notice how the UI remains familiar, while responsibilities are clearer.
Step 7 — Reuse the Widget in the Dashboard Screen
Create a new analytics screen:
features/analytics/ui/screens/dashboard_analytics_screen.dartSimply import the refactored widget:
import 'package:your_app/features/analytics/ui/widgets/live_users_heat_map.dart';This keeps the dashboard layout intact while improving maintainability.
Key Principles to Remember
- Refactor per screen, not globally
- Preserve FlutKit’s copy–paste workflow
- Avoid premature state management
- Architecture is optional, not mandatory
FlutKit provides ready-made UI, not architectural constraints.
You choose when and how far to refactor.
Summary
| Demo Purpose | Recommended Action |
|---|---|
| UI reference | No refactor |
| Production use | Split model, data, UI |
| Complex logic | Introduce state management |
| API-driven | Replace mock data source |
This approach ensures FlutKit remains:
Simple to start. Clean when needed. Scalable by choice.