Update .gitignore to exclude Firebase configuration files and refactor notification handling in the app. Removed redundant logging and notification data storage in SharedPreferences. Introduced NotificationCheckService and NotificationStateService for improved notification management. Updated view models and UI components to utilize the new services, enhancing notification state tracking and user experience.
This commit is contained in:
+6
-1
@@ -45,4 +45,9 @@ app.*.map.json
|
||||
/android/app/release
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
.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,13 +32,14 @@ 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()),
|
||||
ChangeNotifierProvider(create: (context) => NotificationStateService()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -108,20 +111,32 @@ List<SingleChildWidget> get repositories {
|
||||
];
|
||||
}
|
||||
|
||||
List<SingleChildWidget> get services {
|
||||
return [
|
||||
Provider(
|
||||
create:
|
||||
(context) => NotificationCheckService(
|
||||
homeRepository: context.read(),
|
||||
notificationStateService: context.read(),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<SingleChildWidget> get viewModels {
|
||||
return [
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => AuthViewModel(authRepository: context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => HomeViewModel(homeRepository: context.read()),
|
||||
create:
|
||||
(context) => HomeViewModel(
|
||||
homeRepository: context.read(),
|
||||
notificationStateService: context.read(),
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create:
|
||||
(context) => TendersViewModel(
|
||||
tendersRepository: context.read(),
|
||||
homeViewModel: context.read(),
|
||||
),
|
||||
create: (context) => TendersViewModel(tendersRepository: context.read()),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create:
|
||||
@@ -129,7 +144,6 @@ List<SingleChildWidget> get viewModels {
|
||||
tenderDetailRepository: context.read(),
|
||||
tendersRepository: context.read(),
|
||||
tendersViewModel: context.read(),
|
||||
homeViewModel: context.read(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -143,7 +157,6 @@ List<SingleChildWidget> get viewModels {
|
||||
(context) => LikedTendersViewModel(
|
||||
likedTendersRepository: context.read(),
|
||||
tendersRepository: context.read(),
|
||||
homeViewModel: context.read(),
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
@@ -160,13 +173,10 @@ List<SingleChildWidget> get viewModels {
|
||||
ChangeNotifierProvider(
|
||||
create:
|
||||
(context) => NotificationViewModel(
|
||||
repositoryViewModel: context.read(),
|
||||
homeViewModel: context.read(),
|
||||
notificationsRepository: context.read(),
|
||||
notificationStateService: context.read(),
|
||||
notificationCheckService: context.read(),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
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,14 @@ 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 +142,6 @@ class AuthViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
// reset error after notify
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -151,6 +151,7 @@ class AuthViewModel with ChangeNotifier {
|
||||
await _authRepository.localLogout();
|
||||
} catch (e) {
|
||||
// Handle local logout exceptions gracefully
|
||||
AppLogger().error('❌ Failed to local logout: $e');
|
||||
}
|
||||
_loggedInUser = null;
|
||||
notifyListeners();
|
||||
@@ -177,7 +178,7 @@ class AuthViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> forgotPassword(BuildContext context) async {
|
||||
Future<void> forgotPassword() async {
|
||||
_isLoadingForgot = true;
|
||||
_errorMessageForget = null;
|
||||
successMessage = null;
|
||||
@@ -214,7 +215,7 @@ class AuthViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(BuildContext context) async {
|
||||
Future<void> verifyOtp() async {
|
||||
_isLoadingOtp = true;
|
||||
_errorMessageOtp = null;
|
||||
_verifyOtpSuccess = false;
|
||||
@@ -273,16 +274,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 +326,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';
|
||||
@@ -30,65 +29,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;
|
||||
@@ -152,28 +98,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,18 @@ 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 +117,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,83 +49,76 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
|
||||
|
||||
@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<LikedTendersViewModel>(
|
||||
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<LikedTendersViewModel>(
|
||||
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()),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<LikedTendersMobilePage> {
|
||||
|
||||
@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<LikedTendersViewModel>(
|
||||
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<LikedTendersViewModel>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<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(
|
||||
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<LikedTendersViewModel>(
|
||||
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<LikedTendersViewModel>(
|
||||
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()),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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