update perf

This commit is contained in:
Phuoc Nguyen
2025-12-02 17:32:20 +07:00
parent 211ebdf1d8
commit fc9b5e967f
13 changed files with 254 additions and 200 deletions

View File

@@ -10,6 +10,7 @@ library;
import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -569,10 +570,10 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
@riverpod
LoggingInterceptor loggingInterceptor(Ref ref) {
// Only enable logging in debug mode
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter
const bool isDebug = kDebugMode; // TODO: Replace with kDebugMode from Flutter
return LoggingInterceptor(
enableRequestLogging: false,
enableRequestLogging: true,
enableResponseLogging: isDebug,
enableErrorLogging: isDebug,
);

View File

@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
}
String _$loggingInterceptorHash() =>
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
/// Provider for ErrorTransformerInterceptor

View File

@@ -13,6 +13,7 @@ import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -248,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final curl = _cURLRepresentation(options);
// debugPrint(
// '╔╣ CURL Request ╠══════════════════════════════════════════════════',
// );
// debugPrint(curl);
// debugPrint(
// '╚═════════════════════════════════════════════════════════════════',
// );
debugPrint(
'╔╣ CURL Request ╠══════════════════════════════════════════════════',
);
debugPrint(curl);
debugPrint(
'╚═════════════════════════════════════════════════════════════════',
);
// Also log to dart:developer for better filtering in DevTools
developer.log(curl, name: 'DIO_CURL', time: DateTime.now());
handler.next(options);
@@ -467,7 +468,7 @@ Future<Dio> dio(Ref ref) async {
// Add interceptors in order
// 1. Custom Curl interceptor (first to log cURL commands)
// Uses debugPrint and developer.log for better visibility
..interceptors.add(CustomCurlLoggerInterceptor())
// ..interceptors.add(CustomCurlLoggerInterceptor())
// 2. Logging interceptor
..interceptors.add(ref.watch(loggingInterceptorProvider))
// 3. Auth interceptor (add tokens to requests)

View File

@@ -131,7 +131,7 @@ final class DioProvider
}
}
String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4';
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
/// Provider for DioClient

View File

@@ -87,7 +87,7 @@ class FrappeAuthService {
}
}
final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
const url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
// Build cookie header
final storedSession = await getStoredSession();

View File

@@ -6,7 +6,6 @@
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/constants/api_constants.dart';
@@ -14,7 +13,6 @@ import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_service.dart';
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/user.dart';
part 'auth_provider.g.dart';
@@ -80,10 +78,6 @@ class Auth extends _$Auth {
Future<FrappeAuthService> get _frappeAuthService async =>
await ref.read(frappeAuthServiceProvider.future);
/// Get auth remote data source
Future<AuthRemoteDataSource> get _remoteDataSource async =>
await ref.read(authRemoteDataSourceProvider.future);
/// Initialize with saved session if available
@override
Future<User?> build() async {
@@ -170,7 +164,6 @@ class Auth extends _$Auth {
}
final frappeService = await _frappeAuthService;
final remoteDataSource = await _remoteDataSource;
// Get current session (should exist from app startup)
final currentSession = await frappeService.getStoredSession();
@@ -183,22 +176,8 @@ class Auth extends _$Auth {
}
}
// Get stored session again
final session = await frappeService.getStoredSession();
if (session == null) {
throw Exception('Session not available');
}
// Call login API with current session
final loginResponse = await remoteDataSource.login(
phone: phoneNumber,
csrfToken: session['csrfToken']!,
sid: session['sid']!,
password: password, // Reserved for future use
);
// Update FlutterSecureStorage with new authenticated session
await frappeService.login(phoneNumber, password: password);
// Call login API and store session
final loginResponse = await frappeService.login(phoneNumber, password: password);
// Save rememberMe preference
await _localDataSource.saveRememberMe(rememberMe);

View File

@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth();
}
String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae';
String _$authHash() => r'd851980cad7a624f00eba69e19d8a4fee22008e7';
/// Authentication Provider
///

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
@@ -133,10 +134,7 @@ class _HomePageState extends ConsumerState<HomePage> {
},
)
: const SizedBox.shrink(),
loading: () => const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
loading: () => _buildPromotionsShimmer(colorScheme),
error: (error, stack) => const SizedBox.shrink(),
),
),
@@ -241,4 +239,93 @@ class _HomePageState extends ConsumerState<HomePage> {
),
);
}
/// Build shimmer loading for promotions section
Widget _buildPromotionsShimmer(ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title shimmer
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Chương trình ưu đãi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
const SizedBox(height: 12),
// Cards shimmer
SizedBox(
height: 210,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 3,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: colorScheme.surfaceContainerHighest,
highlightColor: colorScheme.surface,
child: Container(
width: 280,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image placeholder
Container(
height: 140,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
),
),
// Text placeholders
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 200,
height: 16,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
width: 140,
height: 12,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
),
);
},
),
),
],
),
);
}
}

