← Back

From God Objects to Single Responsibility: Sections Refactoring

·architecture

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:

  1. Create model file
  2. Create widget file
  3. Add case to SectionUnion sealed class
  4. Add branch to Section widget's 60-case match
  5. Add 5 branches to ExtendedSectionUnion getters (actions, name, order, etc.)
  6. Add callback parameters to Section widget (if needed)
  7. Add callback parameters to ScreenContainer._ViewModel (if needed)
  8. Add callback factory in _ViewModel.fromStore (if needed)
  9. Pass callbacks through 3 layers

Total: 7-9 files modified, ~300 lines added.

After:

  1. Create model file (implement SectionModel interface)
  2. Create widget file
  3. Add case to SectionUnion sealed class
  4. 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

  1. Factory pattern: O(1) lookup, extensible, follows Open/Closed Principle
  2. InheritedWidget: Eliminated prop-drilling without introducing new state management
  3. Interface-based programming: 507 lines of boilerplate collapsed to ~50 lines
  4. Incremental migration: Completed over 3 weeks, each phase independently valuable

What Could Be Improved

  1. Earlier interface adoption: Adding SectionModel interface from the start would have prevented 507 lines of extension boilerplate
  2. Smaller PRs: Some phases touched 50+ files, making code review challenging
  3. Testing during refactoring: Should have added factory tests before migration

Trade-offs Made

Type Safety vs Simplicity:

  • Factory uses as casts: 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 separate SliverScreenContainer
  • 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

  1. Remove StoreConnector entirely: _ViewModel has 0 callbacks now, could use BlocBuilder instead
  2. Section-level testing: Factory makes it easy to test individual sections in isolation
  3. 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