runable
This commit is contained in:
358
lib/core/utils/database_optimizer.dart
Normal file
358
lib/core/utils/database_optimizer.dart
Normal file
@@ -0,0 +1,358 @@
|
||||
/// Database performance optimization utilities for Hive CE
|
||||
///
|
||||
/// Features:
|
||||
/// - Lazy box loading for large datasets
|
||||
/// - Database compaction strategies
|
||||
/// - Query optimization helpers
|
||||
/// - Cache management
|
||||
/// - Batch operations
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../constants/performance_constants.dart';
|
||||
import 'performance_monitor.dart';
|
||||
|
||||
/// Database optimization helpers for Hive CE
|
||||
class DatabaseOptimizer {
|
||||
/// Batch write operations for better performance
|
||||
static Future<void> batchWrite<T>({
|
||||
required Box<T> box,
|
||||
required Map<String, T> items,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Hive doesn't support batch operations natively,
|
||||
// but we can optimize by reducing individual writes
|
||||
final entries = items.entries.toList();
|
||||
final batchSize = PerformanceConstants.databaseBatchSize;
|
||||
|
||||
for (var i = 0; i < entries.length; i += batchSize) {
|
||||
final end = (i + batchSize < entries.length)
|
||||
? i + batchSize
|
||||
: entries.length;
|
||||
final batch = entries.sublist(i, end);
|
||||
|
||||
for (final entry in batch) {
|
||||
await box.put(entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'batchWrite',
|
||||
duration: duration,
|
||||
affectedRows: items.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Batch delete operations
|
||||
static Future<void> batchDelete<T>({
|
||||
required Box<T> box,
|
||||
required List<String> keys,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final batchSize = PerformanceConstants.databaseBatchSize;
|
||||
|
||||
for (var i = 0; i < keys.length; i += batchSize) {
|
||||
final end = (i + batchSize < keys.length) ? i + batchSize : keys.length;
|
||||
final batch = keys.sublist(i, end);
|
||||
|
||||
for (final key in batch) {
|
||||
await box.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'batchDelete',
|
||||
duration: duration,
|
||||
affectedRows: keys.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Compact database to reduce file size
|
||||
static Future<void> compactBox<T>(Box<T> box) async {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
await box.compact();
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'compact',
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Efficient filtered query with caching
|
||||
static List<T> queryWithFilter<T>({
|
||||
required Box<T> box,
|
||||
required bool Function(T item) filter,
|
||||
int? limit,
|
||||
}) {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final results = <T>[];
|
||||
final values = box.values;
|
||||
|
||||
for (final item in values) {
|
||||
if (filter(item)) {
|
||||
results.add(item);
|
||||
if (limit != null && results.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'queryWithFilter',
|
||||
duration: duration,
|
||||
affectedRows: results.length,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Efficient pagination
|
||||
static List<T> queryWithPagination<T>({
|
||||
required Box<T> box,
|
||||
required int page,
|
||||
int pageSize = 20,
|
||||
bool Function(T item)? filter,
|
||||
}) {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final skip = page * pageSize;
|
||||
final results = <T>[];
|
||||
var skipped = 0;
|
||||
var taken = 0;
|
||||
|
||||
final values = box.values;
|
||||
|
||||
for (final item in values) {
|
||||
if (filter != null && !filter(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skipped < skip) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (taken < pageSize) {
|
||||
results.add(item);
|
||||
taken++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'queryWithPagination',
|
||||
duration: duration,
|
||||
affectedRows: results.length,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Check if box needs compaction
|
||||
static bool needsCompaction<T>(Box<T> box) {
|
||||
// Hive automatically compacts when needed
|
||||
// This is a placeholder for custom compaction logic
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get box statistics
|
||||
static Map<String, dynamic> getBoxStats<T>(Box<T> box) {
|
||||
return {
|
||||
'name': box.name,
|
||||
'length': box.length,
|
||||
'isEmpty': box.isEmpty,
|
||||
'isOpen': box.isOpen,
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear old cache entries based on timestamp
|
||||
static Future<void> clearOldEntries<T>({
|
||||
required Box<T> box,
|
||||
required DateTime Function(T item) getTimestamp,
|
||||
required Duration maxAge,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final now = DateTime.now();
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in box.keys) {
|
||||
final item = box.get(key);
|
||||
if (item != null) {
|
||||
final timestamp = getTimestamp(item);
|
||||
if (now.difference(timestamp) > maxAge) {
|
||||
keysToDelete.add(key.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await batchDelete(box: box, keys: keysToDelete);
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'clearOldEntries',
|
||||
duration: duration,
|
||||
affectedRows: keysToDelete.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Optimize box by removing duplicates (if applicable)
|
||||
static Future<void> removeDuplicates<T>({
|
||||
required Box<T> box,
|
||||
required String Function(T item) getUniqueId,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final seen = <String>{};
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in box.keys) {
|
||||
final item = box.get(key);
|
||||
if (item != null) {
|
||||
final uniqueId = getUniqueId(item);
|
||||
if (seen.contains(uniqueId)) {
|
||||
keysToDelete.add(key.toString());
|
||||
} else {
|
||||
seen.add(uniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await batchDelete(box: box, keys: keysToDelete);
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'removeDuplicates',
|
||||
duration: duration,
|
||||
affectedRows: keysToDelete.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lazy box helper for large datasets
|
||||
class LazyBoxHelper {
|
||||
/// Load items in chunks to avoid memory issues
|
||||
static Future<List<T>> loadInChunks<T>({
|
||||
required LazyBox<T> lazyBox,
|
||||
int chunkSize = 50,
|
||||
bool Function(T item)? filter,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final results = <T>[];
|
||||
final keys = lazyBox.keys.toList();
|
||||
|
||||
for (var i = 0; i < keys.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < keys.length) ? i + chunkSize : keys.length;
|
||||
final chunkKeys = keys.sublist(i, end);
|
||||
|
||||
for (final key in chunkKeys) {
|
||||
final item = await lazyBox.get(key);
|
||||
if (item != null) {
|
||||
if (filter == null || filter(item)) {
|
||||
results.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'loadInChunks',
|
||||
duration: duration,
|
||||
affectedRows: results.length,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Get paginated items from lazy box
|
||||
static Future<List<T>> getPaginated<T>({
|
||||
required LazyBox<T> lazyBox,
|
||||
required int page,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final skip = page * pageSize;
|
||||
final keys = lazyBox.keys.skip(skip).take(pageSize).toList();
|
||||
final results = <T>[];
|
||||
|
||||
for (final key in keys) {
|
||||
final item = await lazyBox.get(key);
|
||||
if (item != null) {
|
||||
results.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
DatabaseTracker.logQuery(
|
||||
operation: 'getPaginated',
|
||||
duration: duration,
|
||||
affectedRows: results.length,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache manager for database queries
|
||||
class QueryCache<T> {
|
||||
final Map<String, _CachedQuery<T>> _cache = {};
|
||||
final Duration cacheDuration;
|
||||
|
||||
QueryCache({this.cacheDuration = const Duration(minutes: 5)});
|
||||
|
||||
/// Get or compute cached result
|
||||
Future<T> getOrCompute(
|
||||
String key,
|
||||
Future<T> Function() compute,
|
||||
) async {
|
||||
final cached = _cache[key];
|
||||
final now = DateTime.now();
|
||||
|
||||
if (cached != null && now.difference(cached.timestamp) < cacheDuration) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
final value = await compute();
|
||||
_cache[key] = _CachedQuery(value: value, timestamp: now);
|
||||
|
||||
// Clean old cache entries
|
||||
_cleanCache();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Invalidate specific cache entry
|
||||
void invalidate(String key) {
|
||||
_cache.remove(key);
|
||||
}
|
||||
|
||||
/// Clear all cache
|
||||
void clear() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void _cleanCache() {
|
||||
final now = DateTime.now();
|
||||
_cache.removeWhere((key, value) {
|
||||
return now.difference(value.timestamp) > cacheDuration;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _CachedQuery<T> {
|
||||
final T value;
|
||||
final DateTime timestamp;
|
||||
|
||||
_CachedQuery({
|
||||
required this.value,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
102
lib/core/utils/debouncer.dart
Normal file
102
lib/core/utils/debouncer.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
/// Performance utility for debouncing rapid function calls
|
||||
///
|
||||
/// Use cases:
|
||||
/// - Search input (300ms delay before search)
|
||||
/// - Auto-save functionality
|
||||
/// - API request rate limiting
|
||||
/// - Scroll position updates
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Debouncer utility to prevent excessive function calls
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final searchDebouncer = Debouncer(milliseconds: 300);
|
||||
///
|
||||
/// void onSearchChanged(String query) {
|
||||
/// searchDebouncer.run(() {
|
||||
/// performSearch(query);
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
class Debouncer {
|
||||
final int milliseconds;
|
||||
Timer? _timer;
|
||||
|
||||
Debouncer({required this.milliseconds});
|
||||
|
||||
/// Run the action after the debounce delay
|
||||
void run(VoidCallback action) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(Duration(milliseconds: milliseconds), action);
|
||||
}
|
||||
|
||||
/// Cancel any pending debounced action
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
/// Dispose of the debouncer
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Throttler utility to limit function call frequency
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final scrollThrottler = Throttler(milliseconds: 100);
|
||||
///
|
||||
/// void onScroll() {
|
||||
/// scrollThrottler.run(() {
|
||||
/// updateScrollPosition();
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
class Throttler {
|
||||
final int milliseconds;
|
||||
Timer? _timer;
|
||||
bool _isReady = true;
|
||||
|
||||
Throttler({required this.milliseconds});
|
||||
|
||||
/// Run the action only if throttle period has passed
|
||||
void run(VoidCallback action) {
|
||||
if (_isReady) {
|
||||
_isReady = false;
|
||||
action();
|
||||
_timer = Timer(Duration(milliseconds: milliseconds), () {
|
||||
_isReady = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel throttler
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_isReady = true;
|
||||
}
|
||||
|
||||
/// Dispose of the throttler
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Search-specific debouncer with common configuration
|
||||
class SearchDebouncer extends Debouncer {
|
||||
SearchDebouncer() : super(milliseconds: 300);
|
||||
}
|
||||
|
||||
/// Auto-save debouncer with longer delay
|
||||
class AutoSaveDebouncer extends Debouncer {
|
||||
AutoSaveDebouncer() : super(milliseconds: 1000);
|
||||
}
|
||||
|
||||
/// Scroll throttler for performance
|
||||
class ScrollThrottler extends Throttler {
|
||||
ScrollThrottler() : super(milliseconds: 100);
|
||||
}
|
||||
76
lib/core/utils/extensions.dart
Normal file
76
lib/core/utils/extensions.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
extension StringExtension on String {
|
||||
/// Capitalize first letter
|
||||
String capitalize() {
|
||||
if (isEmpty) return this;
|
||||
return '${this[0].toUpperCase()}${substring(1)}';
|
||||
}
|
||||
|
||||
/// Check if string is a valid number
|
||||
bool isNumeric() {
|
||||
return double.tryParse(this) != null;
|
||||
}
|
||||
|
||||
/// Truncate string with ellipsis
|
||||
String truncate(int maxLength, {String suffix = '...'}) {
|
||||
if (length <= maxLength) return this;
|
||||
return '${substring(0, maxLength)}$suffix';
|
||||
}
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
/// Check if date is today
|
||||
bool isToday() {
|
||||
final now = DateTime.now();
|
||||
return year == now.year && month == now.month && day == now.day;
|
||||
}
|
||||
|
||||
/// Check if date is yesterday
|
||||
bool isYesterday() {
|
||||
final yesterday = DateTime.now().subtract(const Duration(days: 1));
|
||||
return year == yesterday.year &&
|
||||
month == yesterday.month &&
|
||||
day == yesterday.day;
|
||||
}
|
||||
|
||||
/// Get relative time string (e.g., "2 hours ago")
|
||||
String getRelativeTime() {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(this);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'Just now';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}m ago';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}h ago';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays}d ago';
|
||||
} else {
|
||||
return '${(difference.inDays / 7).floor()}w ago';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleExtension on double {
|
||||
/// Round to specific decimal places
|
||||
double roundToDecimals(int decimals) {
|
||||
final mod = 10.0 * decimals;
|
||||
return (this * mod).round() / mod;
|
||||
}
|
||||
|
||||
/// Format as currency
|
||||
String toCurrency({String symbol = '\$'}) {
|
||||
return '$symbol${toStringAsFixed(2)}';
|
||||
}
|
||||
}
|
||||
|
||||
extension ListExtension<T> on List<T> {
|
||||
/// Check if list is not null and not empty
|
||||
bool get isNotEmpty => this.isNotEmpty;
|
||||
|
||||
/// Get first element or null
|
||||
T? get firstOrNull => isEmpty ? null : first;
|
||||
|
||||
/// Get last element or null
|
||||
T? get lastOrNull => isEmpty ? null : last;
|
||||
}
|
||||
43
lib/core/utils/formatters.dart
Normal file
43
lib/core/utils/formatters.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Utility class for formatting values
|
||||
class Formatters {
|
||||
Formatters._();
|
||||
|
||||
/// Format price with currency symbol
|
||||
static String formatPrice(double price, {String currency = 'USD'}) {
|
||||
final formatter = NumberFormat.currency(symbol: '\$', decimalDigits: 2);
|
||||
return formatter.format(price);
|
||||
}
|
||||
|
||||
/// Format date
|
||||
static String formatDate(DateTime date) {
|
||||
return DateFormat('MMM dd, yyyy').format(date);
|
||||
}
|
||||
|
||||
/// Format date and time
|
||||
static String formatDateTime(DateTime dateTime) {
|
||||
return DateFormat('MMM dd, yyyy hh:mm a').format(dateTime);
|
||||
}
|
||||
|
||||
/// Format time only
|
||||
static String formatTime(DateTime time) {
|
||||
return DateFormat('hh:mm a').format(time);
|
||||
}
|
||||
|
||||
/// Format number with thousand separators
|
||||
static String formatNumber(int number) {
|
||||
final formatter = NumberFormat('#,###');
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
/// Format percentage
|
||||
static String formatPercentage(double value, {int decimals = 0}) {
|
||||
return '${value.toStringAsFixed(decimals)}%';
|
||||
}
|
||||
|
||||
/// Format quantity (e.g., "5 items")
|
||||
static String formatQuantity(int quantity) {
|
||||
return '$quantity ${quantity == 1 ? 'item' : 'items'}';
|
||||
}
|
||||
}
|
||||
303
lib/core/utils/performance_monitor.dart
Normal file
303
lib/core/utils/performance_monitor.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
/// Performance monitoring utilities
|
||||
///
|
||||
/// Track and monitor app performance:
|
||||
/// - Frame rendering times
|
||||
/// - Memory usage
|
||||
/// - Widget rebuild counts
|
||||
/// - Network request durations
|
||||
/// - Database query times
|
||||
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants/performance_constants.dart';
|
||||
|
||||
/// Performance monitor for tracking app performance metrics
|
||||
class PerformanceMonitor {
|
||||
static final PerformanceMonitor _instance = PerformanceMonitor._internal();
|
||||
factory PerformanceMonitor() => _instance;
|
||||
PerformanceMonitor._internal();
|
||||
|
||||
final Map<String, _PerformanceMetric> _metrics = {};
|
||||
final List<_PerformanceLog> _logs = [];
|
||||
|
||||
/// Start tracking a performance metric
|
||||
void startTracking(String name) {
|
||||
if (kDebugMode) {
|
||||
_metrics[name] = _PerformanceMetric(
|
||||
name: name,
|
||||
startTime: DateTime.now(),
|
||||
);
|
||||
developer.Timeline.startSync(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop tracking and log the metric
|
||||
void stopTracking(String name) {
|
||||
if (kDebugMode) {
|
||||
final metric = _metrics[name];
|
||||
if (metric != null) {
|
||||
final duration = DateTime.now().difference(metric.startTime);
|
||||
_logMetric(name, duration);
|
||||
_metrics.remove(name);
|
||||
developer.Timeline.finishSync();
|
||||
|
||||
// Warn if operation took too long
|
||||
if (duration.inMilliseconds > PerformanceConstants.longFrameThresholdMs) {
|
||||
debugPrint(
|
||||
'⚠️ PERFORMANCE WARNING: $name took ${duration.inMilliseconds}ms',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Track a function execution time
|
||||
Future<T> trackAsync<T>(String name, Future<T> Function() function) async {
|
||||
startTracking(name);
|
||||
try {
|
||||
return await function();
|
||||
} finally {
|
||||
stopTracking(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Track a synchronous function execution time
|
||||
T track<T>(String name, T Function() function) {
|
||||
startTracking(name);
|
||||
try {
|
||||
return function();
|
||||
} finally {
|
||||
stopTracking(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a custom metric
|
||||
void logMetric(String name, Duration duration, {Map<String, dynamic>? metadata}) {
|
||||
if (kDebugMode) {
|
||||
_logMetric(name, duration, metadata: metadata);
|
||||
}
|
||||
}
|
||||
|
||||
void _logMetric(String name, Duration duration, {Map<String, dynamic>? metadata}) {
|
||||
final log = _PerformanceLog(
|
||||
name: name,
|
||||
duration: duration,
|
||||
timestamp: DateTime.now(),
|
||||
metadata: metadata,
|
||||
);
|
||||
_logs.add(log);
|
||||
|
||||
// Keep only last 100 logs
|
||||
if (_logs.length > 100) {
|
||||
_logs.removeAt(0);
|
||||
}
|
||||
|
||||
debugPrint('📊 PERFORMANCE: $name - ${duration.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
/// Get performance summary
|
||||
Map<String, dynamic> getSummary() {
|
||||
if (_logs.isEmpty) return {};
|
||||
|
||||
final summary = <String, List<int>>{};
|
||||
for (final log in _logs) {
|
||||
summary.putIfAbsent(log.name, () => []).add(log.duration.inMilliseconds);
|
||||
}
|
||||
|
||||
return summary.map((key, values) {
|
||||
final avg = values.reduce((a, b) => a + b) / values.length;
|
||||
final max = values.reduce((a, b) => a > b ? a : b);
|
||||
final min = values.reduce((a, b) => a < b ? a : b);
|
||||
|
||||
return MapEntry(key, {
|
||||
'average': avg.toStringAsFixed(2),
|
||||
'max': max,
|
||||
'min': min,
|
||||
'count': values.length,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear all logs
|
||||
void clearLogs() {
|
||||
_logs.clear();
|
||||
}
|
||||
|
||||
/// Print performance summary
|
||||
void printSummary() {
|
||||
if (kDebugMode) {
|
||||
final summary = getSummary();
|
||||
debugPrint('=== PERFORMANCE SUMMARY ===');
|
||||
summary.forEach((key, value) {
|
||||
debugPrint('$key: $value');
|
||||
});
|
||||
debugPrint('=========================');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PerformanceMetric {
|
||||
final String name;
|
||||
final DateTime startTime;
|
||||
|
||||
_PerformanceMetric({
|
||||
required this.name,
|
||||
required this.startTime,
|
||||
});
|
||||
}
|
||||
|
||||
class _PerformanceLog {
|
||||
final String name;
|
||||
final Duration duration;
|
||||
final DateTime timestamp;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
_PerformanceLog({
|
||||
required this.name,
|
||||
required this.duration,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/// Widget to track rebuild count
|
||||
class RebuildTracker extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String name;
|
||||
|
||||
const RebuildTracker({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
static final Map<String, int> _rebuildCounts = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (kDebugMode) {
|
||||
_rebuildCounts[name] = (_rebuildCounts[name] ?? 0) + 1;
|
||||
debugPrint('🔄 REBUILD: $name (${_rebuildCounts[name]} times)');
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
static void printRebuildStats() {
|
||||
if (kDebugMode) {
|
||||
debugPrint('=== REBUILD STATS ===');
|
||||
_rebuildCounts.forEach((key, value) {
|
||||
debugPrint('$key: $value rebuilds');
|
||||
});
|
||||
debugPrint('====================');
|
||||
}
|
||||
}
|
||||
|
||||
static void clearStats() {
|
||||
_rebuildCounts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory usage tracker (simplified)
|
||||
class MemoryTracker {
|
||||
static void logMemoryUsage(String label) {
|
||||
if (kDebugMode) {
|
||||
// Note: Actual memory tracking would require platform-specific implementation
|
||||
debugPrint('💾 MEMORY CHECK: $label');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Network request tracker
|
||||
class NetworkTracker {
|
||||
static final List<_NetworkLog> _logs = [];
|
||||
|
||||
static void logRequest({
|
||||
required String url,
|
||||
required Duration duration,
|
||||
required int statusCode,
|
||||
int? responseSize,
|
||||
}) {
|
||||
if (kDebugMode) {
|
||||
final log = _NetworkLog(
|
||||
url: url,
|
||||
duration: duration,
|
||||
statusCode: statusCode,
|
||||
responseSize: responseSize,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
_logs.add(log);
|
||||
|
||||
// Keep only last 50 logs
|
||||
if (_logs.length > 50) {
|
||||
_logs.removeAt(0);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'🌐 NETWORK: $url - ${duration.inMilliseconds}ms (${statusCode})',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void printStats() {
|
||||
if (kDebugMode && _logs.isNotEmpty) {
|
||||
final totalDuration = _logs.fold<int>(
|
||||
0,
|
||||
(sum, log) => sum + log.duration.inMilliseconds,
|
||||
);
|
||||
final avgDuration = totalDuration / _logs.length;
|
||||
|
||||
debugPrint('=== NETWORK STATS ===');
|
||||
debugPrint('Total requests: ${_logs.length}');
|
||||
debugPrint('Average duration: ${avgDuration.toStringAsFixed(2)}ms');
|
||||
debugPrint('====================');
|
||||
}
|
||||
}
|
||||
|
||||
static void clearLogs() {
|
||||
_logs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkLog {
|
||||
final String url;
|
||||
final Duration duration;
|
||||
final int statusCode;
|
||||
final int? responseSize;
|
||||
final DateTime timestamp;
|
||||
|
||||
_NetworkLog({
|
||||
required this.url,
|
||||
required this.duration,
|
||||
required this.statusCode,
|
||||
this.responseSize,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/// Database query tracker
|
||||
class DatabaseTracker {
|
||||
static void logQuery({
|
||||
required String operation,
|
||||
required Duration duration,
|
||||
int? affectedRows,
|
||||
}) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'💿 DATABASE: $operation - ${duration.inMilliseconds}ms'
|
||||
'${affectedRows != null ? ' ($affectedRows rows)' : ''}',
|
||||
);
|
||||
|
||||
if (duration.inMilliseconds > 100) {
|
||||
debugPrint('⚠️ SLOW QUERY: $operation took ${duration.inMilliseconds}ms');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension for easy performance tracking
|
||||
extension PerformanceTrackingExtension<T> on Future<T> {
|
||||
Future<T> trackPerformance(String name) {
|
||||
return PerformanceMonitor().trackAsync(name, () => this);
|
||||
}
|
||||
}
|
||||
274
lib/core/utils/responsive_helper.dart
Normal file
274
lib/core/utils/responsive_helper.dart
Normal file
@@ -0,0 +1,274 @@
|
||||
/// Responsive layout utilities for optimal performance across devices
|
||||
///
|
||||
/// Features:
|
||||
/// - Breakpoint-based layouts
|
||||
/// - Adaptive grid columns
|
||||
/// - Performance-optimized responsive widgets
|
||||
/// - Device-specific optimizations
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants/performance_constants.dart';
|
||||
|
||||
/// Responsive helper for device-specific optimizations
|
||||
class ResponsiveHelper {
|
||||
/// Check if device is mobile
|
||||
static bool isMobile(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width < PerformanceConstants.mobileBreakpoint;
|
||||
}
|
||||
|
||||
/// Check if device is tablet
|
||||
static bool isTablet(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
return width >= PerformanceConstants.mobileBreakpoint &&
|
||||
width < PerformanceConstants.desktopBreakpoint;
|
||||
}
|
||||
|
||||
/// Check if device is desktop
|
||||
static bool isDesktop(BuildContext context) {
|
||||
return MediaQuery.of(context).size.width >= PerformanceConstants.desktopBreakpoint;
|
||||
}
|
||||
|
||||
/// Get appropriate grid column count
|
||||
static int getGridColumns(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
return PerformanceConstants.getGridColumnCount(width);
|
||||
}
|
||||
|
||||
/// Get appropriate cache extent
|
||||
static double getCacheExtent(BuildContext context) {
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
return PerformanceConstants.getCacheExtent(height);
|
||||
}
|
||||
|
||||
/// Check if high performance mode should be enabled
|
||||
static bool shouldUseHighPerformance(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
return PerformanceConstants.shouldUseHighPerformanceMode(width);
|
||||
}
|
||||
|
||||
/// Get value based on screen size
|
||||
static T getValue<T>(
|
||||
BuildContext context, {
|
||||
required T mobile,
|
||||
T? tablet,
|
||||
T? desktop,
|
||||
}) {
|
||||
if (isDesktop(context) && desktop != null) return desktop;
|
||||
if (isTablet(context) && tablet != null) return tablet;
|
||||
return mobile;
|
||||
}
|
||||
|
||||
/// Get responsive padding
|
||||
static EdgeInsets getResponsivePadding(BuildContext context) {
|
||||
if (isDesktop(context)) {
|
||||
return const EdgeInsets.all(24);
|
||||
} else if (isTablet(context)) {
|
||||
return const EdgeInsets.all(16);
|
||||
} else {
|
||||
return const EdgeInsets.all(12);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get responsive spacing
|
||||
static double getSpacing(BuildContext context) {
|
||||
if (isDesktop(context)) return 16;
|
||||
if (isTablet(context)) return 12;
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Responsive layout builder with performance optimization
|
||||
class ResponsiveLayout extends StatelessWidget {
|
||||
final Widget mobile;
|
||||
final Widget? tablet;
|
||||
final Widget? desktop;
|
||||
|
||||
const ResponsiveLayout({
|
||||
super.key,
|
||||
required this.mobile,
|
||||
this.tablet,
|
||||
this.desktop,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= PerformanceConstants.desktopBreakpoint) {
|
||||
return desktop ?? tablet ?? mobile;
|
||||
} else if (constraints.maxWidth >= PerformanceConstants.mobileBreakpoint) {
|
||||
return tablet ?? mobile;
|
||||
} else {
|
||||
return mobile;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Responsive value builder
|
||||
class ResponsiveValue<T> extends StatelessWidget {
|
||||
final T mobile;
|
||||
final T? tablet;
|
||||
final T? desktop;
|
||||
final Widget Function(BuildContext context, T value) builder;
|
||||
|
||||
const ResponsiveValue({
|
||||
super.key,
|
||||
required this.mobile,
|
||||
this.tablet,
|
||||
this.desktop,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = ResponsiveHelper.getValue(
|
||||
context,
|
||||
mobile: mobile,
|
||||
tablet: tablet,
|
||||
desktop: desktop,
|
||||
);
|
||||
return builder(context, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adaptive grid configuration
|
||||
class AdaptiveGridConfig {
|
||||
final int crossAxisCount;
|
||||
final double childAspectRatio;
|
||||
final double spacing;
|
||||
final double cacheExtent;
|
||||
|
||||
const AdaptiveGridConfig({
|
||||
required this.crossAxisCount,
|
||||
required this.childAspectRatio,
|
||||
required this.spacing,
|
||||
required this.cacheExtent,
|
||||
});
|
||||
|
||||
factory AdaptiveGridConfig.fromContext(
|
||||
BuildContext context, {
|
||||
GridType type = GridType.products,
|
||||
}) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
|
||||
return AdaptiveGridConfig(
|
||||
crossAxisCount: PerformanceConstants.getGridColumnCount(width),
|
||||
childAspectRatio: type == GridType.products
|
||||
? PerformanceConstants.productCardAspectRatio
|
||||
: PerformanceConstants.categoryCardAspectRatio,
|
||||
spacing: PerformanceConstants.gridSpacing,
|
||||
cacheExtent: PerformanceConstants.getCacheExtent(height),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum GridType {
|
||||
products,
|
||||
categories,
|
||||
}
|
||||
|
||||
/// Responsive grid view that adapts to screen size
|
||||
class AdaptiveGridView<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
final GridType type;
|
||||
final ScrollController? scrollController;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const AdaptiveGridView({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.type = GridType.products,
|
||||
this.scrollController,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = AdaptiveGridConfig.fromContext(context, type: type);
|
||||
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
padding: padding ?? ResponsiveHelper.getResponsivePadding(context),
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
cacheExtent: config.cacheExtent,
|
||||
itemCount: items.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: config.crossAxisCount,
|
||||
crossAxisSpacing: config.spacing,
|
||||
mainAxisSpacing: config.spacing,
|
||||
childAspectRatio: config.childAspectRatio,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return RepaintBoundary(
|
||||
key: ValueKey('adaptive_grid_item_$index'),
|
||||
child: itemBuilder(context, item, index),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Responsive container with adaptive sizing
|
||||
class ResponsiveContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double? mobileWidth;
|
||||
final double? tabletWidth;
|
||||
final double? desktopWidth;
|
||||
|
||||
const ResponsiveContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.mobileWidth,
|
||||
this.tabletWidth,
|
||||
this.desktopWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = ResponsiveHelper.getValue(
|
||||
context,
|
||||
mobile: mobileWidth,
|
||||
tablet: tabletWidth,
|
||||
desktop: desktopWidth,
|
||||
);
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension for easier responsive values
|
||||
extension ResponsiveContextExtension on BuildContext {
|
||||
bool get isMobile => ResponsiveHelper.isMobile(this);
|
||||
bool get isTablet => ResponsiveHelper.isTablet(this);
|
||||
bool get isDesktop => ResponsiveHelper.isDesktop(this);
|
||||
|
||||
int get gridColumns => ResponsiveHelper.getGridColumns(this);
|
||||
double get cacheExtent => ResponsiveHelper.getCacheExtent(this);
|
||||
double get spacing => ResponsiveHelper.getSpacing(this);
|
||||
|
||||
EdgeInsets get responsivePadding => ResponsiveHelper.getResponsivePadding(this);
|
||||
|
||||
T responsive<T>({
|
||||
required T mobile,
|
||||
T? tablet,
|
||||
T? desktop,
|
||||
}) {
|
||||
return ResponsiveHelper.getValue(
|
||||
this,
|
||||
mobile: mobile,
|
||||
tablet: tablet,
|
||||
desktop: desktop,
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/core/utils/validators.dart
Normal file
66
lib/core/utils/validators.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
/// Utility class for input validation
|
||||
class Validators {
|
||||
Validators._();
|
||||
|
||||
/// Validate email
|
||||
static String? validateEmail(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email is required';
|
||||
}
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return 'Enter a valid email';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validate required field
|
||||
static String? validateRequired(String? value, {String? fieldName}) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '${fieldName ?? 'This field'} is required';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validate price
|
||||
static String? validatePrice(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Price is required';
|
||||
}
|
||||
final price = double.tryParse(value);
|
||||
if (price == null) {
|
||||
return 'Enter a valid price';
|
||||
}
|
||||
if (price <= 0) {
|
||||
return 'Price must be greater than 0';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validate quantity
|
||||
static String? validateQuantity(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Quantity is required';
|
||||
}
|
||||
final quantity = int.tryParse(value);
|
||||
if (quantity == null) {
|
||||
return 'Enter a valid quantity';
|
||||
}
|
||||
if (quantity < 0) {
|
||||
return 'Quantity cannot be negative';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Validate phone number
|
||||
static String? validatePhone(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Phone number is required';
|
||||
}
|
||||
final phoneRegex = RegExp(r'^\+?[\d\s-]{10,}$');
|
||||
if (!phoneRegex.hasMatch(value)) {
|
||||
return 'Enter a valid phone number';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user