Merge pull request 'refactors' (#206) from refactors into main
Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/206
This commit is contained in:
@@ -46,3 +46,8 @@ app.*.map.json
|
||||
|
||||
# FVM Version Cache
|
||||
.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
|
||||
@@ -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<FeedbackData> 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<Tender> tenders,
|
||||
@Default([]) List<FeedbackData> 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<void> 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
|
||||
|
||||
|
||||
@@ -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<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
|
||||
```dart
|
||||
// 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)
|
||||
|
||||
```dart
|
||||
// ✅ 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)
|
||||
|
||||
```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<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
|
||||
```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)
|
||||
|
||||
|
||||
@@ -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<NotificationStateService>()
|
||||
.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<bool> checkUnreadNotifications() async {
|
||||
final result = await api.checkUnread();
|
||||
_hasUnread = result.hasUnread;
|
||||
return _hasUnread;
|
||||
}
|
||||
|
||||
Future<void> 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<AppEvent>.broadcast();
|
||||
|
||||
Stream<AppEvent> 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<NotificationViewModel>();
|
||||
final homeVM = context.watch<HomeViewModel>();
|
||||
|
||||
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<NotificationState, bool>(
|
||||
(ref) => NotificationState(),
|
||||
);
|
||||
|
||||
class NotificationState extends StateNotifier<bool> {
|
||||
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
|
||||
|
||||
@@ -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<SingleChildWidget> get providersRemote {
|
||||
return [...cores, ...apiClients, ...repositories, ...viewModels, ...others];
|
||||
return [...cores, ...apiClients, ...repositories, ...services, ...viewModels];
|
||||
}
|
||||
|
||||
List<SingleChildWidget> 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<SingleChildWidget> 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<SingleChildWidget> 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<SingleChildWidget> 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<SingleChildWidget> get services {
|
||||
return [
|
||||
Provider(
|
||||
create:
|
||||
(context) => NotificationCheckService(
|
||||
homeRepository: context.read(),
|
||||
notificationStateService: context.read(),
|
||||
),
|
||||
lazy: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -112,61 +151,62 @@ List<SingleChildWidget> 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<SingleChildWidget> get others {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -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<AppShellScreen> createState() => _AppShellScreenState();
|
||||
}
|
||||
|
||||
class _AppShellScreenState extends State<AppShellScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Check for unread notifications when app shell loads
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<NotificationCheckService>().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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<HomeViewModel>().hasUnreadNotification,
|
||||
showBadge:
|
||||
context
|
||||
.watch<NotificationStateService>()
|
||||
.hasUnreadNotification,
|
||||
),
|
||||
_bottomNavigationItem(
|
||||
context: context,
|
||||
|
||||
@@ -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<void> checkUnreadNotifications() async {
|
||||
final result = await _homeRepository.checkUnreadNotifications();
|
||||
|
||||
switch (result) {
|
||||
case Ok<Map<String, dynamic>>():
|
||||
final response = result.value;
|
||||
final data = response['data'];
|
||||
_notificationStateService.setHasUnreadNotification(
|
||||
data is List && data.isNotEmpty,
|
||||
);
|
||||
break;
|
||||
case Error<Map<String, dynamic>>():
|
||||
_notificationStateService.setHasUnreadNotification(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -66,45 +66,9 @@ class _MyAppState extends State<MyApp> {
|
||||
// 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<MyApp> {
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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<void> logout() async {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _authRepository.logout();
|
||||
|
||||
switch (result) {
|
||||
case Ok<LogoutResponse>():
|
||||
await localLogout();
|
||||
break;
|
||||
case Error<LogoutResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> forgotPassword(BuildContext context) async {
|
||||
Future<void> forgotPassword() async {
|
||||
_isLoadingForgot = true;
|
||||
_errorMessageForget = null;
|
||||
successMessage = null;
|
||||
@@ -214,7 +193,7 @@ class AuthViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(BuildContext context) async {
|
||||
Future<void> verifyOtp() async {
|
||||
_isLoadingOtp = true;
|
||||
_errorMessageOtp = null;
|
||||
_verifyOtpSuccess = false;
|
||||
@@ -273,16 +252,16 @@ class AuthViewModel with ChangeNotifier {
|
||||
_resetPasswordSuccess = true;
|
||||
break;
|
||||
case Error<ResetPasswordResponse>():
|
||||
_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();
|
||||
|
||||
@@ -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<void> checkUnreadNotifications() async {
|
||||
final result = await _homeRepository.checkUnreadNotifications();
|
||||
|
||||
switch (result) {
|
||||
case Ok<Map<String, dynamic>>():
|
||||
final response = result.value;
|
||||
|
||||
final data = response['data'];
|
||||
if (data is List && data.isNotEmpty) {
|
||||
_hasUnreadNotification = true;
|
||||
} else {
|
||||
_hasUnreadNotification = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Error<Map<String, dynamic>>():
|
||||
_hasUnreadNotification = false;
|
||||
break;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setUnreadNotificationFalse() {
|
||||
_hasUnreadNotification = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<void> checkNotifications() async {
|
||||
// Re-check unread notifications from server
|
||||
await _notificationCheckService.checkUnreadNotifications();
|
||||
}
|
||||
|
||||
Future<void> markAsRead({required String notificationId}) async {
|
||||
|
||||
@@ -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 = <String>[];
|
||||
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<void> getProfile() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
@@ -139,7 +81,7 @@ class ProfileViewModel with ChangeNotifier {
|
||||
|
||||
switch (result) {
|
||||
case Ok<LogoutResponse>():
|
||||
await _authViewModel.localLogout();
|
||||
await _authRepository.localLogout();
|
||||
_loggedOut = true;
|
||||
break;
|
||||
case Error<LogoutResponse>():
|
||||
@@ -152,28 +94,4 @@ class ProfileViewModel with ChangeNotifier {
|
||||
_loggedOut = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> getTenderApprovals(String tenderId) async {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ class _DesktopForgotPasswordOtpPageState
|
||||
onPressed:
|
||||
viewModel.canSubmitOtpPage
|
||||
? () async {
|
||||
await viewModel.verifyOtp(context);
|
||||
await viewModel.verifyOtp();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -122,7 +122,7 @@ class _MobileForgotPasswordOtpPageState
|
||||
onPressed:
|
||||
viewModel.canSubmitOtpPage
|
||||
? () async {
|
||||
await viewModel.verifyOtp(context);
|
||||
await viewModel.verifyOtp();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -127,7 +127,7 @@ class _TabletForgotPasswordOtpPageState
|
||||
onPressed:
|
||||
viewModel.canSubmitOtpPage
|
||||
? () async {
|
||||
await viewModel.verifyOtp(context);
|
||||
await viewModel.verifyOtp();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -117,9 +117,7 @@ class _DesktopForgotPasswordPageState extends State<DesktopForgotPasswordPage> {
|
||||
onPressed:
|
||||
viewModel.isEmailValid
|
||||
? () async {
|
||||
await viewModel.forgotPassword(
|
||||
context,
|
||||
);
|
||||
await viewModel.forgotPassword();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -110,7 +110,7 @@ class _MobileForgotPasswordPageState extends State<MobileForgotPasswordPage> {
|
||||
onPressed:
|
||||
viewModel.isEmailValid
|
||||
? () async {
|
||||
await viewModel.forgotPassword(context);
|
||||
await viewModel.forgotPassword();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -111,7 +111,7 @@ class _TabletForgotPasswordPageState extends State<TabletForgotPasswordPage> {
|
||||
onPressed:
|
||||
viewModel.isEmailValid
|
||||
? () async {
|
||||
await viewModel.forgotPassword(context);
|
||||
await viewModel.forgotPassword();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
|
||||
@@ -49,13 +49,7 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
viewModel.updateHome();
|
||||
});
|
||||
},
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
endDrawer: const LikeFiltersDrawer(),
|
||||
@@ -127,7 +121,6 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +61,7 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (kIsWeb) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
viewModel.updateHome();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: SafeArea(
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
appBar: PreferredSize(
|
||||
@@ -244,7 +235,6 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +49,7 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final GlobalKey<ScaffoldState> 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(
|
||||
@@ -119,8 +110,7 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
|
||||
}
|
||||
|
||||
final currentPage = viewModel.currentPage;
|
||||
final totalPages =
|
||||
viewModel.data?.data?.meta?.pages ?? 1;
|
||||
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -146,7 +136,6 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
|
||||
viewModel = context.read<ProfileViewModel>();
|
||||
viewModel.addListener(_profileListener);
|
||||
// Load notification info saved in SharedPreferences
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
viewModel.loadNotificationInfoFromPrefs();
|
||||
});
|
||||
}
|
||||
|
||||
void _profileListener() {
|
||||
@@ -90,18 +87,7 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user