Files
tm_app/REFACTORING_PLAN.md

7.6 KiB

🔧 DI & State Management Refactoring Plan

🚨 Critical Issues Found

1. ViewModel-to-ViewModel Dependencies

Current: ViewModels directly depend on other ViewModels Problem: Tight coupling, difficult testing, cascading rebuilds Solution: Use shared repositories or event bus

2. Eager Instantiation

Current: All 25 providers created at app startup Problem: Slow startup, memory waste Solution: Lazy loading with ProxyProvider

3. Error Handling Anti-Pattern

Current: _errorMessage = error; notifyListeners(); _errorMessage = null; notifyListeners(); Problem: Errors flash and disappear instantly Solution: Proper error state with timed clearing

4. Large ViewModels (338-525 lines)

Current: Monolithic ViewModels with mixed responsibilities Problem: Violates SRP, hard to maintain Solution: Split into smaller, focused controllers


📋 Refactoring Strategy

Phase 1: Remove ViewModel Dependencies (Week 1)

// ✅ BEFORE: TendersViewModel depends on HomeViewModel
class TendersViewModel {
  final HomeViewModel homeViewModel;  // ❌ BAD
  
  void updateFeedback() {
    homeViewModel.init();  // Coupled!
  }
}

// ✅ AFTER: Both share TendersRepository
class TendersViewModel {
  final TendersRepository _repository;  // ✅ GOOD
  
  Future<void> updateFeedback() async {
    await _repository.refreshTenders();
    // Repository notifies all listeners
  }
}

class HomeViewModel {
  final TendersRepository _repository;  // ✅ Same repository
  
  void init() {
    _repository.addListener(_onTendersChanged);
  }
}

Option B: Event Bus Pattern

// Create event bus
class AppEventBus {
  final _controller = StreamController<AppEvent>.broadcast();
  
  Stream<AppEvent> get events => _controller.stream;
  void fire(AppEvent event) => _controller.add(event);
}

// Events
abstract class AppEvent {}
class TenderUpdatedEvent extends AppEvent {
  final String tenderId;
}

// ViewModels listen to events
class HomeViewModel {
  void init() {
    eventBus.events
        .whereType<TenderUpdatedEvent>()
        .listen(_onTenderUpdated);
  }
}

Phase 2: Implement Lazy Loading (Week 1)

// ✅ AFTER: Lazy loading with ProxyProvider
List<SingleChildWidget> get viewModels {
  return [
    // Only AuthViewModel and ThemeProvider are eager
    ChangeNotifierProvider(
      create: (context) => AuthViewModel(
        authRepository: context.read(),
      ),
    ),
    
    // Others are lazy - created on first access
    ChangeNotifierProxyProvider<AuthViewModel, HomeViewModel>(
      create: (_) => HomeViewModel(homeRepository: _),
      update: (_, auth, previous) => previous ?? HomeViewModel(...),
      lazy: true,  // ✅ Not created until needed
    ),
  ];
}

// Remove init() from constructor
class HomeViewModel {
  HomeViewModel({required HomeRepository homeRepository})
    : _homeRepository = homeRepository;
    // ✅ Don't call init() here
}

Phase 3: Fix Error Handling (Week 1)

// ✅ Create proper error state
class ErrorState {
  final String message;
  final DateTime timestamp;
  final Duration displayDuration;
  
  bool get shouldDisplay =>
      DateTime.now().difference(timestamp) < displayDuration;
}

// In ViewModel
ErrorState? _errorState;
Timer? _errorTimer;

void _setError(String message) {
  _errorState = ErrorState(
    message: message,
    timestamp: DateTime.now(),
    displayDuration: Duration(seconds: 3),
  );
  
  _errorTimer?.cancel();
  _errorTimer = Timer(Duration(seconds: 3), () {
    _errorState = null;
    notifyListeners();
  });
  
  notifyListeners();
}

Phase 4: Introduce State Classes (Week 2)

// ✅ Create immutable state classes with Freezed
@freezed
class TendersState with _$TendersState {
  const factory TendersState({
    @Default(LoadingStatus.idle) LoadingStatus status,
    @Default([]) List<Tender> tenders,
    @Default([]) List<FeedbackData> feedbacks,
    @Default(PaginationState()) PaginationState pagination,
    ErrorState? error,
  }) = _TendersState;
}

enum LoadingStatus { idle, loading, loadingMore, success, failure }

@freezed
class PaginationState with _$PaginationState {
  const factory PaginationState({
    @Default(1) int currentPage,
    @Default(true) bool hasMore,
    @Default(10) int pageSize,
  }) = _PaginationState;
}

// ViewModel becomes much simpler
class TendersViewModel extends StateNotifier<TendersState> {
  TendersViewModel() : super(TendersState());
  
  Future<void> loadTenders() async {
    state = state.copyWith(status: LoadingStatus.loading);
    
    final result = await _repository.getTenders();
    
    state = result.when(
      ok: (data) => state.copyWith(
        status: LoadingStatus.success,
        tenders: data.tenders,
      ),
      error: (error) => state.copyWith(
        status: LoadingStatus.failure,
        error: ErrorState(message: error.toString()),
      ),
    );
  }
}

Phase 5: Split Large ViewModels (Week 2-3)

Before: 525 lines in TendersViewModel

class TendersViewModel {
  // Tenders logic (150 lines)
  // Feedback logic (150 lines)
  // Filtering logic (100 lines)
  // Sorting logic (75 lines)
  // Pagination logic (50 lines)
}

After: Multiple focused files

view_models/tenders/
  ├── tenders_view_model.dart          (50 lines - orchestrator)
  ├── controllers/
  │   ├── tenders_loader.dart          (60 lines)
  │   ├── feedback_controller.dart     (70 lines)
  │   └── pagination_controller.dart   (50 lines)
  ├── filters/
  │   ├── tenders_filter_state.dart    (30 lines)
  │   └── tenders_sort_state.dart      (30 lines)
  └── state/
      └── tenders_state.dart           (40 lines)

🎯 Better State Management Options

Pros:

  • Compile-time safety
  • No BuildContext needed
  • Built-in testing support
  • Auto-dispose
  • Family/AutoDispose modifiers
  • No ViewModel dependencies

Cons:

  • 🟡 Learning curve
  • 🟡 Migration effort

Option 2: Bloc/Cubit

Pros:

  • Clear state transitions
  • Built-in testing
  • Event-driven
  • Time-travel debugging

Cons:

  • 🟡 More boilerplate
  • 🟡 Steeper learning curve

Option 3: Stick with Provider + Refactor

Pros:

  • No migration needed
  • Team knows it

Cons:

  • 🔴 Still requires manual state management
  • 🔴 No compile-time safety
  • 🔴 Prone to same issues

📊 Migration Effort Estimate

Phase Effort Priority Files Affected
1. Remove VM deps 3 days 🔴 Critical 9 ViewModels, dependencies.dart
2. Lazy loading 1 day 🟡 High dependencies.dart, main.dart
3. Fix error handling 2 days 🟡 High 9 ViewModels
4. State classes 5 days 🟢 Medium 9 ViewModels + new state files
5. Split ViewModels 7 days 🟢 Medium 9 ViewModels → 30+ files

Total: ~18 days (3.5 weeks) for complete refactor


🔍 Quick Wins (Can Do Today)

1. Fix Error Handling (2 hours)

// Create lib/core/state/error_state.dart
// Update all ViewModels to use it

2. Remove init() from Constructors (1 hour)

// Move all init() calls to explicit method calls

3. Add State Classes (1 day)

// Start with AuthState, HomeState

📚 Resources