View File

@@ -1,23 +1,25 @@
/// Provider: Promotions Provider
///
/// Manages the state of promotions data using Riverpod.
/// Provides access to active promotions throughout the app.
/// Uses the same data source as news articles (single API call).
///
/// Uses AsyncNotifierProvider for automatic loading, error, and data states.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/home/data/datasources/home_local_datasource.dart';
import 'package:worker/features/home/data/repositories/home_repository_impl.dart';
import 'package:worker/features/home/domain/entities/promotion.dart';
import 'package:worker/features/home/domain/usecases/get_promotions.dart';
import 'package:worker/features/news/presentation/providers/news_provider.dart';
part 'promotions_provider.g.dart';
/// Max number of promotions to display on home page
const int _maxPromotions = 5;
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
/// Uses the same data source as news articles to avoid duplicate API calls.
/// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
///
/// Usage:
/// ```dart
@@ -31,33 +33,22 @@ part 'promotions_provider.g.dart';
/// );
/// ```
@riverpod
class PromotionsNotifier extends _$PromotionsNotifier {
@override
Future<List<Promotion>> build() async {
// Initialize dependencies
final localDataSource = const HomeLocalDataSourceImpl();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
Future<List<Promotion>> promotions(Ref ref) async {
// Use newsArticles provider (same API call, no duplicate request)
final articles = await ref.watch(newsArticlesProvider.future);
// Fetch promotions (only active ones)
return await useCase();
}
// Take max 5 articles and convert to Promotion
final limitedArticles = articles.take(_maxPromotions).toList();
/// Refresh promotions data
///
/// Forces a refresh from the server (when API is available).
/// Updates the cached state with fresh data.
Future<void> refresh() async {
// Set loading state
state = const AsyncValue.loading();
// Fetch fresh data
state = await AsyncValue.guard(() async {
final localDataSource = const HomeLocalDataSourceImpl();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
return await useCase.refresh();
});
}
return limitedArticles.map((article) {
final now = DateTime.now();
return Promotion(
id: article.id,
title: article.title,
description: article.excerpt,
imageUrl: article.imageUrl,
startDate: article.publishedDate,
endDate: now.add(const Duration(days: 365)), // Always active
);
}).toList();
}

View File

@@ -10,8 +10,9 @@ part of 'promotions_provider.dart';
// ignore_for_file: type=lint, type=warning
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
/// Uses the same data source as news articles to avoid duplicate API calls.
/// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
///
/// Usage:
/// ```dart
@@ -25,13 +26,14 @@ part of 'promotions_provider.dart';
/// );
/// ```
@ProviderFor(PromotionsNotifier)
const promotionsProvider = PromotionsNotifierProvider._();
@ProviderFor(promotions)
const promotionsProvider = PromotionsProvider._();
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
/// Uses the same data source as news articles to avoid duplicate API calls.
/// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
///
/// Usage:
/// ```dart
@@ -44,12 +46,20 @@ const promotionsProvider = PromotionsNotifierProvider._();
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
final class PromotionsNotifierProvider
extends $AsyncNotifierProvider<PromotionsNotifier, List<Promotion>> {
final class PromotionsProvider
extends
$FunctionalProvider<
AsyncValue<List<Promotion>>,
List<Promotion>,
FutureOr<List<Promotion>>
>
with $FutureModifier<List<Promotion>>, $FutureProvider<List<Promotion>> {
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
/// Uses the same data source as news articles to avoid duplicate API calls.
/// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
///
/// Usage:
/// ```dart
@@ -62,7 +72,7 @@ final class PromotionsNotifierProvider
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
const PromotionsNotifierProvider._()
const PromotionsProvider._()
: super(
from: null,
argument: null,
@@ -74,48 +84,18 @@ final class PromotionsNotifierProvider
);
@override
String debugGetCreateSourceHash() => _$promotionsNotifierHash();
String debugGetCreateSourceHash() => _$promotionsHash();
@$internal
@override
PromotionsNotifier create() => PromotionsNotifier();
}
$FutureProviderElement<List<Promotion>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
String _$promotionsNotifierHash() =>
r'3cd866c74ba11c6519e9b63521e1757ef117c7a9';
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
///
/// Usage:
/// ```dart
/// // In a ConsumerWidget
/// final promotionsAsync = ref.watch(promotionsProvider);
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
abstract class _$PromotionsNotifier extends $AsyncNotifier<List<Promotion>> {
FutureOr<List<Promotion>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<List<Promotion>>, List<Promotion>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Promotion>>, List<Promotion>>,
AsyncValue<List<Promotion>>,
Object?,
Object?
>;
element.handleValue(ref, created);
FutureOr<List<Promotion>> create(Ref ref) {
return promotions(ref);
}
}
String _$promotionsHash() => r'2eac0298d2b84ad5cc50faa6b8a015dbf7b7a1d3';

View File

@@ -11,7 +11,6 @@ import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
@@ -36,8 +35,7 @@ class ProductsPage extends ConsumerWidget {
final l10n = AppLocalizations.of(context);
final productsAsync = ref.watch(productsProvider);
// Preload filter options for better UX when opening filter drawer
ref.watch(productFilterOptionsProvider);
// Filter options loaded lazily when filter drawer is opened (not here)
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLowest,
@@ -105,8 +103,10 @@ class ProductsPage extends ConsumerWidget {
),
),
// Brand Filter Chips
const BrandFilterChips(),
// Brand Filter Chips - only show after products are loaded
productsAsync.hasValue
? const BrandFilterChips()
: const SizedBox(height: 48.0),
// Products Grid
Expanded(

View File

@@ -86,15 +86,25 @@ class Products extends _$Products {
// Get repository with injected data sources
final repository = await ref.watch(productsRepositoryProvider.future);
// Fetch first page of products using unified API
// Fetch first page of products
List<Product> products;
// Build filter parameters from filter drawer
// Check if any filters or search are active
final hasFilters = filters.hasActiveFilters;
final hasSearch = searchQuery.isNotEmpty;
if (!hasFilters && !hasSearch) {
// No filters/search: Use simple getAllProducts for faster initial load
products = await repository.getAllProducts(
limitStart: 0,
limitPageLength: pageSize,
);
} else {
// Filters/search active: Use getProductsWithFilters
final List<String>? itemGroups = filters.productLines.isNotEmpty
? filters.productLines.toList()
: null;
// Use brands from productFiltersProvider (shared by chips and drawer)
final List<String>? brands = filters.brands.isNotEmpty
? filters.brands.toList()
: null;
@@ -102,24 +112,20 @@ class Products extends _$Products {
// Build item attributes from filter drawer (sizes, surfaces, colors)
final List<Map<String, String>> itemAttributes = [];
// Add size attributes
for (final size in filters.sizes) {
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
}
// Add surface attributes
for (final surface in filters.surfaces) {
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
}
// Add color attributes
for (final color in filters.colors) {
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
}
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
final String? keyword = hasSearch ? searchQuery : null;
// Use the comprehensive getProductsWithFilters method
products = await repository.getProductsWithFilters(
limitStart: 0,
limitPageLength: pageSize,
@@ -128,6 +134,7 @@ class Products extends _$Products {
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
searchKeyword: keyword,
);
}
// If we got less than pageSize, there are no more products
_hasMore = products.length >= pageSize;
@@ -149,38 +156,45 @@ class Products extends _$Products {
// Calculate pagination parameters
final limitStart = _currentPage * pageSize;
// Build filter parameters (same logic as build() method)
// Check if any filters or search are active
final hasFilters = filters.hasActiveFilters;
final hasSearch = searchQuery.isNotEmpty;
List<Product> newProducts;
if (!hasFilters && !hasSearch) {
// No filters/search: Use simple getAllProducts
newProducts = await repository.getAllProducts(
limitStart: limitStart,
limitPageLength: pageSize,
);
} else {
// Filters/search active: Use getProductsWithFilters
final List<String>? itemGroups = filters.productLines.isNotEmpty
? filters.productLines.toList()
: null;
// Use brands from productFiltersProvider (shared by chips and drawer)
final List<String>? brands = filters.brands.isNotEmpty
? filters.brands.toList()
: null;
// Build item attributes from filter drawer (sizes, surfaces, colors)
final List<Map<String, String>> itemAttributes = [];
// Add size attributes
for (final size in filters.sizes) {
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
}
// Add surface attributes
for (final surface in filters.surfaces) {
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
}
// Add color attributes
for (final color in filters.colors) {
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
}
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
final String? keyword = hasSearch ? searchQuery : null;
// Fetch next page using unified API
final newProducts = await repository.getProductsWithFilters(
newProducts = await repository.getProductsWithFilters(
limitStart: limitStart,
limitPageLength: pageSize,
itemGroups: itemGroups,
@@ -188,6 +202,7 @@ class Products extends _$Products {
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
searchKeyword: keyword,
);
}
// If we got less than pageSize, there are no more products
_hasMore = newProducts.length >= pageSize;

View File

@@ -228,7 +228,7 @@ final class ProductsProvider
Products create() => Products();
}
String _$productsHash() => r'6c55b22e75b912281feff3a68f84e488ccb7ab79';
String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4';
/// Products Provider
///