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:
amirrezaghabeli
2025-10-06 11:59:54 +03:30
parent 17634744f7
commit 42f2e93a8e
28 changed files with 1499 additions and 595 deletions
+6 -1
View File
@@ -45,4 +45,9 @@ app.*.map.json
/android/app/release /android/app/release
# FVM Version Cache # 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
+427
View File
@@ -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
+316
View File
@@ -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)
+288
View File
@@ -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
+25 -15
View File
@@ -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/repositories/your_tenders_repository.dart';
import 'package:tm_app/data/services/home_service.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/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_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/tender_detail_service.dart';
import 'package:tm_app/data/services/your_tenders_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'; 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'; import '../../view_models/profile_view_model.dart';
List<SingleChildWidget> get providersRemote { List<SingleChildWidget> get providersRemote {
return [...cores, ...apiClients, ...repositories, ...viewModels, ...others]; return [...cores, ...apiClients, ...repositories, ...services, ...viewModels];
} }
List<SingleChildWidget> get cores { List<SingleChildWidget> get cores {
return [ return [
Provider(create: (context) => NetworkManager(context.read())), Provider(create: (context) => NetworkManager(context.read())),
ChangeNotifierProvider(create: (context) => ThemeProvider()), 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 { List<SingleChildWidget> get viewModels {
return [ return [
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => AuthViewModel(authRepository: context.read()), create: (context) => AuthViewModel(authRepository: context.read()),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => HomeViewModel(homeRepository: context.read()), create:
(context) => HomeViewModel(
homeRepository: context.read(),
notificationStateService: context.read(),
),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: create: (context) => TendersViewModel(tendersRepository: context.read()),
(context) => TendersViewModel(
tendersRepository: context.read(),
homeViewModel: context.read(),
),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: create:
@@ -129,7 +144,6 @@ List<SingleChildWidget> get viewModels {
tenderDetailRepository: context.read(), tenderDetailRepository: context.read(),
tendersRepository: context.read(), tendersRepository: context.read(),
tendersViewModel: context.read(), tendersViewModel: context.read(),
homeViewModel: context.read(),
), ),
), ),
@@ -143,7 +157,6 @@ List<SingleChildWidget> get viewModels {
(context) => LikedTendersViewModel( (context) => LikedTendersViewModel(
likedTendersRepository: context.read(), likedTendersRepository: context.read(),
tendersRepository: context.read(), tendersRepository: context.read(),
homeViewModel: context.read(),
), ),
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
@@ -160,13 +173,10 @@ List<SingleChildWidget> get viewModels {
ChangeNotifierProvider( ChangeNotifierProvider(
create: create:
(context) => NotificationViewModel( (context) => NotificationViewModel(
repositoryViewModel: context.read(), notificationsRepository: context.read(),
homeViewModel: 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/desktop_shell_page.dart';
import 'package:tm_app/core/routes/app_shell/mobile_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/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/home_view_model.dart';
import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/responsive_builder.dart'; import 'package:tm_app/views/shared/responsive_builder.dart';
import '../../../view_models/notification_view_model.dart'; import '../../../view_models/notification_view_model.dart';
class AppShellScreen extends StatelessWidget { class AppShellScreen extends StatefulWidget {
const AppShellScreen({required this.navigationShell, super.key}); const AppShellScreen({required this.navigationShell, super.key});
final StatefulNavigationShell navigationShell; 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) { void _gotoBranch(int index, BuildContext context) {
if (index == navigationShell.currentIndex) { if (index == widget.navigationShell.currentIndex) {
return; return;
} }
// Navigate first // Navigate first
navigationShell.goBranch( widget.navigationShell.goBranch(
index, index,
initialLocation: index == navigationShell.currentIndex, initialLocation: index == widget.navigationShell.currentIndex,
); );
// Then trigger data loading after navigation is complete // Then trigger data loading after navigation is complete
@@ -45,16 +60,16 @@ class AppShellScreen extends StatelessWidget {
return ResponsiveBuilder( return ResponsiveBuilder(
mobile: SafeArea( mobile: SafeArea(
child: MobileShellPage( child: MobileShellPage(
navigationShell: navigationShell, navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context), onTap: (value) => _gotoBranch(value, context),
), ),
), ),
tablet: TabletShellPage( tablet: TabletShellPage(
navigationShell: navigationShell, navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context), onTap: (value) => _gotoBranch(value, context),
), ),
desktop: DesktopShellPage( desktop: DesktopShellPage(
navigationShell: navigationShell, navigationShell: widget.navigationShell,
onTap: (value) => _gotoBranch(value, context), 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/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.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 '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/home/strings/home_strings.dart';
import '../../../views/tenders/strings/tenders_strings.dart'; import '../../../views/tenders/strings/tenders_strings.dart';
@@ -65,7 +65,10 @@ class MobileShellPage extends StatelessWidget {
onTap: () => onTap(3), onTap: () => onTap(3),
iconPath: AssetsManager.notify, iconPath: AssetsManager.notify,
activeIconPath: AssetsManager.notifyActive, activeIconPath: AssetsManager.notifyActive,
showBadge: context.watch<HomeViewModel>().hasUnreadNotification, showBadge:
context
.watch<NotificationStateService>()
.hasUnreadNotification,
), ),
_bottomNavigationItem( _bottomNavigationItem(
context: context, 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);
}
-62
View File
@@ -66,45 +66,9 @@ class _MyAppState extends State<MyApp> {
// if (kIsWeb) { // if (kIsWeb) {
FirebaseMessaging.onMessage.listen((event) { FirebaseMessaging.onMessage.listen((event) {
final overlayCtx = rootNavigatorKey.currentContext; 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 != null) {
if (overlayCtx.mounted) { 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( AppToast.notification(
overlayCtx, overlayCtx,
event.notification?.title ?? 'New Notification', event.notification?.title ?? 'New Notification',
@@ -119,32 +83,6 @@ class _MyAppState extends State<MyApp> {
} }
void _handleMessage(RemoteMessage message) { 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( appRouter.go(
const SplashScreenRoute(navigateToNotification: true).location, const SplashScreenRoute(navigateToNotification: true).location,
); );
+9 -9
View File
@@ -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/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_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 '../core/utils/result.dart';
import '../data/repositories/auth_repository.dart'; import '../data/repositories/auth_repository.dart';
import '../data/services/model/login_response/login_response_model.dart'; import '../data/services/model/login_response/login_response_model.dart';
import '../data/services/model/logout_response/logout_response.dart'; import '../data/services/model/logout_response/logout_response.dart';
import '../data/services/model/reset_password_response/reset_password_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 { class AuthViewModel with ChangeNotifier {
final AuthRepository _authRepository; final AuthRepository _authRepository;
@@ -139,9 +142,6 @@ class AuthViewModel with ChangeNotifier {
} }
_isLoading = false; _isLoading = false;
notifyListeners();
// reset error after notify
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
} }
@@ -151,6 +151,7 @@ class AuthViewModel with ChangeNotifier {
await _authRepository.localLogout(); await _authRepository.localLogout();
} catch (e) { } catch (e) {
// Handle local logout exceptions gracefully // Handle local logout exceptions gracefully
AppLogger().error('❌ Failed to local logout: $e');
} }
_loggedInUser = null; _loggedInUser = null;
notifyListeners(); notifyListeners();
@@ -177,7 +178,7 @@ class AuthViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> forgotPassword(BuildContext context) async { Future<void> forgotPassword() async {
_isLoadingForgot = true; _isLoadingForgot = true;
_errorMessageForget = null; _errorMessageForget = null;
successMessage = null; successMessage = null;
@@ -214,7 +215,7 @@ class AuthViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> verifyOtp(BuildContext context) async { Future<void> verifyOtp() async {
_isLoadingOtp = true; _isLoadingOtp = true;
_errorMessageOtp = null; _errorMessageOtp = null;
_verifyOtpSuccess = false; _verifyOtpSuccess = false;
@@ -273,16 +274,16 @@ class AuthViewModel with ChangeNotifier {
_resetPasswordSuccess = true; _resetPasswordSuccess = true;
break; break;
case Error<ResetPasswordResponse>(): case Error<ResetPasswordResponse>():
_resetPasswordErrorMessage = result.error.toString(); _resetPasswordErrorMessage = ErrorUtils.getErrorMessage(result.error);
_resetPasswordSuccess = false; _resetPasswordSuccess = false;
break; break;
} }
_isResetPasswordLoading = false; _isResetPasswordLoading = false;
notifyListeners();
_resetPasswordErrorMessage = null; _resetPasswordErrorMessage = null;
notifyListeners(); notifyListeners();
} else { } else {
_resetPasswordErrorMessage = 'Reset token is required'; _resetPasswordErrorMessage =
ForgotPasswordCreateStrings.passwordResetFailed;
_isResetPasswordLoading = false; _isResetPasswordLoading = false;
notifyListeners(); notifyListeners();
} }
@@ -325,7 +326,6 @@ class AuthViewModel with ChangeNotifier {
emailController.dispose(); emailController.dispose();
newPasswordController.dispose(); newPasswordController.dispose();
otpController.dispose(); otpController.dispose();
usernameFocus.dispose(); usernameFocus.dispose();
passwordFocus.dispose(); passwordFocus.dispose();
emailFocus.dispose(); emailFocus.dispose();
+20 -33
View File
@@ -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/feedback_stats_response/feedback_stat_response.dart';
import '../data/services/model/home/home_response/home_response_model.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/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import '../data/services/notification_state_service.dart';
class HomeViewModel with ChangeNotifier { class HomeViewModel with ChangeNotifier {
final HomeRepository _homeRepository; final HomeRepository _homeRepository;
final NotificationStateService _notificationStateService;
HomeViewModel({required HomeRepository homeRepository}) HomeViewModel({
: _homeRepository = homeRepository { required HomeRepository homeRepository,
required NotificationStateService notificationStateService,
}) : _homeRepository = homeRepository,
_notificationStateService = notificationStateService {
_notificationStateService.addListener(_onNotificationStateChanged);
init(); init();
} }
void _onNotificationStateChanged() {
notifyListeners();
}
@override
void dispose() {
_notificationStateService.removeListener(_onNotificationStateChanged);
super.dispose();
}
bool _isLoading = false; bool _isLoading = false;
String? _errorMessage; String? _errorMessage;
HomeResponseModel? _homeResponse; HomeResponseModel? _homeResponse;
@@ -44,8 +60,8 @@ class HomeViewModel with ChangeNotifier {
bool get hasMore => _hasMore; bool get hasMore => _hasMore;
bool get isRecommendedMode => _isRecommendedMode; bool get isRecommendedMode => _isRecommendedMode;
bool _hasUnreadNotification = false; bool get hasUnreadNotification =>
bool get hasUnreadNotification => _hasUnreadNotification; _notificationStateService.hasUnreadNotification;
void init() async { void init() async {
_isLoading = true; _isLoading = true;
@@ -54,7 +70,6 @@ class HomeViewModel with ChangeNotifier {
await getApprovedStates(); await getApprovedStates();
await getYourTenders(reset: true); await getYourTenders(reset: true);
await getFeedbackStats(); await getFeedbackStats();
await checkUnreadNotifications();
if (_errorMessage != null) { if (_errorMessage != null) {
_isLoading = false; _isLoading = false;
@@ -212,32 +227,4 @@ class HomeViewModel with ChangeNotifier {
double get selfApplyPercent { double get selfApplyPercent {
return totalCount > 0 ? (selfApplyCount / totalCount) * 100 : 0; 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/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_tender_feedback/liked_tender_feedback.dart';
import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.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/feedback_response/feedback_response.dart';
import '../data/services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.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 { class LikedTendersViewModel with ChangeNotifier {
final LikedTendersRepository _likedTendersRepository; final LikedTendersRepository _likedTendersRepository;
final TendersRepository _tendersRepository; final TendersRepository _tendersRepository;
final HomeViewModel _homeViewModel;
LikedTendersViewModel({ LikedTendersViewModel({
required LikedTendersRepository likedTendersRepository, required LikedTendersRepository likedTendersRepository,
required TendersRepository tendersRepository, required TendersRepository tendersRepository,
required HomeViewModel homeViewModel,
}) : _likedTendersRepository = likedTendersRepository, }) : _likedTendersRepository = likedTendersRepository,
_homeViewModel = homeViewModel,
_tendersRepository = tendersRepository; _tendersRepository = tendersRepository;
bool isLoading = false; bool isLoading = false;
@@ -44,10 +42,6 @@ class LikedTendersViewModel with ChangeNotifier {
bool get isSubmitApprovalLoading => _isSubmitApprovalLoading; bool get isSubmitApprovalLoading => _isSubmitApprovalLoading;
bool get isRejectApprovalLoading => _isRejectApprovalLoading; bool get isRejectApprovalLoading => _isRejectApprovalLoading;
void updateHome() {
_homeViewModel.init();
}
void clearApprovalErrorMessage() { void clearApprovalErrorMessage() {
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
+14 -9
View File
@@ -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/core/utils/result.dart';
import 'package:tm_app/data/repositories/notification_repository.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/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'; import 'package:tm_app/views/notification/strings/notification_strings.dart';
class NotificationViewModel with ChangeNotifier { class NotificationViewModel with ChangeNotifier {
final NotificationsRepository _repository; final NotificationsRepository _repository;
final HomeViewModel _homeViewModel; final NotificationStateService _notificationStateService;
final NotificationCheckService _notificationCheckService;
NotificationViewModel({ NotificationViewModel({
required NotificationsRepository repositoryViewModel, required NotificationsRepository notificationsRepository,
required HomeViewModel homeViewModel, required NotificationStateService notificationStateService,
}) : _repository = repositoryViewModel, required NotificationCheckService notificationCheckService,
_homeViewModel = homeViewModel { }) : _repository = notificationsRepository,
_notificationStateService = notificationStateService,
_notificationCheckService = notificationCheckService {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
init(); init();
}); });
@@ -347,7 +351,7 @@ class NotificationViewModel with ChangeNotifier {
} }
void setNotificationFalse(BuildContext context) { void setNotificationFalse(BuildContext context) {
_homeViewModel.setUnreadNotificationFalse(); _notificationStateService.clearUnreadNotification();
} }
void _checkAndUpdateUnreadState() { void _checkAndUpdateUnreadState() {
@@ -379,8 +383,9 @@ class NotificationViewModel with ChangeNotifier {
return importantCount > 0; return importantCount > 0;
} }
void checkNotifications() { Future<void> checkNotifications() async {
_homeViewModel.checkUnreadNotifications(); // Re-check unread notifications from server
await _notificationCheckService.checkUnreadNotifications();
} }
Future<void> markAsRead({required String notificationId}) async { Future<void> markAsRead({required String notificationId}) async {
-78
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; 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/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_data/company_profile_data.dart';
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.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; CompanyProfileData? _companyProfileData;
bool _loggedOut = false; 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 isLoading => _isLoading;
bool get loggedOut => _loggedOut; bool get loggedOut => _loggedOut;
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
ProfileData? get profileData => _profileData; ProfileData? get profileData => _profileData;
CompanyProfileData? get companyProfileData => _companyProfileData; 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 { Future<void> getProfile() async {
_isLoading = true; _isLoading = true;
_errorMessage = null; _errorMessage = null;
@@ -152,28 +98,4 @@ class ProfileViewModel with ChangeNotifier {
_loggedOut = false; _loggedOut = false;
notifyListeners(); 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();
}
} }
+1 -10
View File
@@ -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_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_data/tender_approvals_data.dart';
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart'; import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart';
import 'home_view_model.dart';
import 'tenders_view_model.dart'; import 'tenders_view_model.dart';
class TenderDetailViewModel with ChangeNotifier { class TenderDetailViewModel with ChangeNotifier {
final TenderDetailRepository _tenderDetailRepository; final TenderDetailRepository _tenderDetailRepository;
final TendersRepository _tendersRepository; final TendersRepository _tendersRepository;
final HomeViewModel _homeViewModel;
TenderDetailViewModel({ TenderDetailViewModel({
required TenderDetailRepository tenderDetailRepository, required TenderDetailRepository tenderDetailRepository,
required TendersRepository tendersRepository, required TendersRepository tendersRepository,
required TendersViewModel tendersViewModel, required TendersViewModel tendersViewModel,
required HomeViewModel homeViewModel,
}) : _tenderDetailRepository = tenderDetailRepository, }) : _tenderDetailRepository = tenderDetailRepository,
_tendersRepository = tendersRepository, _tendersRepository = tendersRepository;
_homeViewModel = homeViewModel;
bool _isLoading = false; bool _isLoading = false;
bool _isSubmitApprovalLoading = false; bool _isSubmitApprovalLoading = false;
@@ -122,10 +117,6 @@ class TenderDetailViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void updateHome() async {
_homeViewModel.init();
}
Future<void> getTenderApprovals(String tenderId) async { Future<void> getTenderApprovals(String tenderId) async {
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
+2 -5
View File
@@ -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 'package:tm_app/views/tenders/strings/tenders_strings.dart';
import '../core/utils/result.dart'; import '../core/utils/result.dart';
import 'home_view_model.dart';
class TendersViewModel with ChangeNotifier { class TendersViewModel with ChangeNotifier {
final TendersRepository _tendersRepository; final TendersRepository _tendersRepository;
TendersViewModel({ TendersViewModel({required TendersRepository tendersRepository})
required TendersRepository tendersRepository, : _tendersRepository = tendersRepository;
required HomeViewModel homeViewModel,
}) : _tendersRepository = tendersRepository;
bool _isLoading = false; bool _isLoading = false;
bool _isLoadingMore = false; bool _isLoadingMore = false;
@@ -13,4 +13,5 @@ class ForgotPasswordCreateStrings {
static const String atLeastSpecial = static const String atLeastSpecial =
'• At least one special character: ! @ # \$ % & * ^'; '• At least one special character: ! @ # \$ % & * ^';
static const String passwordResetSuccess = 'Password reset successfully'; static const String passwordResetSuccess = 'Password reset successfully';
static const String passwordResetFailed = 'Reset Password failed';
} }
@@ -128,7 +128,7 @@ class _DesktopForgotPasswordOtpPageState
onPressed: onPressed:
viewModel.canSubmitOtpPage viewModel.canSubmitOtpPage
? () async { ? () async {
await viewModel.verifyOtp(context); await viewModel.verifyOtp();
} }
: null, : null,
), ),
@@ -122,7 +122,7 @@ class _MobileForgotPasswordOtpPageState
onPressed: onPressed:
viewModel.canSubmitOtpPage viewModel.canSubmitOtpPage
? () async { ? () async {
await viewModel.verifyOtp(context); await viewModel.verifyOtp();
} }
: null, : null,
), ),
@@ -127,7 +127,7 @@ class _TabletForgotPasswordOtpPageState
onPressed: onPressed:
viewModel.canSubmitOtpPage viewModel.canSubmitOtpPage
? () async { ? () async {
await viewModel.verifyOtp(context); await viewModel.verifyOtp();
} }
: null, : null,
), ),
@@ -117,9 +117,7 @@ class _DesktopForgotPasswordPageState extends State<DesktopForgotPasswordPage> {
onPressed: onPressed:
viewModel.isEmailValid viewModel.isEmailValid
? () async { ? () async {
await viewModel.forgotPassword( await viewModel.forgotPassword();
context,
);
} }
: null, : null,
), ),
@@ -110,7 +110,7 @@ class _MobileForgotPasswordPageState extends State<MobileForgotPasswordPage> {
onPressed: onPressed:
viewModel.isEmailValid viewModel.isEmailValid
? () async { ? () async {
await viewModel.forgotPassword(context); await viewModel.forgotPassword();
} }
: null, : null,
), ),
@@ -111,7 +111,7 @@ class _TabletForgotPasswordPageState extends State<TabletForgotPasswordPage> {
onPressed: onPressed:
viewModel.isEmailValid viewModel.isEmailValid
? () async { ? () async {
await viewModel.forgotPassword(context); await viewModel.forgotPassword();
} }
: null, : null,
), ),
@@ -49,83 +49,76 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return Scaffold(
onPopInvokedWithResult: (didPop, result) { key: scaffoldKey,
WidgetsBinding.instance.addPostFrameCallback((_) { backgroundColor: AppColors.backgroundColor,
viewModel.updateHome(); endDrawer: const LikeFiltersDrawer(),
}); body: SafeArea(
}, child: Column(
child: Scaffold( children: [
key: scaffoldKey, const DesktopNavigationWidget(currentIndex: 1, haveFilter: true),
backgroundColor: AppColors.backgroundColor, const SizedBox(height: 40),
endDrawer: const LikeFiltersDrawer(), SizedBox(
body: SafeArea( width: 740,
child: Column( child: TabletDesktopAppbar(
children: [ title: LikedTendersStrings.likedTenders,
const DesktopNavigationWidget(currentIndex: 1, haveFilter: true), haveFilter: true,
const SizedBox(height: 40), onFilterPressed: () {
SizedBox( scaffoldKey.currentState?.openEndDrawer();
},
),
),
const SizedBox(height: 24),
Expanded(
child: SizedBox(
width: 740, width: 740,
child: TabletDesktopAppbar( child: Consumer<LikedTendersViewModel>(
title: LikedTendersStrings.likedTenders, builder: (context, viewModel, child) {
haveFilter: true, if (viewModel.isLoading &&
onFilterPressed: () { (viewModel.data?.data?.feedback ?? []).isEmpty) {
scaffoldKey.currentState?.openEndDrawer(); 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/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -62,188 +61,179 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return SafeArea(
onPopInvokedWithResult: (didPop, result) { child: Scaffold(
if (kIsWeb) { backgroundColor: AppColors.backgroundColor,
WidgetsBinding.instance.addPostFrameCallback((_) { appBar: PreferredSize(
viewModel.updateHome(); preferredSize: const Size.fromHeight(60),
}); child: AppBar(
} backgroundColor: AppColors.backgroundColor,
}, elevation: 0,
child: SafeArea( titleSpacing: 0,
child: Scaffold( leading: IconButton(
backgroundColor: AppColors.backgroundColor, icon: SvgPicture.asset(
appBar: PreferredSize( AssetsManager.arrowLeft,
preferredSize: const Size.fromHeight(60), height: 24.0.w(),
child: AppBar( width: 24.0.w(),
backgroundColor: AppColors.backgroundColor, ),
elevation: 0, onPressed: () => Navigator.pop(context),
titleSpacing: 0, ),
leading: IconButton( title: Text(
LikedTendersStrings.likedTenders,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
actions: [
IconButton(
icon: SvgPicture.asset( icon: SvgPicture.asset(
AssetsManager.arrowLeft, AssetsManager.filter,
height: 24.0.w(), height: 24.0.w(),
width: 24.0.w(), width: 24.0.w(),
), ),
onPressed: () => Navigator.pop(context), onPressed: () {
), showModalBottomSheet(
title: Text( backgroundColor: AppColors.backgroundColor,
LikedTendersStrings.likedTenders, context: context,
style: TextStyle( isScrollControlled: true,
color: AppColors.grey70, shape: const RoundedRectangleBorder(
fontWeight: FontWeight.w600, borderRadius: BorderRadius.vertical(
fontSize: 20.0.sp(), top: Radius.circular(20),
),
),
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),
),
), ),
builder: (context) { ),
return Padding( builder: (context) {
padding: const EdgeInsets.all(16.0), return Padding(
child: Column( padding: const EdgeInsets.all(16.0),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
SizedBox(height: 10.0.h()), children: [
Text( SizedBox(height: 10.0.h()),
LikedTendersStrings.filter, Text(
style: TextStyle( LikedTendersStrings.filter,
fontWeight: FontWeight.w600, style: TextStyle(
color: AppColors.grey70, fontWeight: FontWeight.w600,
fontSize: 20.0.sp(), color: AppColors.grey70,
), fontSize: 20.0.sp(),
), ),
SizedBox(height: 20.0.h()), ),
_buildFilterOption('Industry'), SizedBox(height: 20.0.h()),
_buildFilterOption('Finance'), _buildFilterOption('Industry'),
_buildFilterOption('Business'), _buildFilterOption('Finance'),
_buildFilterOption('IT'), _buildFilterOption('Business'),
_buildFilterOption('Software'), _buildFilterOption('IT'),
SizedBox(height: 20.0.h()), _buildFilterOption('Software'),
BaseButton( SizedBox(height: 20.0.h()),
backgroundColor: AppColors.primary2, BaseButton(
borderRadius: 24, backgroundColor: AppColors.primary2,
isEnabled: true, borderRadius: 24,
onPressed: () { isEnabled: true,
Navigator.pop(context); 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!);
}
}, },
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/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -50,99 +49,89 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> key = GlobalKey(); final GlobalKey<ScaffoldState> key = GlobalKey();
return PopScope( return Scaffold(
onPopInvokedWithResult: (didPop, result) { key: key,
if (kIsWeb) { backgroundColor: AppColors.backgroundColor,
WidgetsBinding.instance.addPostFrameCallback((_) { appBar: tabletAppBar(
viewModel.updateHome(); title: LikedTendersStrings.likedTenders,
});
}
},
child: Scaffold(
key: key, key: key,
backgroundColor: AppColors.backgroundColor, context: context,
appBar: tabletAppBar( ),
title: LikedTendersStrings.likedTenders, drawer: const TabletNavigationWidget(currentIndex: 1),
key: key, body: SafeArea(
context: context, child: Center(
), child: SizedBox(
drawer: const TabletNavigationWidget(currentIndex: 1), width: 720,
body: SafeArea( child: Column(
child: Center( children: [
child: SizedBox( TabletDesktopAppbar(
width: 720, title: LikedTendersStrings.likedTenders,
child: Column( haveFilter: true,
children: [ onFilterPressed: () {
TabletDesktopAppbar( showModalBottomSheet(
title: LikedTendersStrings.likedTenders, context: context,
haveFilter: true, isScrollControlled: true,
onFilterPressed: () { backgroundColor: AppColors.backgroundColor,
showModalBottomSheet( shape: const RoundedRectangleBorder(
context: context, borderRadius: BorderRadius.vertical(
isScrollControlled: true, top: Radius.circular(20),
backgroundColor: AppColors.backgroundColor, ),
shape: const RoundedRectangleBorder( ),
borderRadius: BorderRadius.vertical( constraints: BoxConstraints(
top: Radius.circular(20), 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, if (viewModel.errorMessage != null) {
), return Center(child: Text(viewModel.errorMessage!));
builder: (context) => const LikeFiltersBottomSheet(), }
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()),
],
);
},
),
),
],
),
), ),
), ),
), ),
+1 -15
View File
@@ -29,9 +29,6 @@ class _MobileProfilePageState extends State<MobileProfilePage> {
viewModel = context.read<ProfileViewModel>(); viewModel = context.read<ProfileViewModel>();
viewModel.addListener(_profileListener); viewModel.addListener(_profileListener);
// Load notification info saved in SharedPreferences // Load notification info saved in SharedPreferences
WidgetsBinding.instance.addPostFrameCallback((_) {
viewModel.loadNotificationInfoFromPrefs();
});
} }
void _profileListener() { 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()), SizedBox(height: 24.0.h()),
TitleDescription( TitleDescription(
title: ProfileStrings.companyName, title: ProfileStrings.companyName,