← 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

SmellLocationScaleProblem
God Widgetsection.dart592 lines30+ callback parameters, 60-branch match() statement
Boilerplate Extensionssection_union.dart extensions325 lines5 getters × 60 branches each, ~95% identical code
God Containerscreen_container.dart1,274 linesBLoC + Redux + Riverpod wiring, 20+ callbacks in _ViewModel
File ExplosionPer new section type7-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

MetricBeforeAfterChange
Section widget lines592131-461 lines (-78%)
Extension lines507~50-457 lines (-90%)
ScreenContainer lines1,274~855-419 lines (-33%)
Callbacks on Section30+0-100%
_ViewModel callbacks13 dispatch0-100%
Dispatch callbacks migrated0/1515/15Complete
Files per new section7-94-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

MetricBeforeAfter
Layers of callback passing30
Parameters on Section widget30+1
Callback creation code252 lines0 lines
Sections with unnecessary params57/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