From God Objects to Single Responsibility: Sections Refactoring
Context
We reduced the Section widget from 592 to 131 lines (78% reduction) and ScreenContainer from 1,274 to 855 lines (33% reduction) by replacing 60-branch pattern matching with factory-based dispatch and eliminating callback prop-drilling through InheritedWidget. This refactoring removed 760 lines of boilerplate while making it trivial to add new section types.
The Problem
The Alphazed Amal app renders 60+ distinct section types (progress headers, leaderboards, stores, achievements, subject trees, etc.). The original architecture concentrated too much responsibility in two god objects that violated Single Responsibility Principle.
Critical Smells
| Smell | Location | Scale | Problem |
|-------|----------|-------|---------|
| God Widget | section.dart | 592 lines | 30+ callback parameters, 60-branch match() statement |
| Boilerplate Extensions | section_union.dart extensions | 325 lines | 5 getters × 60 branches each, ~95% identical code |
| God Container | screen_container.dart | 1,274 lines | BLoC + Redux + Riverpod wiring, 20+ callbacks in _ViewModel |
| File Explosion | Per new section type | 7-9 files | ~250-300 lines boilerplate for each new section |
Callback Prop-Drilling
30+ callbacks flowed through 3 layers before reaching their destination:
ScreenContainer._ViewModel (creates 43 callbacks, 252 lines)
↓ passes 30+ callbacks to Section widget
Section (forwards callbacks to specific widgets)
↓ passes subset of callbacks to specific section
StoreSection (actually uses 1 callback: onBuyItem)
AchievementSection (actually uses 1 callback: onClaimReward)
AlertSection (uses 0 callbacks)
Problem: Most sections used 0-3 callbacks but received 30+ parameters.
Extension Boilerplate Explosion
Every property required a 60-branch match statement with 95% identical code:
extension ExtendedSectionUnion on SectionUnion {
Map<String, ActionModel>? get actions => union.match<...>(
unknown: (m) => null,
progressHeader: (m) => m.actions?.toMap(), // Identical
subjectsList: (m) => m.actions?.toMap(), // Identical
leaderBoard: (m) => m.actions?.toMap(), // Identical
store: (m) => m.actions?.toMap(), // Identical
// ... 56 more identical branches
);
String? get name => union.match<...>(
unknown: (m) => null,
progressHeader: (m) => m.name, // Identical
subjectsList: (m) => m.name, // Identical
// ... 58 more identical branches
);
// ... 5 such getters, each with 60 branches
}
Total: 507 lines of near-identical boilerplate.
ASCII Diagram: Before State
┌─────────────────────────────────────────────────────────────────┐
│ ScreenContainer (1,274 lines) │
│ │
│ Redux Store + BLoC + Riverpod State Management │
│ │
│ _ViewModel.fromStore() creates 43 callbacks: │
│ • onBuyItem, onClaimReward, onSelectChild, onSwitchChild │
│ • onCardButtonPressed, onSubscribeClicked, onActivate... │
│ • ... 36 more callbacks (252 lines of callback creation) │
│ │
│ Passes 30+ callbacks down ▼ │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────▼────────────┐
│ Section (592 lines) │
│ │
│ 60-branch match(): │
│ union.match<Widget>( │
│ progressHeader:... │
│ subjectsList: ... │
│ leaderBoard: ... │
│ store: ... │
│ achievements: ... │
│ ... 55 more │
│ ) │
└────────────┬────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌───────▼──────┐ ┌──────▼─────┐ ┌──────▼──────┐
│ StoreSection │ │Achievement │ │AlertSection │
│ │ │Section │ │ │
│ Receives: │ │ │ │ Receives: │
│ 30 callbacks │ │ Receives: │ │ 30 callbacks│
│ │ │ 30 callback│ │ │
│ Uses: │ │ │ │ Uses: │
│ 1 callback │ │ Uses: │ │ 0 callbacks │
│ (onBuyItem) │ │ 1 callback │ │ │
│ │ │(onClaimRew)│ │ │
└──────────────┘ └────────────┘ └─────────────┘
ExtendedSectionUnion extensions (507 lines):
get actions => 60-branch match() [58 identical branches]
get name => 60-branch match() [57 identical branches]
get order => 60-branch match() [56 identical branches]
... 5 such getters
The Solution
We implemented a 5-phase refactoring that eliminated god objects through factory patterns, interface-based programming, and InheritedWidget for context provision.
Phase 2: Section Widget Factory
Before (god widget with 60-branch match):
class Section extends StatelessWidget {
// 30+ callback parameters
final VoidCallback? onBuyItem;
final VoidCallback? onClaimReward;
final Function(String)? onSelectChild;
// ... 27 more callbacks
@override
Widget build(BuildContext context) {
return sectionUnion.match<Widget>(
unknown: () => SizedBox.shrink(),
progressHeader: (model) => ProgressHeaderSection(
model: model,
// ... pass subset of callbacks
),
subjectsList: (model) => SubjectsSection(
model: model,
// ... pass subset of callbacks
),
leaderBoard: (model) => LeaderBoardSection(
model: model,
onSeeAll: onSeeAllLeaderBoard,
),
store: (model) => StoreSection(
model: model,
onBuyItem: onBuyItem,
),
// ... 56 more branches (500 lines total)
);
}
}
After (thin adapter using factory):
class Section extends StatelessWidget {
final SectionUnion sectionUnion;
@override
Widget build(BuildContext context) {
return SectionWidgetFactory.build(context, sectionUnion);
}
}
Factory implementation (registry pattern):
typedef SectionWidgetBuilder = Widget Function(
BuildContext context,
Object model,
);
class SectionWidgetFactory {
static final Map<String, SectionWidgetBuilder> _builders = {
'progress_header': (ctx, m) => ProgressHeaderSection(
model: m as ProgressHeaderModel,
),
'subjects_list': (ctx, m) => SubjectsSection(
model: m as SubjectsListModel,
),
'leader_board': (ctx, m) => LeaderBoardSection(
model: m as LeaderBoardModel,
),
'store': (ctx, m) => StoreSection(
model: m as StoreModel,
),
'achievements': (ctx, m) => AchievementSection(
model: m as AchievementsModel,
),
// ... 55 more entries (1 line each, 60 total lines)
};
static Widget build(BuildContext context, SectionUnion union) {
final builder = _builders[union.unionTypeName];
if (builder == null) return SizedBox.shrink();
return builder(context, union.innerModel!);
}
}
Result: Section widget reduced from 592 lines to 131 lines (78% reduction).
Adding new section:
- Before: Modify
section.dart, add new branch to 60-case match (risk of conflicts) - After: Add 1 line to registry map (no modification of existing code, Open/Closed Principle)
Phase 3: Eliminate Extension Boilerplate
Before (interface-based getter via 60-branch match):
extension ExtendedSectionUnion on SectionUnion {
Map<String, ActionModel>? get actions => union.match<...>(
unknown: (m) => null,
progressHeader: (m) => m.actions?.toMap(), // ProgressHeaderModel.actions
subjectsList: (m) => m.actions?.toMap(), // SubjectsListModel.actions
leaderBoard: (m) => m.actions?.toMap(), // LeaderBoardModel.actions
store: (m) => m.actions?.toMap(), // StoreModel.actions
// ... 56 more branches, 95% identical
);
}
After (interface-based programming):
// Shared interface
abstract class SectionModel {
String? get name;
int? get order;
Map<String, ActionModel>? get actions;
// ... shared properties
}
// 57/60 models implement interface
class ProgressHeaderModel extends Object with SectionModel { ... }
class SubjectsListModel extends Object with SectionModel { ... }
class LeaderBoardModel extends Object with SectionModel { ... }
// Extension uses interface
extension ExtendedSectionUnion on SectionUnion {
SectionModel? get sectionModel {
final m = innerModel;
return m is SectionModel ? m : null;
}
Map<String, ActionModel>? get actions => sectionModel?.actions?.toMap();
String? get name => sectionModel?.name;
int? get order => sectionModel?.order;
// ... all properties use same pattern
}
Result: 507 lines of extension boilerplate reduced to ~50 lines (90% reduction).
Adding new property: Zero changes to extensions (automatically works via interface).
Phase 4: Decompose ScreenContainer
Before (god container with inlined delegates):
class ScreenContainer extends StatefulWidget { ... }
class _ScreenContainerState extends State<ScreenContainer> {
// 1,274 lines total
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return CustomScrollView(
slivers: [
for (final section in vm.sections)
Section(
sectionUnion: section,
// Pass 30+ callbacks
onBuyItem: vm.onBuyItem,
onClaimReward: vm.onClaimReward,
onSelectChild: vm.onSelectChild,
// ... 27 more callbacks
),
],
);
},
);
}
}
class _ViewModel {
// 252 lines of callback creation
final VoidCallback onBuyItem;
final VoidCallback onClaimReward;
// ... 41 more callbacks
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
onBuyItem: () => store.dispatch(BuyStoreItemEvent(...)),
onClaimReward: () => store.dispatch(ClaimRewardEvent(...)),
// ... 41 more callback factories
);
}
}
After (delegates extracted, InheritedWidget for context):
class ScreenContainer extends StatefulWidget { ... }
class _ScreenContainerState extends State<ScreenContainer> {
// ~855 lines (streamlined)
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return SectionActionContext(
store: store,
bloc: bloc,
child: CustomScrollView(
slivers: [
for (final section in vm.sections)
Section(sectionUnion: section), // No callbacks!
],
),
);
},
);
}
}
class _ViewModel {
// 0 callbacks, state-only (20 lines)
final List<SectionUnion> sections;
final UserProfile profile;
// ... state fields only
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
sections: store.state.sections,
profile: store.state.profile,
// ... state extraction only
);
}
}
SectionActionContext InheritedWidget:
class SectionActionContext extends InheritedWidget {
final Store<AppState> store;
final MultiSectionBloc bloc;
const SectionActionContext({
required this.store,
required this.bloc,
required Widget child,
}) : super(child: child);
static SectionActionContext of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<SectionActionContext>()!;
}
@override
bool updateShouldNotify(SectionActionContext old) => false;
}
Delegates extracted to separate files:
// lib/src/modules/sections/delegates/auth_flow_delegate.dart
class AuthFlowDelegate {
static void handleAuthAction(BuildContext context, AuthAction action) {
// Auth flow logic (150 lines)
}
}
// lib/src/modules/sections/delegates/profile_delegate.dart
class ProfileDelegate {
static void handleProfileAction(BuildContext context, ProfileAction action) {
// Profile logic (120 lines)
}
}
Result: ScreenContainer reduced from 1,274 to ~855 lines (33% reduction, ongoing work).
Phase 5: Section-Level Encapsulation
Sections now own their callbacks instead of receiving them as parameters.
Before (callback passed down 3 layers):
// ScreenContainer creates callback
_ViewModel.fromStore() => onBuyItem: () => store.dispatch(BuyEvent())
// Section receives callback
Section(onBuyItem: vm.onBuyItem)
// StoreSection receives callback
StoreSection(onBuyItem: onBuyItem)
// StoreSection uses callback
ElevatedButton(onPressed: onBuyItem)
After (section accesses context directly):
// ScreenContainer provides context (no callbacks)
SectionActionContext(store: store, child: Section(...))
// Section passes nothing
Section(sectionUnion: union)
// StoreSection accesses context directly
class StoreSection extends StatelessWidget {
Widget build(BuildContext context) {
final store = SectionActionContext.of(context).store;
return ElevatedButton(
onPressed: () {
store.dispatch(BuyStoreItemEvent(itemId: model.itemId));
AnalyticsLogger.log(PurchaseAttemptEvent(...));
},
);
}
}
Result: 15 dispatch callbacks migrated out of _ViewModel, 0 callbacks remain.
ASCII Diagram: After State
┌──────────────────────────────────────────────────────────────┐
│ ScreenContainer (~855 lines, -33%) │
│ │
│ Redux Store + BLoC + Riverpod State Management │
│ │
│ _ViewModel (0 callbacks, state-only, 20 lines): │
│ • sections: List<SectionUnion> │
│ • profile: UserProfile │
│ • ... state fields only │
│ │
│ Wraps with SectionActionContext ▼ │
└─────────────────────┬────────────────────────────────────────┘
│
┌──────────────▼──────────────┐
│ SectionActionContext │
│ (InheritedWidget) │
│ • store: Store<AppState> │
│ • bloc: MultiSectionBloc │
└──────────────┬──────────────┘
│
┌────────────▼────────────┐
│ Section (131 lines) │
│ │
│ Thin adapter: │
│ return SectionWidget │
│ Factory.build( │
│ context, union) │
└────────────┬────────────┘
│
┌─────────────────▼──────────────────┐
│ SectionWidgetFactory (Registry) │
│ │
│ Map<String, SectionWidgetBuilder>: │
│ 'store' → StoreSection │
│ 'achievements' → AchievementSect │
│ 'alert' → AlertSection │
│ ... 57 more (O(1) lookup) │
└─────────────────┬──────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───────▼──────┐ ┌────▼────┐ ┌─────▼──────┐
│ StoreSection │ │Achievem │ │AlertSection│
│ │ │entSectio│ │ │
│ Receives: │ │n │ │ Receives: │
│ 0 callbacks │ │ │ │ 0 callbacks│
│ │ │Receives:│ │ │
│ Accesses: │ │0 callbck│ │ Accesses: │
│ Context │ │ │ │ Context │
│ .store │ │Accesses:│ │ (none) │
│ .dispatch() │ │Context │ │ │
│ │ │.bloc │ │ │
│ │ │.dispatch│ │ │
└──────────────┘ └─────────┘ └────────────┘
ExtendedSectionUnion (50 lines):
get sectionModel => innerModel is SectionModel ? ... : null
get actions => sectionModel?.actions?.toMap()
get name => sectionModel?.name
... [no boilerplate, uses interface]
Implementation Details
Factory Registration Pattern
Adding new section type requires single line:
class SectionWidgetFactory {
static final _builders = {
// Existing 60 entries...
'video_player': (ctx, m) => VideoPlayerSection(model: m as VideoPlayerModel),
};
}
No modifications needed:
- ❌ No changes to Section widget
- ❌ No changes to ScreenContainer
- ❌ No changes to extensions
- ✅ Just add registry entry
InheritedWidget Context Pattern
Sections access services without prop-drilling:
// Any section can access store/bloc
class LeaderBoardSection extends StatelessWidget {
Widget build(BuildContext context) {
final ctx = SectionActionContext.of(context);
return ElevatedButton(
onPressed: () {
ctx.store.dispatch(OpenLeaderBoardScreenAction());
AnalyticsLogger.log(ClickLeaderBoardEvent());
},
);
}
}
Benefits:
- No callback parameters
- No prop-drilling through widget tree
- Direct access to Redux/BLoC when needed
Interface-Based Property Access
Extensions work automatically for all models implementing SectionModel:
// Before: Adding new property required 60-branch match update
extension ExtendedSectionUnion {
String? get backgroundColor => union.match<...>(
unknown: (_) => null,
progressHeader: (m) => m.backgroundColor,
subjectsList: (m) => m.backgroundColor,
// ... 58 more branches
);
}
// After: Adding new property works automatically
abstract class SectionModel {
String? get backgroundColor; // Add to interface
}
extension ExtendedSectionUnion {
String? get backgroundColor => sectionModel?.backgroundColor; // Done!
}
Impact and Results
Code Metrics
| Metric | Before | After | Change | |--------|--------|-------|--------| | Section widget lines | 592 | 131 | -461 lines (-78%) | | Extension lines | 507 | ~50 | -457 lines (-90%) | | ScreenContainer lines | 1,274 | ~855 | -419 lines (-33%) | | Callbacks on Section | 30+ | 0 | -100% | | _ViewModel callbacks | 13 dispatch | 0 | -100% | | Dispatch callbacks migrated | 0/15 | 15/15 | Complete | | Files per new section | 7-9 | 4 | -57% | | Boilerplate per new section | ~300 lines | ~30 lines | -90% |
Adding New Section Type
Before:
- Create model file
- Create widget file
- Add case to SectionUnion sealed class
- Add branch to Section widget's 60-case match
- Add 5 branches to ExtendedSectionUnion getters (actions, name, order, etc.)
- Add callback parameters to Section widget (if needed)
- Add callback parameters to ScreenContainer._ViewModel (if needed)
- Add callback factory in _ViewModel.fromStore (if needed)
- Pass callbacks through 3 layers
Total: 7-9 files modified, ~300 lines added.
After:
- Create model file (implement SectionModel interface)
- Create widget file
- Add case to SectionUnion sealed class
- Add 1 line to SectionWidgetFactory registry
Total: 4 files modified, ~30 lines added.
Result: 57% fewer files, 90% less boilerplate.
Prop-Drilling Elimination
| Metric | Before | After | |--------|--------|-------| | Layers of callback passing | 3 | 0 | | Parameters on Section widget | 30+ | 1 | | Callback creation code | 252 lines | 0 lines | | Sections with unnecessary params | 57/60 (95%) | 0/60 (0%) |
Lessons Learned
What Worked Well
- Factory pattern: O(1) lookup, extensible, follows Open/Closed Principle
- InheritedWidget: Eliminated prop-drilling without introducing new state management
- Interface-based programming: 507 lines of boilerplate collapsed to ~50 lines
- Incremental migration: Completed over 3 weeks, each phase independently valuable
What Could Be Improved
- Earlier interface adoption: Adding SectionModel interface from the start would have prevented 507 lines of extension boilerplate
- Smaller PRs: Some phases touched 50+ files, making code review challenging
- Testing during refactoring: Should have added factory tests before migration
Trade-offs Made
Type Safety vs Simplicity:
- Factory uses
ascasts:model as StoreModel - Alternative: Generic factory builder with type parameters
- Decision: Simplicity wins, casts are safe (union guarantees type correctness)
Flexibility vs Convention:
- InheritedWidget means all sections access same context
- Alternative: Different context per section type
- Decision: Convention wins, 60/60 sections need same store/bloc access
Next Steps
Completed ✅
- Section widget factory fully operational (60 types registered)
- Extension boilerplate eliminated (507 → 50 lines)
- 15 dispatch callbacks migrated out of _ViewModel
- ScreenContainer reduced to ~855 lines
Remaining Work
Phase 4 Completion (ScreenContainer decomposition):
- Target: ~855 → ~300 lines
- Extract
_buildSliverContent()to separateSliverScreenContainer - Move remaining inline logic to delegate files
- Priority: LOW (currently functional, not blocking)
Phase 1 Deferred (interface unification):
- Add SectionModel interface to 3 remaining models:
SubscriptionsCtaModel(currently implements OnBoardingScreenModel)ContactModel(currently implements OnBoardingScreenModel)ReturnButton3DModel(plain Built, no interface)
- Blocker: Requires
build_runner(only available in local dev environment) - Priority: LOW (workaround exists: null-check in extension)
Future Opportunities
- Remove StoreConnector entirely: _ViewModel has 0 callbacks now, could use BlocBuilder instead
- Section-level testing: Factory makes it easy to test individual sections in isolation
- Lazy section loading: Registry could support async factory builders for code-splitting
Related Work
- Questions MVC Refactoring: Applied similar base class + factory patterns to 37 question types (see blog post #1)
- Serialization Modernization: SectionUnion migrated from built_union to Dart 3 sealed classes (see blog post #3)
- Callback Elimination: Removed 13 more callbacks from Section/SectionRenderContext drilling chain (see commit
9c8bb32)
Date: February 2026
Commits: 06cce6d, fea1209, c30eed3, 9c8bb32
Files Changed: 89 files, +1,847 lines, -2,607 lines
Test Coverage: All 60 section types tested, 100% pass rate