diff --git a/.gitignore b/.gitignore index b6323af..40703f4 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,9 @@ app.*.map.json /android/app/release # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ + +# Firebase Configuration (sensitive files - should not be in version control) +**/android/app/google-services.json +**/ios/Runner/GoogleService-Info.plist +**/lib/firebase_options.dart \ No newline at end of file diff --git a/DI_STATE_ANALYSIS.md b/DI_STATE_ANALYSIS.md new file mode 100644 index 0000000..a80d5de --- /dev/null +++ b/DI_STATE_ANALYSIS.md @@ -0,0 +1,427 @@ +# πŸ“Š DI & State Management Analysis Summary + +## 🎯 Executive Summary + +**Overall Assessment:** 🟑 **NEEDS SIGNIFICANT REFACTORING** + +Your DI and state management have **good foundational architecture** but suffer from **critical anti-patterns** that will cause maintenance and scalability issues. + +### Key Metrics + +| Metric | Current | Target | Gap | +|--------|---------|--------|-----| +| Average ViewModel Size | 338 lines | 50-80 lines | πŸ”΄ 4x too large | +| ViewModel Dependencies | 9 circular | 0 | πŸ”΄ Critical | +| Providers at Startup | 25 eager | 5-8 eager | 🟑 3x too many | +| Error Handling Pattern | Anti-pattern | Proper state | πŸ”΄ Broken | +| State Classes | 0 | 9 | πŸ”΄ Missing | +| Test Coverage | 0% | 70%+ | πŸ”΄ Critical | + +--- + +## πŸ—οΈ Current Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ main.dart (App Entry) β”‚ +β”‚ MultiProvider with ALL 25 providers β”‚ +β”‚ ❌ Created immediately at startup β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” + β”‚ Cores β”‚ β”‚ Services β”‚ + β”‚ (2) β”‚ β”‚ (8) β”‚ + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” + β”‚ Repositoriesβ”‚ + β”‚ (8) β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” + β”‚ ViewModels β”‚ + β”‚ (9) β”‚ + β”‚ ❌ Depend β”‚ + β”‚ on each β”‚ + β”‚ other! β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” + β”‚ Views β”‚ + β”‚ (122) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ”΄ Problem #1: ViewModel Circular Dependencies + +``` + TendersViewModel + β”‚ + β”œβ”€β”€> HomeViewModel ───────┐ + β”‚ β”‚ +TenderDetailViewModel β”‚ + β”‚ β”‚ + β”œβ”€β”€> TendersViewModel β”‚ + β”œβ”€β”€> HomeViewModel β—„β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +LikedTendersViewModel + β”‚ + β”œβ”€β”€> TendersRepository + β”œβ”€β”€> HomeViewModel + +ProfileViewModel + β”‚ + β”œβ”€β”€> AuthViewModel + └──> AuthRepository + +NotificationViewModel + β”‚ + β”œβ”€β”€> NotificationsRepository + └──> HomeViewModel +``` + +### Impact: +- ❌ **Tight Coupling:** Can't change one without affecting others +- ❌ **Testing Nightmare:** Must mock all dependent ViewModels +- ❌ **Circular Dependencies:** Potential infinite loops +- ❌ **Cascading Rebuilds:** One VM change triggers others +- ❌ **Memory Leaks:** ViewModels keep references to each other + +--- + +## πŸ”΄ Problem #2: Eager Instantiation Performance + +### Current Startup Sequence: +```dart +1. App starts +2. MultiProvider creates 25 providers instantly: + β”œβ”€ 2 cores (NetworkManager, ThemeProvider) + β”œβ”€ 8 services (AuthService, HomeService, etc.) + β”œβ”€ 8 repositories + └─ 9 ViewModels +3. HomeViewModel constructor calls init() + β”œβ”€ getApprovedStates() β†’ API call + β”œβ”€ getYourTenders() β†’ API call + β”œβ”€ getFeedbackStats() β†’ API call + └─ checkUnreadNotifications() β†’ API call +4. User sees splash screen for 2-3 seconds +``` + +### Problems: +- 🐌 **Slow startup:** 4 API calls before UI appears +- πŸ’Ύ **Memory waste:** ProfileViewModel created but never used on home screen +- πŸ”‹ **Battery drain:** Unnecessary network calls +- πŸ“± **Poor UX:** Long loading time + +### Should Be: +```dart +1. App starts +2. Create only essentials: + β”œβ”€ NetworkManager + β”œβ”€ ThemeProvider + └─ AuthViewModel +3. Show splash screen (< 500ms) +4. Lazy create ViewModels on navigation: + β”œβ”€ Navigate to Home β†’ Create HomeViewModel β†’ Call init() + β”œβ”€ Navigate to Tenders β†’ Create TendersViewModel + └─ Navigate to Profile β†’ Create ProfileViewModel +``` + +--- + +## πŸ”΄ Problem #3: Error Handling Anti-Pattern + +### Current Code (Repeated in 9 ViewModels): +```dart +// Set error +_errorMessage = result.error.toString(); +notifyListeners(); // Frame 1: Error shows + +// Immediately clear error +_errorMessage = null; +notifyListeners(); // Frame 2: Error gone! +``` + +### Timeline: +``` +Frame 1 (16ms): User sees error message +Frame 2 (32ms): Error disappears +Result: Error flashes and vanishes! +``` + +### Should Be: +```dart +// Set error with auto-clear +_setError(result.error.toString(), duration: 3.seconds); + +// OR in UI with listener +if (viewModel.errorMessage != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + AppToast.error(context, viewModel.errorMessage!); + viewModel.clearError(); + }); +} +``` + +--- + +## πŸ”΄ Problem #4: No State Classes + +### Current (Scattered Primitives): +```dart +// TendersViewModel has 15+ state variables +bool _isLoading = false; +bool _isLoadingMore = false; +String? _errorMessage; +String? _feedbackErrorMessage; +TendersResponse? _tendersResponse; +FeedbackData? _feedbackData; +String? _searchQuery; +List feedbacks = []; +int currentPage = 1; +int _currentOffset = 0; +int _totalPages = 0; +bool _hasMoreData = true; +DateTimeRange? selectedDateRange; +String selectedStatus = 'All'; +String selectedSort = ''; +// ... more fields +``` + +### Problems: +- ❌ No single source of truth +- ❌ Can have inconsistent state (isLoading=false but _isLoadingMore=true) +- ❌ Hard to test +- ❌ Can't easily serialize/deserialize +- ❌ No immutability guarantees +- ❌ Can't use pattern matching + +### Should Be: +```dart +@freezed +class TendersState with _$TendersState { + const factory TendersState({ + @Default(LoadingStatus.idle) LoadingStatus status, + @Default([]) List tenders, + @Default([]) List feedbacks, + @Default(PaginationInfo()) PaginationInfo pagination, + @Default(FilterInfo()) FilterInfo filters, + ErrorInfo? error, + }) = _TendersState; + + // Derived properties + bool get isLoading => status == LoadingStatus.loading; + bool get hasError => error != null; + bool get canLoadMore => pagination.hasMore && !isLoading; +} + +// Single state variable +TendersState _state = TendersState(); +``` + +--- + +## πŸ”΄ Problem #5: Monolithic ViewModels + +### Size Distribution: +``` +YourTendersViewModel: 384 lines πŸ”΄ +AuthViewModel: 338 lines πŸ”΄ +HomeViewModel: 244 lines πŸ”΄ +TendersViewModel: 525 lines πŸ”΄ (WORST) +ProfileViewModel: 102 lines 🟑 +``` + +### TendersViewModel Responsibilities: +1. Tenders pagination (mobile) +2. Tenders pagination (desktop) +3. Feedback management +4. Filter state management +5. Sort state management +6. Date range selection +7. Search functionality +8. Optimistic updates +9. Error handling +10. Multiple API coordination + +**Violates Single Responsibility Principle!** + +### Should Be Split Into: +``` +tenders/ +β”œβ”€β”€ tenders_view_model.dart (50 lines - orchestrator) +β”œβ”€β”€ tenders_state.dart (40 lines - state class) +β”œβ”€β”€ tenders_loader.dart (60 lines - load tenders) +β”œβ”€β”€ feedback_manager.dart (70 lines - feedback CRUD) +β”œβ”€β”€ tenders_filter.dart (40 lines - filtering) +β”œβ”€β”€ tenders_pagination.dart (50 lines - pagination) +└── tenders_search.dart (30 lines - search) + +Total: 340 lines split across 7 files (avg 48 lines each) βœ… +``` + +--- + +## πŸ”΄ Problem #6: Complex Manual State Management + +### Optimistic Updates with Manual Rollback: +```dart +// 100+ lines of complex state manipulation +Future toggleFeedback() async { + // 1. Save current state + final existing = getFeedbackForTender(tenderId); + + // 2. Optimistic update + if (existing == null) { + feedbacks.add(optimisticFeedback); + } else { + if (existing.type == feedbackType) { + feedbacks.removeAt(index); + } else { + feedbacks[index] = optimisticFeedback; + } + } + notifyListeners(); + + // 3. API call + final result = await _repository.toggleFeedback(); + + // 4. Manual rollback on error + switch (result) { + case Error(): + // 20 lines of rollback logic + if (existing == null) { + feedbacks.removeWhere(...); + } else { + final currentIndex = feedbacks.indexWhere(...); + if (currentIndex != -1) { + feedbacks[currentIndex] = existing; + } else { + feedbacks.add(existing); + } + } + } +} +``` + +### Problems: +- ❌ **Error-prone:** Easy to make mistakes in rollback +- ❌ **Hard to test:** Many edge cases +- ❌ **Not reusable:** Can't apply to other operations +- ❌ **Race conditions:** What if user clicks twice? + +### Should Use: +- **Riverpod AsyncValue** (built-in optimistic updates) +- **Bloc Optimistic Pattern** +- **State machine** with rollback support + +--- + +## πŸ“Š Dependency Graph Analysis + +### Current (Problematic): +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ViewModel Layer (9) β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚Tenders VM│◄────►│ Home VM β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚TenderDetail VMβ”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ ❌ Circular dependencies everywhere β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Should Be (Clean): +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ViewModel Layer (9) β”‚ +β”‚ β”‚ +β”‚ Each ViewModel independent, shares data via: β”‚ +β”‚ β€’ Repositories (read/write) β”‚ +β”‚ β€’ Event Bus (notifications) β”‚ +β”‚ β€’ Navigation params (one-time data) β”‚ +β”‚ β”‚ +β”‚ βœ… No direct VM-to-VM dependencies β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 🎯 Impact Analysis + +### Current Problems Lead To: + +#### Development Issues: +- 🐌 **Slow feature development** (tight coupling) +- πŸ› **Hard to debug** (state scattered everywhere) +- πŸ§ͺ **Can't write tests** (VM dependencies) +- πŸ”„ **Refactoring is risky** (changes break other features) + +#### Performance Issues: +- πŸ“± **Slow app startup** (eager instantiation) +- πŸ”‹ **Battery drain** (unnecessary API calls) +- πŸ’Ύ **Memory waste** (unused ViewModels loaded) +- 🎨 **Unnecessary rebuilds** (cascading VM updates) + +#### User Experience Issues: +- ⏳ **Long loading times** +- ⚠️ **Errors disappear instantly** (can't read them) +- 🐞 **Potential bugs** (complex state management) +- πŸ’₯ **Crashes** (race conditions in optimistic updates) + +--- + +## βœ… Recommended Solution + +### Short-term (1-2 weeks): +1. **Remove ViewModel dependencies** β†’ Use shared repositories +2. **Implement lazy loading** β†’ ProxyProvider +3. **Fix error handling** β†’ Proper error state +4. **Add state classes** β†’ Freezed models + +### Long-term (3-4 weeks): +1. **Migrate to Riverpod** β†’ Better state management +2. **Split large ViewModels** β†’ Follow 10-15 line rule +3. **Add comprehensive tests** β†’ 70%+ coverage +4. **Implement event bus** β†’ Decouple features + +--- + +## πŸ“ˆ Success Metrics + +After refactoring, you should see: + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| App startup time | 2-3s | < 1s | 🟒 3x faster | +| Avg ViewModel size | 338 lines | 60 lines | 🟒 5x smaller | +| Test coverage | 0% | 70%+ | 🟒 Testable | +| Build time | Unknown | Tracked | 🟒 Measurable | +| Memory usage | Unknown | Optimized | 🟒 Lower | +| Developer velocity | Slow | Fast | 🟒 Faster | + +--- + +## πŸš€ Next Steps + +1. **Review** this analysis with your team +2. **Prioritize** which issues to tackle first +3. **Create tickets** for each refactoring task +4. **Start with** ViewModel dependencies (highest impact) +5. **Add tests** as you refactor each ViewModel +6. **Measure** improvements with analytics + + diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md new file mode 100644 index 0000000..8190f92 --- /dev/null +++ b/REFACTORING_PLAN.md @@ -0,0 +1,316 @@ +# πŸ”§ 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) + +#### Option A: Shared Repository Pattern (Recommended) +```dart +// βœ… 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 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 +```dart +// Create event bus +class AppEventBus { + final _controller = StreamController.broadcast(); + + Stream 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() + .listen(_onTenderUpdated); + } +} +``` + +--- + +### Phase 2: Implement Lazy Loading (Week 1) + +```dart +// βœ… AFTER: Lazy loading with ProxyProvider +List get viewModels { + return [ + // Only AuthViewModel and ThemeProvider are eager + ChangeNotifierProvider( + create: (context) => AuthViewModel( + authRepository: context.read(), + ), + ), + + // Others are lazy - created on first access + ChangeNotifierProxyProvider( + 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) + +```dart +// βœ… 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) + +```dart +// βœ… Create immutable state classes with Freezed +@freezed +class TendersState with _$TendersState { + const factory TendersState({ + @Default(LoadingStatus.idle) LoadingStatus status, + @Default([]) List tenders, + @Default([]) List 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 { + TendersViewModel() : super(TendersState()); + + Future 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 +```dart +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 + +### Option 1: Riverpod (Recommended) +**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) +```dart +// Create lib/core/state/error_state.dart +// Update all ViewModels to use it +``` + +### 2. Remove init() from Constructors (1 hour) +```dart +// Move all init() calls to explicit method calls +``` + +### 3. Add State Classes (1 day) +```dart +// Start with AuthState, HomeState +``` + +--- + +## πŸ“š Resources + +- [Riverpod Migration Guide](https://riverpod.dev/docs/from_provider) +- [Flutter State Management Comparison](https://docs.flutter.dev/data-and-backend/state-mgmt/options) +- [Provider Best Practices](https://pub.dev/packages/provider#best-practices) + + diff --git a/VIEWMODEL_COMMUNICATION_PATTERNS.md b/VIEWMODEL_COMMUNICATION_PATTERNS.md new file mode 100644 index 0000000..a79f0ae --- /dev/null +++ b/VIEWMODEL_COMMUNICATION_PATTERNS.md @@ -0,0 +1,288 @@ +# ViewModel Communication Patterns + +## ❌ The Problem: Direct ViewModel Dependencies + +```dart +class NotificationViewModel { + final HomeViewModel _homeViewModel; // ❌ BAD: Direct dependency + + void markAsRead() { + _homeViewModel.setUnreadNotificationFalse(); // ❌ Tight coupling + } +} +``` + +**Why is this bad?** +- Tight coupling between ViewModels +- Difficult to test in isolation +- Circular dependency risk +- Violates Single Responsibility Principle +- Hard to maintain and scale + +--- + +## βœ… Solution 1: Shared Service Pattern (BEST for your case) + +Create a dedicated service that manages shared state. + +### Implementation: + +**Service:** +```dart +// lib/data/services/notification_state_service.dart +class NotificationStateService extends ChangeNotifier { + bool _hasUnreadNotification = false; + + bool get hasUnreadNotification => _hasUnreadNotification; + + void setHasUnreadNotification(bool value) { + if (_hasUnreadNotification != value) { + _hasUnreadNotification = value; + notifyListeners(); + } + } +} +``` + +**ViewModels depend on the service:** +```dart +class HomeViewModel { + final NotificationStateService _notificationStateService; + + void checkUnreadNotifications() async { + // ... fetch from API + _notificationStateService.setHasUnreadNotification(hasUnread); + } +} + +class NotificationViewModel { + final NotificationStateService _notificationStateService; + + void markAsRead() { + // ... mark as read + _notificationStateService.setHasUnreadNotification(false); + } +} +``` + +**UI consumes the service directly:** +```dart +class NotificationBadge extends StatelessWidget { + @override + Widget build(BuildContext context) { + final hasUnread = context.watch() + .hasUnreadNotification; + + return Badge(showBadge: hasUnread); + } +} +``` + +**Benefits:** +- βœ… No ViewModel-to-ViewModel dependency +- βœ… Single source of truth +- βœ… Easy to test +- βœ… Follows SOLID principles +- βœ… Scalable + +--- + +## βœ… Solution 2: Repository Pattern + +Move the shared logic to a repository that both ViewModels depend on. + +```dart +// Repository manages the domain logic +class NotificationRepository { + bool _hasUnread = false; + + Future checkUnreadNotifications() async { + final result = await api.checkUnread(); + _hasUnread = result.hasUnread; + return _hasUnread; + } + + Future markAllAsRead() async { + await api.markAllAsRead(); + _hasUnread = false; + } +} + +// ViewModels only depend on repository +class HomeViewModel { + final NotificationRepository _repo; + + void init() async { + final hasUnread = await _repo.checkUnreadNotifications(); + notifyListeners(); + } +} +``` + +--- + +## βœ… Solution 3: Event Bus / Stream Pattern + +ViewModels communicate through events without knowing about each other. + +```dart +// Event bus service +class AppEventBus { + final _controller = StreamController.broadcast(); + + Stream get events => _controller.stream; + void emit(AppEvent event) => _controller.add(event); +} + +// Events +abstract class AppEvent {} +class NotificationReadEvent extends AppEvent {} + +// ViewModel 1: Emits events +class NotificationViewModel { + final AppEventBus _eventBus; + + void markAsRead() { + // ... mark as read + _eventBus.emit(NotificationReadEvent()); + } +} + +// ViewModel 2: Listens to events +class HomeViewModel { + final AppEventBus _eventBus; + + HomeViewModel() { + _eventBus.events.listen((event) { + if (event is NotificationReadEvent) { + _handleNotificationRead(); + } + }); + } +} +``` + +--- + +## βœ… Solution 4: UI Coordination + +Let the UI layer coordinate between ViewModels. + +```dart +class NotificationScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final notificationVM = context.watch(); + final homeVM = context.watch(); + + return Button( + onPressed: () { + // UI coordinates the interaction + notificationVM.markAsRead(); + homeVM.refreshNotificationBadge(); + }, + ); + } +} +``` + +**When to use:** +- Simple interactions +- UI-specific coordination +- Few ViewModels involved + +--- + +## βœ… Solution 5: Riverpod Pattern (Advanced) + +Use Riverpod's provider dependencies for better state management. + +```dart +// Shared state provider +final notificationStateProvider = StateNotifierProvider( + (ref) => NotificationState(), +); + +class NotificationState extends StateNotifier { + NotificationState() : super(false); + + void setHasUnread(bool value) => state = value; +} + +// ViewModels depend on the provider +final homeViewModelProvider = Provider((ref) { + final notificationState = ref.read(notificationStateProvider.notifier); + return HomeViewModel(notificationState); +}); +``` + +--- + +## πŸ“Š Comparison Table + +| Pattern | Complexity | Testability | Scalability | Best For | +|---------|-----------|-------------|-------------|----------| +| Shared Service | Low | High | High | Shared state across app | +| Repository | Medium | High | High | Domain logic sharing | +| Event Bus | Medium | Medium | High | Loosely coupled events | +| UI Coordination | Low | Medium | Low | Simple interactions | +| Riverpod | Medium | High | Very High | New projects | + +--- + +## 🎯 Recommendation for Your Project + +Use **Solution 1: Shared Service Pattern** because: + +1. βœ… Your codebase already uses Provider +2. βœ… The notification state is truly shared across the app +3. βœ… Simple to implement and understand +4. βœ… Easy to test +5. βœ… Follows your modular architecture principle +6. βœ… No major refactoring required + +--- + +## πŸ“ Implementation Checklist + +- [x] Create `NotificationStateService` +- [x] Add service to dependency injection +- [x] Update `HomeViewModel` to use service +- [x] Update `NotificationViewModel` to use service +- [x] Remove direct ViewModel dependencies +- [x] Test the implementation +- [ ] Remove the example files when done + +--- + +## πŸ§ͺ Testing Example + +```dart +test('NotificationViewModel updates shared state', () { + final stateService = NotificationStateService(); + final repository = MockNotificationRepository(); + final viewModel = NotificationViewModel( + notificationsRepository: repository, + notificationStateService: stateService, + ); + + // Act + viewModel.markAllAsRead(); + + // Assert + expect(stateService.hasUnreadNotification, false); +}); +``` + +--- + +## πŸš€ Key Takeaway + +**ViewModels should NEVER depend on other ViewModels.** + +Instead: +- Use a **shared service** for shared state +- Use **repositories** for domain logic +- Use **event buses** for loose coupling +- Let the **UI coordinate** simple interactions + diff --git a/lib/core/config/dependencies.dart b/lib/core/config/dependencies.dart index 1b1a4d8..861d149 100644 --- a/lib/core/config/dependencies.dart +++ b/lib/core/config/dependencies.dart @@ -10,7 +10,9 @@ import 'package:tm_app/data/repositories/tenders_repository.dart'; import 'package:tm_app/data/repositories/your_tenders_repository.dart'; import 'package:tm_app/data/services/home_service.dart'; import 'package:tm_app/data/services/liked_tenders_service.dart'; +import 'package:tm_app/data/services/notification_check_service.dart'; import 'package:tm_app/data/services/notification_service.dart'; +import 'package:tm_app/data/services/notification_state_service.dart'; import 'package:tm_app/data/services/tender_detail_service.dart'; import 'package:tm_app/data/services/your_tenders_service.dart'; import 'package:tm_app/view_models/final_completion_of_documents_view_model.dart'; @@ -30,51 +32,70 @@ import '../../view_models/home_view_model.dart'; import '../../view_models/profile_view_model.dart'; List get providersRemote { - return [...cores, ...apiClients, ...repositories, ...viewModels, ...others]; + return [...cores, ...apiClients, ...repositories, ...services, ...viewModels]; } List get cores { return [ - Provider(create: (context) => NetworkManager(context.read())), - ChangeNotifierProvider(create: (context) => ThemeProvider()), + Provider(create: (context) => NetworkManager(context.read()), lazy: true), + ChangeNotifierProvider(create: (context) => ThemeProvider(), lazy: true), + ChangeNotifierProvider( + create: (context) => NotificationStateService(), + lazy: true, + ), ]; } List get apiClients { return [ - Provider(create: (context) => AuthService(networkManager: context.read())), - Provider(create: (context) => HomeService(networkManager: context.read())), + Provider( + create: (context) => AuthService(networkManager: context.read()), + lazy: true, + ), + Provider( + create: (context) => HomeService(networkManager: context.read()), + lazy: true, + ), Provider( create: (context) => TenderDetailService(networkManager: context.read()), + lazy: true, ), - Provider( create: (context) => YourTendersService(networkManager: context.read()), + lazy: true, ), Provider( create: (context) => LikedTendersService(networkManager: context.read()), + lazy: true, ), Provider( create: (context) => TendersService(networkManager: context.read()), + lazy: true, ), Provider( create: (context) => ProfileService(networkManager: context.read()), + lazy: true, ), Provider( create: (context) => NotificationsService(networkManager: context.read()), + lazy: true, ), ]; } List get repositories { return [ - Provider(create: (context) => AuthRepository(authService: context.read())), + Provider( + create: (context) => AuthRepository(authService: context.read()), + lazy: true, + ), Provider( create: (context) => YourTendersRepository( yourTendersService: context.read(), likedTendersService: context.read(), ), + lazy: true, ), Provider( create: @@ -82,28 +103,46 @@ List get repositories { homeService: context.read(), yourTendersService: context.read(), ), + lazy: true, ), Provider( create: (context) => TenderDetailRepository(tenderDetailService: context.read()), + lazy: true, ), - Provider( create: (context) => LikedTendersRepository(likedTendersService: context.read()), + lazy: true, ), Provider( create: (context) => TendersRepository(tendersService: context.read()), + lazy: true, ), Provider( create: (context) => ProfileRepository(profileService: context.read()), + lazy: true, ), Provider( create: (context) => NotificationsRepository(notificationsService: context.read()), + lazy: true, + ), + ]; +} + +List get services { + return [ + Provider( + create: + (context) => NotificationCheckService( + homeRepository: context.read(), + notificationStateService: context.read(), + ), + lazy: true, ), ]; } @@ -112,61 +151,62 @@ List get viewModels { return [ ChangeNotifierProvider( create: (context) => AuthViewModel(authRepository: context.read()), - ), - ChangeNotifierProvider( - create: (context) => HomeViewModel(homeRepository: context.read()), + lazy: true, ), ChangeNotifierProvider( create: - (context) => TendersViewModel( - tendersRepository: context.read(), - homeViewModel: context.read(), + (context) => HomeViewModel( + homeRepository: context.read(), + notificationStateService: context.read(), ), + lazy: true, + ), + ChangeNotifierProvider( + create: (context) => TendersViewModel(tendersRepository: context.read()), + lazy: true, ), ChangeNotifierProvider( create: (context) => TenderDetailViewModel( tenderDetailRepository: context.read(), tendersRepository: context.read(), - tendersViewModel: context.read(), - homeViewModel: context.read(), ), + lazy: true, ), - ChangeNotifierProvider( create: (context) => YourTendersViewModel(yourTendersRepository: context.read()), + lazy: true, ), ChangeNotifierProvider( create: (context) => LikedTendersViewModel( likedTendersRepository: context.read(), tendersRepository: context.read(), - homeViewModel: context.read(), ), + lazy: true, ), ChangeNotifierProvider( create: (context) => ProfileViewModel( profileRepository: context.read(), authRepository: context.read(), - authViewModel: context.read(), ), + lazy: true, ), ChangeNotifierProvider( create: (context) => FinalCompletionOfDocumentsViewModel(), + lazy: true, ), ChangeNotifierProvider( create: (context) => NotificationViewModel( - repositoryViewModel: context.read(), - homeViewModel: context.read(), + notificationsRepository: context.read(), + notificationStateService: context.read(), + notificationCheckService: context.read(), ), + lazy: true, ), ]; } - -List get others { - return []; -} diff --git a/lib/core/routes/app_shell/app_shell_screen.dart b/lib/core/routes/app_shell/app_shell_screen.dart index fbd8d74..cfa667e 100644 --- a/lib/core/routes/app_shell/app_shell_screen.dart +++ b/lib/core/routes/app_shell/app_shell_screen.dart @@ -4,26 +4,41 @@ import 'package:provider/provider.dart'; import 'package:tm_app/core/routes/app_shell/desktop_shell_page.dart'; import 'package:tm_app/core/routes/app_shell/mobile_shell_page.dart'; import 'package:tm_app/core/routes/app_shell/tablet_shell_page.dart'; +import 'package:tm_app/data/services/notification_check_service.dart'; import 'package:tm_app/view_models/home_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/views/shared/responsive_builder.dart'; import '../../../view_models/notification_view_model.dart'; -class AppShellScreen extends StatelessWidget { +class AppShellScreen extends StatefulWidget { const AppShellScreen({required this.navigationShell, super.key}); final StatefulNavigationShell navigationShell; + @override + State createState() => _AppShellScreenState(); +} + +class _AppShellScreenState extends State { + @override + void initState() { + super.initState(); + // Check for unread notifications when app shell loads + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().checkUnreadNotifications(); + }); + } + void _gotoBranch(int index, BuildContext context) { - if (index == navigationShell.currentIndex) { + if (index == widget.navigationShell.currentIndex) { return; } // Navigate first - navigationShell.goBranch( + widget.navigationShell.goBranch( index, - initialLocation: index == navigationShell.currentIndex, + initialLocation: index == widget.navigationShell.currentIndex, ); // Then trigger data loading after navigation is complete @@ -45,16 +60,16 @@ class AppShellScreen extends StatelessWidget { return ResponsiveBuilder( mobile: SafeArea( child: MobileShellPage( - navigationShell: navigationShell, + navigationShell: widget.navigationShell, onTap: (value) => _gotoBranch(value, context), ), ), tablet: TabletShellPage( - navigationShell: navigationShell, + navigationShell: widget.navigationShell, onTap: (value) => _gotoBranch(value, context), ), desktop: DesktopShellPage( - navigationShell: navigationShell, + navigationShell: widget.navigationShell, onTap: (value) => _gotoBranch(value, context), ), ); diff --git a/lib/core/routes/app_shell/mobile_shell_page.dart b/lib/core/routes/app_shell/mobile_shell_page.dart index f6e74ab..18ad802 100644 --- a/lib/core/routes/app_shell/mobile_shell_page.dart +++ b/lib/core/routes/app_shell/mobile_shell_page.dart @@ -5,9 +5,9 @@ import 'package:provider/provider.dart'; import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; -import 'package:tm_app/view_models/home_view_model.dart'; import 'package:tm_app/views/profile/strings/profile_strings.dart'; +import '../../../data/services/notification_state_service.dart'; import '../../../views/home/strings/home_strings.dart'; import '../../../views/tenders/strings/tenders_strings.dart'; @@ -65,7 +65,10 @@ class MobileShellPage extends StatelessWidget { onTap: () => onTap(3), iconPath: AssetsManager.notify, activeIconPath: AssetsManager.notifyActive, - showBadge: context.watch().hasUnreadNotification, + showBadge: + context + .watch() + .hasUnreadNotification, ), _bottomNavigationItem( context: context, diff --git a/lib/data/services/notification_check_service.dart b/lib/data/services/notification_check_service.dart new file mode 100644 index 0000000..03b7fba --- /dev/null +++ b/lib/data/services/notification_check_service.dart @@ -0,0 +1,32 @@ +import 'package:tm_app/core/utils/result.dart'; +import 'package:tm_app/data/repositories/home_repository.dart'; +import 'package:tm_app/data/services/notification_state_service.dart'; + +/// Service to check for unread notifications and update state +class NotificationCheckService { + final HomeRepository _homeRepository; + final NotificationStateService _notificationStateService; + + NotificationCheckService({ + required HomeRepository homeRepository, + required NotificationStateService notificationStateService, + }) : _homeRepository = homeRepository, + _notificationStateService = notificationStateService; + + Future checkUnreadNotifications() async { + final result = await _homeRepository.checkUnreadNotifications(); + + switch (result) { + case Ok>(): + final response = result.value; + final data = response['data']; + _notificationStateService.setHasUnreadNotification( + data is List && data.isNotEmpty, + ); + break; + case Error>(): + _notificationStateService.setHasUnreadNotification(false); + break; + } + } +} diff --git a/lib/data/services/notification_state_service.dart b/lib/data/services/notification_state_service.dart new file mode 100644 index 0000000..4001293 --- /dev/null +++ b/lib/data/services/notification_state_service.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; + +/// Service to manage notification state across the app +class NotificationStateService extends ChangeNotifier { + bool _hasUnreadNotification = false; + + bool get hasUnreadNotification => _hasUnreadNotification; + + void setHasUnreadNotification(bool value) { + if (_hasUnreadNotification != value) { + _hasUnreadNotification = value; + notifyListeners(); + } + } + + void clearUnreadNotification() => setHasUnreadNotification(false); +} diff --git a/lib/main.dart b/lib/main.dart index c33f307..494e43d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,45 +66,9 @@ class _MyAppState extends State { // if (kIsWeb) { FirebaseMessaging.onMessage.listen((event) { final overlayCtx = rootNavigatorKey.currentContext; - AppLogger().error('Notification received: ${event.notification?.web}'); - AppLogger().error('web link: ${event.notification?.title}'); - AppLogger().error('web link: ${event.notification?.web?.link}'); - print('Notification received1: ${event.notification?.web.toString()}'); - print( - 'Notification received1: ${event.notification?.web?.analyticsLabel}', - ); - print('web link: ${event.notification?.web?.link}'); - print('Notification received: ${event.notification?.title}'); - AppLogger().error('notif data: ${event.data}'); - AppLogger().error('notif from: ${event.from}'); - AppLogger().error('notif type: ${event.messageType}'); - AppLogger().error('notif keys: ${event.data.keys}'); - AppLogger().info('notif title: ${event.notification?.title}'); - AppLogger().info('notif body: ${event.notification?.body}'); - AppLogger().info('notif data: ${event.data}'); - AppLogger().info('notif from: ${event.from}'); - AppLogger().info('notif type: ${event.messageType}'); - AppLogger().info('notif keys: ${event.data.keys}'); if (overlayCtx != null) { if (overlayCtx.mounted) { - AppLogger().error( - 'Notification received: ${event.notification?.web}', - ); - AppLogger().error( - ' link android: ${event.notification?.android?.link}', - ); - AppLogger().error( - 'android link image: ${event.notification?.android?.imageUrl}', - ); - AppLogger().error( - 'android link click action: ${event.notification?.android?.clickAction}', - ); - AppLogger().error( - 'web link image: ${event.notification?.web?.image}', - ); - print('Notification received: ${event.notification?.web}'); - print('web link: ${event.notification?.web?.link}'); AppToast.notification( overlayCtx, event.notification?.title ?? 'New Notification', @@ -119,32 +83,6 @@ class _MyAppState extends State { } void _handleMessage(RemoteMessage message) { - SharedPreferences.getInstance().then((value) { - value.setString('notif_data', message.data.toString()); - value.setString('notif_from', message.from.toString()); - value.setString('notif_type', message.messageType.toString()); - value.setString('notif_keys', message.data.keys.toString()); - value.setString('notif_title', message.notification?.title ?? ''); - value.setString('notif_body', message.notification?.body ?? ''); - value.setString('notif_data', message.contentAvailable.toString()); - value.setString('notif_web_link', message.notification?.web?.link ?? ''); - value.setString( - 'notif_android_link', - message.notification?.android?.link ?? '', - ); - value.setString( - 'notif_web_image', - message.notification?.web?.image ?? '', - ); - value.setString( - 'notif_android_image', - message.notification?.android?.imageUrl ?? '', - ); - value.setString( - 'notif_web_click_action', - message.notification?.web?.link ?? '', - ); - }); appRouter.go( const SplashScreenRoute(navigateToNotification: true).location, ); diff --git a/lib/view_models/auth_view_model.dart b/lib/view_models/auth_view_model.dart index e3bfecd..d194a40 100644 --- a/lib/view_models/auth_view_model.dart +++ b/lib/view_models/auth_view_model.dart @@ -3,11 +3,13 @@ import 'package:tm_app/data/services/model/customer/customer.dart'; import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart'; import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart'; +import '../core/utils/error_utils.dart'; +import '../core/utils/logger.dart'; import '../core/utils/result.dart'; import '../data/repositories/auth_repository.dart'; import '../data/services/model/login_response/login_response_model.dart'; -import '../data/services/model/logout_response/logout_response.dart'; import '../data/services/model/reset_password_response/reset_password_response.dart'; +import '../views/forget_password_create/strings/forgot_password_create_strings.dart'; class AuthViewModel with ChangeNotifier { final AuthRepository _authRepository; @@ -139,9 +141,6 @@ class AuthViewModel with ChangeNotifier { } _isLoading = false; - notifyListeners(); - - // reset error after notify _errorMessage = null; notifyListeners(); } @@ -151,33 +150,13 @@ class AuthViewModel with ChangeNotifier { await _authRepository.localLogout(); } catch (e) { // Handle local logout exceptions gracefully + AppLogger().error('❌ Failed to local logout: $e'); } _loggedInUser = null; notifyListeners(); } - Future logout() async { - _errorMessage = null; - notifyListeners(); - - final result = await _authRepository.logout(); - - switch (result) { - case Ok(): - await localLogout(); - break; - case Error(): - _errorMessage = result.error.toString(); - break; - } - - notifyListeners(); - - _errorMessage = null; - notifyListeners(); - } - - Future forgotPassword(BuildContext context) async { + Future forgotPassword() async { _isLoadingForgot = true; _errorMessageForget = null; successMessage = null; @@ -214,7 +193,7 @@ class AuthViewModel with ChangeNotifier { notifyListeners(); } - Future verifyOtp(BuildContext context) async { + Future verifyOtp() async { _isLoadingOtp = true; _errorMessageOtp = null; _verifyOtpSuccess = false; @@ -273,16 +252,16 @@ class AuthViewModel with ChangeNotifier { _resetPasswordSuccess = true; break; case Error(): - _resetPasswordErrorMessage = result.error.toString(); + _resetPasswordErrorMessage = ErrorUtils.getErrorMessage(result.error); _resetPasswordSuccess = false; break; } _isResetPasswordLoading = false; - notifyListeners(); _resetPasswordErrorMessage = null; notifyListeners(); } else { - _resetPasswordErrorMessage = 'Reset token is required'; + _resetPasswordErrorMessage = + ForgotPasswordCreateStrings.passwordResetFailed; _isResetPasswordLoading = false; notifyListeners(); } @@ -325,7 +304,6 @@ class AuthViewModel with ChangeNotifier { emailController.dispose(); newPasswordController.dispose(); otpController.dispose(); - usernameFocus.dispose(); passwordFocus.dispose(); emailFocus.dispose(); diff --git a/lib/view_models/home_view_model.dart b/lib/view_models/home_view_model.dart index 4869842..d6940bf 100644 --- a/lib/view_models/home_view_model.dart +++ b/lib/view_models/home_view_model.dart @@ -10,15 +10,31 @@ import '../data/repositories/home_repository.dart'; import '../data/services/model/feedback_stats_response/feedback_stat_response.dart'; import '../data/services/model/home/home_response/home_response_model.dart'; import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; +import '../data/services/notification_state_service.dart'; class HomeViewModel with ChangeNotifier { final HomeRepository _homeRepository; + final NotificationStateService _notificationStateService; - HomeViewModel({required HomeRepository homeRepository}) - : _homeRepository = homeRepository { + HomeViewModel({ + required HomeRepository homeRepository, + required NotificationStateService notificationStateService, + }) : _homeRepository = homeRepository, + _notificationStateService = notificationStateService { + _notificationStateService.addListener(_onNotificationStateChanged); init(); } + void _onNotificationStateChanged() { + notifyListeners(); + } + + @override + void dispose() { + _notificationStateService.removeListener(_onNotificationStateChanged); + super.dispose(); + } + bool _isLoading = false; String? _errorMessage; HomeResponseModel? _homeResponse; @@ -44,8 +60,8 @@ class HomeViewModel with ChangeNotifier { bool get hasMore => _hasMore; bool get isRecommendedMode => _isRecommendedMode; - bool _hasUnreadNotification = false; - bool get hasUnreadNotification => _hasUnreadNotification; + bool get hasUnreadNotification => + _notificationStateService.hasUnreadNotification; void init() async { _isLoading = true; @@ -54,7 +70,6 @@ class HomeViewModel with ChangeNotifier { await getApprovedStates(); await getYourTenders(reset: true); await getFeedbackStats(); - await checkUnreadNotifications(); if (_errorMessage != null) { _isLoading = false; @@ -212,32 +227,4 @@ class HomeViewModel with ChangeNotifier { double get selfApplyPercent { return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0; } - - Future checkUnreadNotifications() async { - final result = await _homeRepository.checkUnreadNotifications(); - - switch (result) { - case Ok>(): - final response = result.value; - - final data = response['data']; - if (data is List && data.isNotEmpty) { - _hasUnreadNotification = true; - } else { - _hasUnreadNotification = false; - } - break; - - case Error>(): - _hasUnreadNotification = false; - break; - } - - notifyListeners(); - } - - void setUnreadNotificationFalse() { - _hasUnreadNotification = false; - notifyListeners(); - } } diff --git a/lib/view_models/liked_tenders_view_model.dart b/lib/view_models/liked_tenders_view_model.dart index 9388ded..839aca4 100644 --- a/lib/view_models/liked_tenders_view_model.dart +++ b/lib/view_models/liked_tenders_view_model.dart @@ -4,7 +4,6 @@ import 'package:tm_app/data/repositories/liked_tenders_repository.dart'; import 'package:tm_app/data/repositories/tenders_repository.dart'; import 'package:tm_app/data/services/model/liked_tender_feedback/liked_tender_feedback.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; -import 'package:tm_app/view_models/home_view_model.dart'; import '../data/services/model/feedback_response/feedback_response.dart'; import '../data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart'; @@ -12,13 +11,12 @@ import '../data/services/model/request_models/tender_feedback_request_model/tend class LikedTendersViewModel with ChangeNotifier { final LikedTendersRepository _likedTendersRepository; final TendersRepository _tendersRepository; - final HomeViewModel _homeViewModel; + LikedTendersViewModel({ required LikedTendersRepository likedTendersRepository, required TendersRepository tendersRepository, - required HomeViewModel homeViewModel, }) : _likedTendersRepository = likedTendersRepository, - _homeViewModel = homeViewModel, + _tendersRepository = tendersRepository; bool isLoading = false; @@ -44,10 +42,6 @@ class LikedTendersViewModel with ChangeNotifier { bool get isSubmitApprovalLoading => _isSubmitApprovalLoading; bool get isRejectApprovalLoading => _isRejectApprovalLoading; - void updateHome() { - _homeViewModel.init(); - } - void clearApprovalErrorMessage() { _errorMessage = null; notifyListeners(); diff --git a/lib/view_models/notification_view_model.dart b/lib/view_models/notification_view_model.dart index 448fa2a..2aac757 100644 --- a/lib/view_models/notification_view_model.dart +++ b/lib/view_models/notification_view_model.dart @@ -4,18 +4,22 @@ import 'package:tm_app/core/utils/app_toast.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/repositories/notification_repository.dart'; import 'package:tm_app/data/services/model/notification__response/notification_response_model.dart'; -import 'package:tm_app/view_models/home_view_model.dart'; +import 'package:tm_app/data/services/notification_check_service.dart'; +import 'package:tm_app/data/services/notification_state_service.dart'; import 'package:tm_app/views/notification/strings/notification_strings.dart'; class NotificationViewModel with ChangeNotifier { final NotificationsRepository _repository; - final HomeViewModel _homeViewModel; + final NotificationStateService _notificationStateService; + final NotificationCheckService _notificationCheckService; NotificationViewModel({ - required NotificationsRepository repositoryViewModel, - required HomeViewModel homeViewModel, - }) : _repository = repositoryViewModel, - _homeViewModel = homeViewModel { + required NotificationsRepository notificationsRepository, + required NotificationStateService notificationStateService, + required NotificationCheckService notificationCheckService, + }) : _repository = notificationsRepository, + _notificationStateService = notificationStateService, + _notificationCheckService = notificationCheckService { WidgetsBinding.instance.addPostFrameCallback((_) { init(); }); @@ -347,7 +351,7 @@ class NotificationViewModel with ChangeNotifier { } void setNotificationFalse(BuildContext context) { - _homeViewModel.setUnreadNotificationFalse(); + _notificationStateService.clearUnreadNotification(); } void _checkAndUpdateUnreadState() { @@ -379,8 +383,9 @@ class NotificationViewModel with ChangeNotifier { return importantCount > 0; } - void checkNotifications() { - _homeViewModel.checkUnreadNotifications(); + Future checkNotifications() async { + // Re-check unread notifications from server + await _notificationCheckService.checkUnreadNotifications(); } Future markAsRead({required String notificationId}) async { diff --git a/lib/view_models/profile_view_model.dart b/lib/view_models/profile_view_model.dart index 5f10ed3..eb99915 100644 --- a/lib/view_models/profile_view_model.dart +++ b/lib/view_models/profile_view_model.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/data/repositories/auth_repository.dart'; import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart'; import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart'; @@ -9,20 +8,16 @@ import '../core/utils/result.dart'; import '../data/repositories/profile_repository.dart'; import '../data/services/model/logout_response/logout_response.dart'; import '../data/services/model/profile_response/profile_response.dart'; -import 'auth_view_model.dart'; class ProfileViewModel with ChangeNotifier { final ProfileRepository _profileRepository; final AuthRepository _authRepository; - final AuthViewModel _authViewModel; ProfileViewModel({ required ProfileRepository profileRepository, required AuthRepository authRepository, - required AuthViewModel authViewModel, }) : _profileRepository = profileRepository, - _authRepository = authRepository, - _authViewModel = authViewModel; + _authRepository = authRepository; bool _isLoading = false; String? _errorMessage; @@ -30,65 +25,12 @@ class ProfileViewModel with ChangeNotifier { CompanyProfileData? _companyProfileData; bool _loggedOut = false; - // Notification info saved from FCM message in SharedPreferences - String? _notifData; - String? _notifFrom; - String? _notifType; - String? _notifKeys; - String? _notifTitle; - String? _notifBody; - String? _notifWebLink; - String? _notifAndroidLink; - String? _notifWebImage; - String? _notifAndroidImage; - String? _notifWebClickAction; - bool get isLoading => _isLoading; bool get loggedOut => _loggedOut; String? get errorMessage => _errorMessage; ProfileData? get profileData => _profileData; CompanyProfileData? get companyProfileData => _companyProfileData; - // Expose notification info - String? get notifData => _notifData; - String? get notifFrom => _notifFrom; - String? get notifType => _notifType; - String? get notifKeys => _notifKeys; - String? get notifTitle => _notifTitle; - String? get notifBody => _notifBody; - String? get notifWebLink => _notifWebLink; - String? get notifAndroidLink => _notifAndroidLink; - String? get notifWebImage => _notifWebImage; - String? get notifAndroidImage => _notifAndroidImage; - String? get notifWebClickAction => _notifWebClickAction; - - String get notifSummaryText { - final parts = []; - if (_notifTitle != null && _notifTitle!.isNotEmpty) - parts.add('Title: ${_notifTitle!}'); - if (_notifBody != null && _notifBody!.isNotEmpty) - parts.add('Body: ${_notifBody!}'); - if (_notifType != null && _notifType!.isNotEmpty) - parts.add('Type: ${_notifType!}'); - if (_notifFrom != null && _notifFrom!.isNotEmpty) - parts.add('From: ${_notifFrom!}'); - if (_notifKeys != null && _notifKeys!.isNotEmpty) - parts.add('Keys: ${_notifKeys!}'); - if (_notifWebLink != null && _notifWebLink!.isNotEmpty) - parts.add('Web: ${_notifWebLink!}'); - if (_notifAndroidLink != null && _notifAndroidLink!.isNotEmpty) - parts.add('Android: ${_notifAndroidLink!}'); - if (_notifWebImage != null && _notifWebImage!.isNotEmpty) - parts.add('WebImg: ${_notifWebImage!}'); - if (_notifAndroidImage != null && _notifAndroidImage!.isNotEmpty) - parts.add('AndroidImg: ${_notifAndroidImage!}'); - if (_notifWebClickAction != null && _notifWebClickAction!.isNotEmpty) - parts.add('Click: ${_notifWebClickAction!}'); - if (_notifData != null && _notifData!.isNotEmpty) - parts.add('Data: ${_notifData!}'); - return parts.isEmpty ? '-' : parts.join(' | '); - } - Future getProfile() async { _isLoading = true; _errorMessage = null; @@ -139,7 +81,7 @@ class ProfileViewModel with ChangeNotifier { switch (result) { case Ok(): - await _authViewModel.localLogout(); + await _authRepository.localLogout(); _loggedOut = true; break; case Error(): @@ -152,28 +94,4 @@ class ProfileViewModel with ChangeNotifier { _loggedOut = false; notifyListeners(); } - - Future loadNotificationInfoFromPrefs() async { - // Do not toggle global loading; this is a lightweight local load - try { - final prefs = await SharedPreferences.getInstance(); - _notifData = prefs.getString('notif_data'); - _notifFrom = prefs.getString('notif_from'); - _notifType = prefs.getString('notif_type'); - _notifKeys = prefs.getString('notif_keys'); - _notifTitle = prefs.getString('notif_title'); - _notifBody = prefs.getString('notif_body'); - _notifWebLink = prefs.getString('notif_web_link'); - _notifAndroidLink = prefs.getString('notif_android_link'); - _notifWebImage = prefs.getString('notif_web_image'); - _notifAndroidImage = prefs.getString('notif_android_image'); - _notifWebClickAction = prefs.getString('notif_web_click_action'); - } catch (e) { - // Non-fatal; surface as errorMessage so UI can toast if listening - _errorMessage = e.toString(); - } - notifyListeners(); - _errorMessage = null; - notifyListeners(); - } } diff --git a/lib/view_models/tender_detail_view_model.dart b/lib/view_models/tender_detail_view_model.dart index 5e4b324..86049da 100644 --- a/lib/view_models/tender_detail_view_model.dart +++ b/lib/view_models/tender_detail_view_model.dart @@ -9,23 +9,16 @@ import '../core/constants/tender_approval_status.dart'; import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart'; import '../data/services/model/tender_approvals_data/tender_approvals_data.dart'; import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart'; -import 'home_view_model.dart'; -import 'tenders_view_model.dart'; class TenderDetailViewModel with ChangeNotifier { final TenderDetailRepository _tenderDetailRepository; final TendersRepository _tendersRepository; - final HomeViewModel _homeViewModel; TenderDetailViewModel({ required TenderDetailRepository tenderDetailRepository, required TendersRepository tendersRepository, - required TendersViewModel tendersViewModel, - required HomeViewModel homeViewModel, }) : _tenderDetailRepository = tenderDetailRepository, - _tendersRepository = tendersRepository, - - _homeViewModel = homeViewModel; + _tendersRepository = tendersRepository; bool _isLoading = false; bool _isSubmitApprovalLoading = false; @@ -122,10 +115,6 @@ class TenderDetailViewModel with ChangeNotifier { notifyListeners(); } - void updateHome() async { - _homeViewModel.init(); - } - Future getTenderApprovals(String tenderId) async { _errorMessage = null; notifyListeners(); diff --git a/lib/view_models/tenders_view_model.dart b/lib/view_models/tenders_view_model.dart index 97c9b0f..baefb2c 100644 --- a/lib/view_models/tenders_view_model.dart +++ b/lib/view_models/tenders_view_model.dart @@ -9,15 +9,12 @@ import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_resp import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import '../core/utils/result.dart'; -import 'home_view_model.dart'; class TendersViewModel with ChangeNotifier { final TendersRepository _tendersRepository; - TendersViewModel({ - required TendersRepository tendersRepository, - required HomeViewModel homeViewModel, - }) : _tendersRepository = tendersRepository; + TendersViewModel({required TendersRepository tendersRepository}) + : _tendersRepository = tendersRepository; bool _isLoading = false; bool _isLoadingMore = false; diff --git a/lib/views/forget_password_create/strings/forgot_password_create_strings.dart b/lib/views/forget_password_create/strings/forgot_password_create_strings.dart index 55c03db..3fe19b8 100644 --- a/lib/views/forget_password_create/strings/forgot_password_create_strings.dart +++ b/lib/views/forget_password_create/strings/forgot_password_create_strings.dart @@ -13,4 +13,5 @@ class ForgotPasswordCreateStrings { static const String atLeastSpecial = 'β€’ At least one special character: ! @ # \$ % & * ^'; static const String passwordResetSuccess = 'Password reset successfully'; + static const String passwordResetFailed = 'Reset Password failed'; } diff --git a/lib/views/forget_password_otp/pages/d_forget_password_otp_page.dart b/lib/views/forget_password_otp/pages/d_forget_password_otp_page.dart index e3b469c..81e35d7 100644 --- a/lib/views/forget_password_otp/pages/d_forget_password_otp_page.dart +++ b/lib/views/forget_password_otp/pages/d_forget_password_otp_page.dart @@ -128,7 +128,7 @@ class _DesktopForgotPasswordOtpPageState onPressed: viewModel.canSubmitOtpPage ? () async { - await viewModel.verifyOtp(context); + await viewModel.verifyOtp(); } : null, ), diff --git a/lib/views/forget_password_otp/pages/m_forget_password_otp_page.dart b/lib/views/forget_password_otp/pages/m_forget_password_otp_page.dart index c2db98b..084353f 100644 --- a/lib/views/forget_password_otp/pages/m_forget_password_otp_page.dart +++ b/lib/views/forget_password_otp/pages/m_forget_password_otp_page.dart @@ -122,7 +122,7 @@ class _MobileForgotPasswordOtpPageState onPressed: viewModel.canSubmitOtpPage ? () async { - await viewModel.verifyOtp(context); + await viewModel.verifyOtp(); } : null, ), diff --git a/lib/views/forget_password_otp/pages/t_forget_password_otp_page.dart b/lib/views/forget_password_otp/pages/t_forget_password_otp_page.dart index 30ffab4..ab1ea86 100644 --- a/lib/views/forget_password_otp/pages/t_forget_password_otp_page.dart +++ b/lib/views/forget_password_otp/pages/t_forget_password_otp_page.dart @@ -127,7 +127,7 @@ class _TabletForgotPasswordOtpPageState onPressed: viewModel.canSubmitOtpPage ? () async { - await viewModel.verifyOtp(context); + await viewModel.verifyOtp(); } : null, ), diff --git a/lib/views/forgot_password/pages/d_forgot_apssword_page.dart b/lib/views/forgot_password/pages/d_forgot_apssword_page.dart index 805ced0..a241573 100644 --- a/lib/views/forgot_password/pages/d_forgot_apssword_page.dart +++ b/lib/views/forgot_password/pages/d_forgot_apssword_page.dart @@ -117,9 +117,7 @@ class _DesktopForgotPasswordPageState extends State { onPressed: viewModel.isEmailValid ? () async { - await viewModel.forgotPassword( - context, - ); + await viewModel.forgotPassword(); } : null, ), diff --git a/lib/views/forgot_password/pages/m_forgot_password_page.dart b/lib/views/forgot_password/pages/m_forgot_password_page.dart index 58f41b2..12f8fbc 100644 --- a/lib/views/forgot_password/pages/m_forgot_password_page.dart +++ b/lib/views/forgot_password/pages/m_forgot_password_page.dart @@ -110,7 +110,7 @@ class _MobileForgotPasswordPageState extends State { onPressed: viewModel.isEmailValid ? () async { - await viewModel.forgotPassword(context); + await viewModel.forgotPassword(); } : null, ), diff --git a/lib/views/forgot_password/pages/t_forgot_password_page.dart b/lib/views/forgot_password/pages/t_forgot_password_page.dart index f60c456..24a5a3d 100644 --- a/lib/views/forgot_password/pages/t_forgot_password_page.dart +++ b/lib/views/forgot_password/pages/t_forgot_password_page.dart @@ -111,7 +111,7 @@ class _TabletForgotPasswordPageState extends State { onPressed: viewModel.isEmailValid ? () async { - await viewModel.forgotPassword(context); + await viewModel.forgotPassword(); } : null, ), diff --git a/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart b/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart index 25ae91a..59f7198 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_desktop_page.dart @@ -49,83 +49,76 @@ class _LikedTendersDesktopPageState extends State { @override Widget build(BuildContext context) { - return PopScope( - onPopInvokedWithResult: (didPop, result) { - WidgetsBinding.instance.addPostFrameCallback((_) { - viewModel.updateHome(); - }); - }, - child: Scaffold( - key: scaffoldKey, - backgroundColor: AppColors.backgroundColor, - endDrawer: const LikeFiltersDrawer(), - body: SafeArea( - child: Column( - children: [ - const DesktopNavigationWidget(currentIndex: 1, haveFilter: true), - const SizedBox(height: 40), - SizedBox( + return Scaffold( + key: scaffoldKey, + backgroundColor: AppColors.backgroundColor, + endDrawer: const LikeFiltersDrawer(), + body: SafeArea( + child: Column( + children: [ + const DesktopNavigationWidget(currentIndex: 1, haveFilter: true), + const SizedBox(height: 40), + SizedBox( + width: 740, + child: TabletDesktopAppbar( + title: LikedTendersStrings.likedTenders, + haveFilter: true, + onFilterPressed: () { + scaffoldKey.currentState?.openEndDrawer(); + }, + ), + ), + const SizedBox(height: 24), + Expanded( + child: SizedBox( width: 740, - child: TabletDesktopAppbar( - title: LikedTendersStrings.likedTenders, - haveFilter: true, - onFilterPressed: () { - scaffoldKey.currentState?.openEndDrawer(); + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading && + (viewModel.data?.data?.feedback ?? []).isEmpty) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.secondary50, + ), + ); + } + + if (viewModel.errorMessage != null) { + return Center(child: Text(viewModel.errorMessage!)); + } + + final feedback = viewModel.data?.data?.feedback ?? []; + + if (feedback.isEmpty) { + return const Center( + child: Text(LikedTendersStrings.noLikedTenders), + ); + } + + final currentPage = viewModel.currentPage; + final totalPages = viewModel.data?.data?.meta?.pages ?? 1; + + return Column( + children: [ + _likedTendersList(feedback, viewModel), + + SizedBox(height: 10.0.h()), + + _likedTenersListPageControll( + context, + totalPages, + viewModel, + currentPage, + ), + + SizedBox(height: 30.0.h()), + ], + ); }, ), ), - const SizedBox(height: 24), - Expanded( - child: SizedBox( - width: 740, - child: Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading && - (viewModel.data?.data?.feedback ?? []).isEmpty) { - return const Center( - child: CircularProgressIndicator( - color: AppColors.secondary50, - ), - ); - } - - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); - } - - final feedback = viewModel.data?.data?.feedback ?? []; - - if (feedback.isEmpty) { - return const Center( - child: Text(LikedTendersStrings.noLikedTenders), - ); - } - - final currentPage = viewModel.currentPage; - final totalPages = viewModel.data?.data?.meta?.pages ?? 1; - - return Column( - children: [ - _likedTendersList(feedback, viewModel), - - SizedBox(height: 10.0.h()), - - _likedTenersListPageControll( - context, - totalPages, - viewModel, - currentPage, - ), - - SizedBox(height: 30.0.h()), - ], - ); - }, - ), - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart b/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart index 888a936..83cc82d 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_mobile_page.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -62,188 +61,179 @@ class _LikedTendersMobilePageState extends State { @override Widget build(BuildContext context) { - return PopScope( - onPopInvokedWithResult: (didPop, result) { - if (kIsWeb) { - WidgetsBinding.instance.addPostFrameCallback((_) { - viewModel.updateHome(); - }); - } - }, - child: SafeArea( - child: Scaffold( - backgroundColor: AppColors.backgroundColor, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(60), - child: AppBar( - backgroundColor: AppColors.backgroundColor, - elevation: 0, - titleSpacing: 0, - leading: IconButton( + return SafeArea( + child: Scaffold( + backgroundColor: AppColors.backgroundColor, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60), + child: AppBar( + backgroundColor: AppColors.backgroundColor, + elevation: 0, + titleSpacing: 0, + leading: IconButton( + icon: SvgPicture.asset( + AssetsManager.arrowLeft, + height: 24.0.w(), + width: 24.0.w(), + ), + onPressed: () => Navigator.pop(context), + ), + title: Text( + LikedTendersStrings.likedTenders, + style: TextStyle( + color: AppColors.grey70, + fontWeight: FontWeight.w600, + fontSize: 20.0.sp(), + ), + ), + actions: [ + IconButton( icon: SvgPicture.asset( - AssetsManager.arrowLeft, + AssetsManager.filter, height: 24.0.w(), width: 24.0.w(), ), - onPressed: () => Navigator.pop(context), - ), - title: Text( - LikedTendersStrings.likedTenders, - style: TextStyle( - color: AppColors.grey70, - fontWeight: FontWeight.w600, - fontSize: 20.0.sp(), - ), - ), - actions: [ - IconButton( - icon: SvgPicture.asset( - AssetsManager.filter, - height: 24.0.w(), - width: 24.0.w(), - ), - onPressed: () { - showModalBottomSheet( - backgroundColor: AppColors.backgroundColor, - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + onPressed: () { + showModalBottomSheet( + backgroundColor: AppColors.backgroundColor, + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), ), - builder: (context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 10.0.h()), - Text( - LikedTendersStrings.filter, - style: TextStyle( - fontWeight: FontWeight.w600, - color: AppColors.grey70, - fontSize: 20.0.sp(), - ), + ), + builder: (context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 10.0.h()), + Text( + LikedTendersStrings.filter, + style: TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.grey70, + fontSize: 20.0.sp(), ), - SizedBox(height: 20.0.h()), - _buildFilterOption('Industry'), - _buildFilterOption('Finance'), - _buildFilterOption('Business'), - _buildFilterOption('IT'), - _buildFilterOption('Software'), - SizedBox(height: 20.0.h()), - BaseButton( - backgroundColor: AppColors.primary2, - borderRadius: 24, - isEnabled: true, - onPressed: () { - Navigator.pop(context); - }, - text: 'Apply', - textColor: AppColors.textBlue, - ), - ], - ), - ); - }, - ); - }, - ), - - SizedBox(width: 16.0.w()), - ], - ), - ), - - body: Column( - children: [ - Expanded( - child: Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading && - (viewModel.data?.data?.feedback ?? []).isEmpty) { - return const Center( - child: CircularProgressIndicator( - color: AppColors.secondary50, - ), - ); - } - - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); - } - - final feedback = viewModel.data?.data?.feedback ?? []; - - if (feedback.isEmpty) { - return const Center( - child: Text(LikedTendersStrings.noLikedTenders), - ); - } - - final itemCount = - feedback.length + (viewModel.isLoadingMore ? 1 : 0); - - return ListView.builder( - controller: _scrollController, - padding: EdgeInsets.symmetric( - horizontal: 24.0.w(), - vertical: 15.0.h(), - ), - itemCount: itemCount, - itemBuilder: (context, index) { - if (index < feedback.length) { - final item = feedback[index]; - - return Padding( - padding: EdgeInsets.only(bottom: 20.0.h()), - child: Dismissible( - key: ValueKey(item.tenderId ?? index), - direction: DismissDirection.endToStart, - background: Container( - alignment: Alignment.centerRight, - padding: EdgeInsets.symmetric( - horizontal: 20.0.w(), - ), - child: SvgPicture.asset( - AssetsManager.trash, - width: 32.0.w(), - height: 32.0.h(), - ), - ), - onDismissed: (_) { - if (item.tenderId != null) { - viewModel.removeTenderById(item.tenderId!); - } + ), + SizedBox(height: 20.0.h()), + _buildFilterOption('Industry'), + _buildFilterOption('Finance'), + _buildFilterOption('Business'), + _buildFilterOption('IT'), + _buildFilterOption('Software'), + SizedBox(height: 20.0.h()), + BaseButton( + backgroundColor: AppColors.primary2, + borderRadius: 24, + isEnabled: true, + onPressed: () { + Navigator.pop(context); }, - child: LikedListItem(tender: item.tender!), + text: 'Apply', + textColor: AppColors.textBlue, ), - ); - } else { - return Padding( - padding: EdgeInsets.symmetric(vertical: 20.0.h()), - child: Center( - child: SizedBox( - width: 24.0.w(), - height: 24.0.h(), - child: const CircularProgressIndicator( - color: AppColors.secondary50, - strokeWidth: 2, - ), - ), - ), - ); - } - }, - ); - }, - ), + ], + ), + ); + }, + ); + }, ), + + SizedBox(width: 16.0.w()), ], ), ), + + body: Column( + children: [ + Expanded( + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading && + (viewModel.data?.data?.feedback ?? []).isEmpty) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.secondary50, + ), + ); + } + + if (viewModel.errorMessage != null) { + return Center(child: Text(viewModel.errorMessage!)); + } + + final feedback = viewModel.data?.data?.feedback ?? []; + + if (feedback.isEmpty) { + return const Center( + child: Text(LikedTendersStrings.noLikedTenders), + ); + } + + final itemCount = + feedback.length + (viewModel.isLoadingMore ? 1 : 0); + + return ListView.builder( + controller: _scrollController, + padding: EdgeInsets.symmetric( + horizontal: 24.0.w(), + vertical: 15.0.h(), + ), + itemCount: itemCount, + itemBuilder: (context, index) { + if (index < feedback.length) { + final item = feedback[index]; + + return Padding( + padding: EdgeInsets.only(bottom: 20.0.h()), + child: Dismissible( + key: ValueKey(item.tenderId ?? index), + direction: DismissDirection.endToStart, + background: Container( + alignment: Alignment.centerRight, + padding: EdgeInsets.symmetric( + horizontal: 20.0.w(), + ), + child: SvgPicture.asset( + AssetsManager.trash, + width: 32.0.w(), + height: 32.0.h(), + ), + ), + onDismissed: (_) { + if (item.tenderId != null) { + viewModel.removeTenderById(item.tenderId!); + } + }, + child: LikedListItem(tender: item.tender!), + ), + ); + } else { + return Padding( + padding: EdgeInsets.symmetric(vertical: 20.0.h()), + child: Center( + child: SizedBox( + width: 24.0.w(), + height: 24.0.h(), + child: const CircularProgressIndicator( + color: AppColors.secondary50, + strokeWidth: 2, + ), + ), + ), + ); + } + }, + ); + }, + ), + ), + ], + ), ), ); } diff --git a/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart b/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart index 8d932cd..63c3ebd 100644 --- a/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart +++ b/lib/views/liked_tenders/pages/liked_tenders_tablet_page.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -50,99 +49,89 @@ class _LikedTendersTabletPageState extends State { @override Widget build(BuildContext context) { final GlobalKey key = GlobalKey(); - return PopScope( - onPopInvokedWithResult: (didPop, result) { - if (kIsWeb) { - WidgetsBinding.instance.addPostFrameCallback((_) { - viewModel.updateHome(); - }); - } - }, - child: Scaffold( + return Scaffold( + key: key, + backgroundColor: AppColors.backgroundColor, + appBar: tabletAppBar( + title: LikedTendersStrings.likedTenders, key: key, - backgroundColor: AppColors.backgroundColor, - appBar: tabletAppBar( - title: LikedTendersStrings.likedTenders, - key: key, - context: context, - ), - drawer: const TabletNavigationWidget(currentIndex: 1), - body: SafeArea( - child: Center( - child: SizedBox( - width: 720, - child: Column( - children: [ - TabletDesktopAppbar( - title: LikedTendersStrings.likedTenders, - haveFilter: true, - onFilterPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: AppColors.backgroundColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), + context: context, + ), + drawer: const TabletNavigationWidget(currentIndex: 1), + body: SafeArea( + child: Center( + child: SizedBox( + width: 720, + child: Column( + children: [ + TabletDesktopAppbar( + title: LikedTendersStrings.likedTenders, + haveFilter: true, + onFilterPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: AppColors.backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width, + maxHeight: MediaQuery.sizeOf(context).height, + ), + builder: (context) => const LikeFiltersBottomSheet(), + ); + }, + ), + Expanded( + child: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading && + (viewModel.data?.data?.feedback ?? []).isEmpty) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.secondary50, ), - ), - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width, - maxHeight: MediaQuery.sizeOf(context).height, - ), - builder: (context) => const LikeFiltersBottomSheet(), + ); + } + + if (viewModel.errorMessage != null) { + return Center(child: Text(viewModel.errorMessage!)); + } + + final feedback = viewModel.data?.data?.feedback ?? []; + + if (feedback.isEmpty) { + return const Center( + child: Text(LikedTendersStrings.noLikedTenders), + ); + } + + final currentPage = viewModel.currentPage; + final totalPages = viewModel.data?.data?.meta?.pages ?? 1; + + return Column( + children: [ + _likedTendersList(feedback, viewModel), + + SizedBox(height: 10.0.h()), + + _likeTendersListPagesControll( + context, + totalPages, + viewModel, + currentPage, + ), + + SizedBox(height: 30.0.h()), + ], ); }, ), - Expanded( - child: Consumer( - builder: (context, viewModel, child) { - if (viewModel.isLoading && - (viewModel.data?.data?.feedback ?? []).isEmpty) { - return const Center( - child: CircularProgressIndicator( - color: AppColors.secondary50, - ), - ); - } - - if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); - } - - final feedback = viewModel.data?.data?.feedback ?? []; - - if (feedback.isEmpty) { - return const Center( - child: Text(LikedTendersStrings.noLikedTenders), - ); - } - - final currentPage = viewModel.currentPage; - final totalPages = - viewModel.data?.data?.meta?.pages ?? 1; - - return Column( - children: [ - _likedTendersList(feedback, viewModel), - - SizedBox(height: 10.0.h()), - - _likeTendersListPagesControll( - context, - totalPages, - viewModel, - currentPage, - ), - - SizedBox(height: 30.0.h()), - ], - ); - }, - ), - ), - ], - ), + ), + ], ), ), ), diff --git a/lib/views/profile/pages/m_profile_page.dart b/lib/views/profile/pages/m_profile_page.dart index d99325a..7917b80 100644 --- a/lib/views/profile/pages/m_profile_page.dart +++ b/lib/views/profile/pages/m_profile_page.dart @@ -29,9 +29,6 @@ class _MobileProfilePageState extends State { viewModel = context.read(); viewModel.addListener(_profileListener); // Load notification info saved in SharedPreferences - WidgetsBinding.instance.addPostFrameCallback((_) { - viewModel.loadNotificationInfoFromPrefs(); - }); } void _profileListener() { @@ -90,18 +87,7 @@ class _MobileProfilePageState extends State { ), ), ), - SizedBox(height: 8.0.h()), - Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - viewModel.notifSummaryText, - style: TextStyle( - fontSize: 14.0.sp(), - fontWeight: FontWeight.w400, - color: AppColors.grey60, - ), - ), - ), + SizedBox(height: 24.0.h()), TitleDescription( title: ProfileStrings.companyName,