How to Refactor a FlutKit Screen for Production Use

Estimated reading: 8 minutes 22 views

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:
Dart
lib/demo/dashboard/dashboard_analytics_screen.dart

Within 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):

  • CountryUserData model
  • liveUserData mock list
  • LiveUsersHeatMap widget
  • Map configuration logic
Dart
/// 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:

Dart
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:

Dart
features/analytics/data/models/country_user_data.dart

Dart
class 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:

Dart
features/analytics/data/datasources/live_user_mock_data.dart

Dart
import '../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:

Dart
features/analytics/ui/widgets/live_users_heat_map.dart

Key changes:

  • Import model and data source
  • Keep UI logic intact
  • Avoid introducing state management prematurely
Dart
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:

Dart
features/analytics/ui/screens/dashboard_analytics_screen.dart

Simply import the refactored widget:

Dart
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 PurposeRecommended Action
UI referenceNo refactor
Production useSplit model, data, UI
Complex logicIntroduce state management
API-drivenReplace mock data source

This approach ensures FlutKit remains:

Simple to start. Clean when needed. Scalable by choice.

Share this Doc

How to Refactor a FlutKit Screen for Production Use

Or copy link

CONTENTS