Best Practices¶
This guide covers best practices for building robust, performant, and maintainable applications with LDK.
Architecture & Design Patterns¶
1. Service Layer Pattern¶
Organize your LDK usage into service classes for better maintainability:
// services/user_service.dart
class UserService {
static final UserService _instance = UserService._internal();
factory UserService() => _instance;
UserService._internal();
late final LDKCollection _usersCollection;
late final LDKEnhancedAuth _auth;
Future<void> initialize() async {
_usersCollection = LDK.collection('users') as LDKEnhancedCollection;
_auth = LDK.instance.enhancedAuth;
}
Future<List<User>> getUsers({
String? search,
int page = 1,
int limit = 20,
}) async {
final query = LDKQueryBuilder()
.limit(limit)
.offset((page - 1) * limit)
.sort(['-createdAt']);
if (search != null && search.isNotEmpty) {
query.where('username', containsi: search);
}
final response = await _usersCollection.get(
query: query,
useCache: true,
);
return response.data.map((json) => User.fromJson(json)).toList();
}
Future<User> createUser(CreateUserRequest request) async {
final response = await _usersCollection.create(
request.toJson(),
optimistic: true,
);
return User.fromJson(response);
}
Stream<User> watchUser(String userId) {
return _usersCollection.subscribeToEntry(userId).map((event) {
return User.fromJson(event.data);
});
}
}
2. Repository Pattern¶
Implement repositories for complex data operations:
// repositories/post_repository.dart
abstract class PostRepository {
Future<List<Post>> getPosts({PostFilter? filter});
Future<Post> getPost(String id);
Future<Post> createPost(CreatePostRequest request);
Future<Post> updatePost(String id, UpdatePostRequest request);
Future<void> deletePost(String id);
Stream<List<Post>> watchPosts({PostFilter? filter});
}
class LDKPostRepository implements PostRepository {
final LDKEnhancedCollection _collection;
final LDKIntelligentCache _cache;
LDKPostRepository()
: _collection = LDK.collection('posts') as LDKEnhancedCollection,
_cache = LDK.instance.cache;
@override
Future<List<Post>> getPosts({PostFilter? filter}) async {
final query = _buildQuery(filter);
final response = await _collection.get(
query: query,
useCache: true,
);
return response.data.map((json) => Post.fromJson(json)).toList();
}
@override
Stream<List<Post>> watchPosts({PostFilter? filter}) {
return _collection.watchQuery(
query: _buildQuery(filter),
refreshInterval: Duration(seconds: 30),
).map((response) {
return response.data.map((json) => Post.fromJson(json)).toList();
});
}
LDKQueryBuilder _buildQuery(PostFilter? filter) {
final query = LDKQueryBuilder()
.populate(['author', 'categories'])
.sort(['-publishedAt']);
if (filter != null) {
if (filter.authorId != null) {
query.where('author', eq: filter.authorId);
}
if (filter.categoryId != null) {
query.where('categories', eq: filter.categoryId);
}
if (filter.status != null) {
query.where('status', eq: filter.status);
}
if (filter.search != null) {
query.or([
LDKQueryBuilder().where('title', containsi: filter.search),
LDKQueryBuilder().where('content', containsi: filter.search),
]);
}
}
return query;
}
}
3. State Management Integration¶
Integrate LDK with your preferred state management solution:
With Provider¶
// providers/posts_provider.dart
class PostsProvider extends ChangeNotifier {
final PostRepository _repository;
List<Post> _posts = [];
bool _isLoading = false;
String? _error;
StreamSubscription? _subscription;
PostsProvider(this._repository);
List<Post> get posts => _posts;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> loadPosts({PostFilter? filter}) async {
_setLoading(true);
_setError(null);
try {
_posts = await _repository.getPosts(filter: filter);
notifyListeners();
} catch (e) {
_setError(e.toString());
} finally {
_setLoading(false);
}
}
void startWatching({PostFilter? filter}) {
_subscription?.cancel();
_subscription = _repository.watchPosts(filter: filter).listen(
(posts) {
_posts = posts;
notifyListeners();
},
onError: (error) {
_setError(error.toString());
},
);
}
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
void _setError(String? error) {
_error = error;
notifyListeners();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
With Riverpod¶
// providers/posts_providers.dart
final postRepositoryProvider = Provider<PostRepository>((ref) {
return LDKPostRepository();
});
final postsProvider = StateNotifierProvider<PostsNotifier, AsyncValue<List<Post>>>((ref) {
final repository = ref.watch(postRepositoryProvider);
return PostsNotifier(repository);
});
class PostsNotifier extends StateNotifier<AsyncValue<List<Post>>> {
final PostRepository _repository;
StreamSubscription? _subscription;
PostsNotifier(this._repository) : super(const AsyncValue.loading()) {
loadPosts();
}
Future<void> loadPosts({PostFilter? filter}) async {
state = const AsyncValue.loading();
try {
final posts = await _repository.getPosts(filter: filter);
state = AsyncValue.data(posts);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
void startWatching({PostFilter? filter}) {
_subscription?.cancel();
_subscription = _repository.watchPosts(filter: filter).listen(
(posts) {
state = AsyncValue.data(posts);
},
onError: (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
},
);
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
Performance Optimization¶
1. Efficient Data Loading¶
Use pagination and lazy loading for large datasets:
class InfiniteScrollController {
final PostRepository _repository;
final int _pageSize;
List<Post> _posts = [];
int _currentPage = 1;
bool _isLoading = false;
bool _hasMore = true;
InfiniteScrollController(this._repository, {int pageSize = 20})
: _pageSize = pageSize;
List<Post> get posts => _posts;
bool get isLoading => _isLoading;
bool get hasMore => _hasMore;
Future<void> loadNextPage() async {
if (_isLoading || !_hasMore) return;
_isLoading = true;
try {
final newPosts = await _repository.getPosts(
filter: PostFilter(
page: _currentPage,
limit: _pageSize,
),
);
if (newPosts.length < _pageSize) {
_hasMore = false;
}
_posts.addAll(newPosts);
_currentPage++;
} catch (e) {
// Handle error
rethrow;
} finally {
_isLoading = false;
}
}
void reset() {
_posts.clear();
_currentPage = 1;
_hasMore = true;
}
}
// Usage in widget
class PostListWidget extends StatefulWidget {
@override
_PostListWidgetState createState() => _PostListWidgetState();
}
class _PostListWidgetState extends State<PostListWidget> {
late final InfiniteScrollController _controller;
late final ScrollController _scrollController;
@override
void initState() {
super.initState();
_controller = InfiniteScrollController(PostRepository());
_scrollController = ScrollController()..addListener(_onScroll);
_controller.loadNextPage();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_controller.loadNextPage();
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: _controller.posts.length + (_controller.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _controller.posts.length) {
return Center(child: CircularProgressIndicator());
}
return PostTile(post: _controller.posts[index]);
},
);
}
}
2. Smart Caching Strategies¶
Configure caching based on data characteristics:
class CacheConfigurationService {
static Future<void> configureCaching() async {
final cache = LDK.instance.cache;
// Static content - long TTL, LFU eviction
await cache.setCacheStrategy('categories', CacheStrategy(
ttl: Duration(hours: 24),
maxSize: 100,
evictionPolicy: EvictionPolicy.lfu,
backgroundRefresh: true,
));
// User-generated content - medium TTL, LRU eviction
await cache.setCacheStrategy('posts', CacheStrategy(
ttl: Duration(minutes: 30),
maxSize: 500,
evictionPolicy: EvictionPolicy.lru,
refreshOnAccess: true,
backgroundRefresh: true,
));
// Real-time data - short TTL, FIFO eviction
await cache.setCacheStrategy('notifications', CacheStrategy(
ttl: Duration(minutes: 5),
maxSize: 50,
evictionPolicy: EvictionPolicy.fifo,
autoInvalidate: true,
));
// User profiles - medium TTL, size-based eviction
await cache.setCacheStrategy('users', CacheStrategy(
ttl: Duration(hours: 2),
maxSize: 200,
evictionPolicy: EvictionPolicy.lru,
));
}
}
3. Optimistic Updates¶
Implement optimistic updates for better UX:
class OptimisticUpdateService {
final LDKEnhancedCollection _collection;
final StreamController<List<Post>> _postsController;
OptimisticUpdateService(this._collection)
: _postsController = StreamController.broadcast();
Stream<List<Post>> get postsStream => _postsController.stream;
Future<Post> createPost(CreatePostRequest request) async {
// Create optimistic post
final optimisticPost = Post(
id: 'temp_${DateTime.now().millisecondsSinceEpoch}',
title: request.title,
content: request.content,
status: 'draft',
isOptimistic: true,
createdAt: DateTime.now(),
);
// Add to stream immediately
_addOptimisticPost(optimisticPost);
try {
// Create actual post
final response = await _collection.create(
request.toJson(),
optimistic: true,
);
final realPost = Post.fromJson(response);
// Replace optimistic post with real post
_replaceOptimisticPost(optimisticPost.id, realPost);
return realPost;
} catch (e) {
// Remove optimistic post on error
_removeOptimisticPost(optimisticPost.id);
rethrow;
}
}
void _addOptimisticPost(Post post) {
// Implementation depends on your state management
}
void _replaceOptimisticPost(String optimisticId, Post realPost) {
// Implementation depends on your state management
}
void _removeOptimisticPost(String optimisticId) {
// Implementation depends on your state management
}
}
Error Handling¶
1. Comprehensive Error Handling¶
Create a centralized error handling system:
// services/error_service.dart
class ErrorService {
static final ErrorService _instance = ErrorService._internal();
factory ErrorService() => _instance;
ErrorService._internal();
final StreamController<AppError> _errorController = StreamController.broadcast();
Stream<AppError> get errorStream => _errorController.stream;
void handleError(dynamic error, {String? context}) {
final appError = _mapError(error, context);
_errorController.add(appError);
// Log error
_logError(appError);
// Report to crash analytics if needed
_reportError(appError);
}
AppError _mapError(dynamic error, String? context) {
if (error is LDKAuthException) {
return AppError(
type: ErrorType.authentication,
message: _getAuthErrorMessage(error),
originalError: error,
context: context,
);
} else if (error is LDKServerException) {
return AppError(
type: ErrorType.server,
message: _getServerErrorMessage(error),
originalError: error,
context: context,
);
} else if (error is LDKNetworkException) {
return AppError(
type: ErrorType.network,
message: 'Network connection failed. Please check your internet connection.',
originalError: error,
context: context,
);
} else {
return AppError(
type: ErrorType.unknown,
message: 'An unexpected error occurred.',
originalError: error,
context: context,
);
}
}
String _getAuthErrorMessage(LDKAuthException error) {
switch (error.code) {
case 'invalid_credentials':
return 'Invalid email or password.';
case 'account_locked':
return 'Your account has been temporarily locked.';
case 'token_expired':
return 'Your session has expired. Please sign in again.';
default:
return 'Authentication failed.';
}
}
String _getServerErrorMessage(LDKServerException error) {
if (error.statusCode == 404) {
return 'The requested resource was not found.';
} else if (error.statusCode == 500) {
return 'Server error. Please try again later.';
} else {
return 'Server error: ${error.message}';
}
}
void _logError(AppError error) {
print('Error [${error.type}]: ${error.message}');
if (error.context != null) {
print('Context: ${error.context}');
}
}
void _reportError(AppError error) {
// Report to crash analytics service
// FirebaseCrashlytics.instance.recordError(error.originalError, null);
}
}
class AppError {
final ErrorType type;
final String message;
final dynamic originalError;
final String? context;
AppError({
required this.type,
required this.message,
required this.originalError,
this.context,
});
}
enum ErrorType {
authentication,
network,
server,
validation,
unknown,
}
2. Retry Logic¶
Implement intelligent retry mechanisms:
class RetryService {
static Future<T> withRetry<T>(
Future<T> Function() operation, {
int maxRetries = 3,
Duration delay = const Duration(seconds: 1),
bool Function(dynamic error)? shouldRetry,
}) async {
int attempts = 0;
while (attempts < maxRetries) {
try {
return await operation();
} catch (error) {
attempts++;
if (attempts >= maxRetries ||
(shouldRetry != null && !shouldRetry(error))) {
rethrow;
}
// Exponential backoff
final retryDelay = delay * math.pow(2, attempts - 1);
await Future.delayed(retryDelay);
}
}
throw StateError('Retry logic error'); // Should never reach here
}
static bool shouldRetryNetworkError(dynamic error) {
if (error is LDKNetworkException) {
return true;
}
if (error is LDKServerException) {
// Retry on 5xx errors, not on 4xx errors
return error.statusCode >= 500;
}
return false;
}
}
// Usage
Future<List<Post>> getPosts() async {
return RetryService.withRetry(
() => _repository.getPosts(),
maxRetries: 3,
shouldRetry: RetryService.shouldRetryNetworkError,
);
}
Security Best Practices¶
1. Token Management¶
Implement secure token handling:
class TokenManager {
static const String _tokenKey = 'auth_token';
static const String _refreshTokenKey = 'refresh_token';
final FlutterSecureStorage _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: IOSAccessibility.first_unlock_this_device,
),
);
Future<void> storeTokens({
required String accessToken,
required String refreshToken,
}) async {
await Future.wait([
_storage.write(key: _tokenKey, value: accessToken),
_storage.write(key: _refreshTokenKey, value: refreshToken),
]);
}
Future<String?> getAccessToken() async {
return await _storage.read(key: _tokenKey);
}
Future<String?> getRefreshToken() async {
return await _storage.read(key: _refreshTokenKey);
}
Future<void> clearTokens() async {
await Future.wait([
_storage.delete(key: _tokenKey),
_storage.delete(key: _refreshTokenKey),
]);
}
Future<bool> isTokenExpired(String token) async {
try {
final payload = _decodeJWT(token);
final exp = payload['exp'] as int;
final expiryDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
// Consider token expired 5 minutes before actual expiry
return DateTime.now().isAfter(expiryDate.subtract(Duration(minutes: 5)));
} catch (e) {
return true; // Assume expired if can't decode
}
}
Map<String, dynamic> _decodeJWT(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw FormatException('Invalid JWT token');
}
final payload = parts[1];
final normalized = base64Url.normalize(payload);
final decoded = utf8.decode(base64Url.decode(normalized));
return jsonDecode(decoded) as Map<String, dynamic>;
}
}
2. Input Validation¶
Validate data before sending to server:
class ValidationService {
static String? validateEmail(String? email) {
if (email == null || email.isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
if (!emailRegex.hasMatch(email)) {
return 'Please enter a valid email address';
}
return null;
}
static String? validatePassword(String? password) {
if (password == null || password.isEmpty) {
return 'Password is required';
}
if (password.length < 8) {
return 'Password must be at least 8 characters long';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(password)) {
return 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
}
return null;
}
static String? validateRequired(String? value, String fieldName) {
if (value == null || value.trim().isEmpty) {
return '$fieldName is required';
}
return null;
}
static String? validateLength(String? value, int minLength, int maxLength, String fieldName) {
if (value == null) return null;
if (value.length < minLength) {
return '$fieldName must be at least $minLength characters long';
}
if (value.length > maxLength) {
return '$fieldName must be no more than $maxLength characters long';
}
return null;
}
}
// Usage in forms
class CreatePostForm extends StatefulWidget {
@override
_CreatePostFormState createState() => _CreatePostFormState();
}
class _CreatePostFormState extends State<CreatePostForm> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _contentController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Title'),
validator: (value) {
return ValidationService.validateRequired(value, 'Title') ??
ValidationService.validateLength(value, 1, 100, 'Title');
},
),
TextFormField(
controller: _contentController,
decoration: InputDecoration(labelText: 'Content'),
maxLines: 5,
validator: (value) {
return ValidationService.validateRequired(value, 'Content') ??
ValidationService.validateLength(value, 10, 5000, 'Content');
},
),
ElevatedButton(
onPressed: _submitForm,
child: Text('Create Post'),
),
],
),
);
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Form is valid, proceed with submission
_createPost();
}
}
Future<void> _createPost() async {
try {
await PostService().createPost(CreatePostRequest(
title: _titleController.text.trim(),
content: _contentController.text.trim(),
));
Navigator.pop(context);
} catch (e) {
ErrorService().handleError(e, context: 'Create Post');
}
}
}
Testing Strategies¶
1. Unit Testing Services¶
// test/services/post_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([LDKEnhancedCollection])
void main() {
group('PostService', () {
late PostService postService;
late MockLDKEnhancedCollection mockCollection;
setUp(() {
mockCollection = MockLDKEnhancedCollection();
postService = PostService(mockCollection);
});
test('should return posts when getPosts is called', () async {
// Arrange
final mockResponse = StrapiResponse<List<Map<String, dynamic>>>(
data: [
{
'id': '1',
'attributes': {
'title': 'Test Post',
'content': 'Test Content',
'createdAt': '2023-01-01T00:00:00.000Z',
}
}
],
meta: {},
);
when(mockCollection.get(query: anyNamed('query')))
.thenAnswer((_) async => mockResponse);
// Act
final result = await postService.getPosts();
// Assert
expect(result, hasLength(1));
expect(result.first.title, equals('Test Post'));
verify(mockCollection.get(query: anyNamed('query'))).called(1);
});
test('should throw exception when getPosts fails', () async {
// Arrange
when(mockCollection.get(query: anyNamed('query')))
.thenThrow(LDKServerException('Server error'));
// Act & Assert
expect(
() => postService.getPosts(),
throwsA(isA<LDKServerException>()),
);
});
});
}
2. Integration Testing¶
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('App Integration Tests', () {
testWidgets('should login and load posts', (WidgetTester tester) async {
// Start the app
app.main();
await tester.pumpAndSettle();
// Find login form
expect(find.byType(TextField), findsNWidgets(2));
// Enter credentials
await tester.enterText(find.byType(TextField).first, 'test@example.com');
await tester.enterText(find.byType(TextField).last, 'password123');
// Tap login button
await tester.tap(find.text('Sign In'));
await tester.pumpAndSettle();
// Verify navigation to home screen
expect(find.text('Posts'), findsOneWidget);
// Wait for posts to load
await tester.pumpAndSettle(Duration(seconds: 3));
// Verify posts are displayed
expect(find.byType(ListTile), findsAtLeastNWidgets(1));
});
});
}
Monitoring & Analytics¶
1. Performance Monitoring¶
class PerformanceMonitor {
static final PerformanceMonitor _instance = PerformanceMonitor._internal();
factory PerformanceMonitor() => _instance;
PerformanceMonitor._internal();
final Map<String, Stopwatch> _timers = {};
void startTimer(String operation) {
_timers[operation] = Stopwatch()..start();
}
void stopTimer(String operation) {
final timer = _timers[operation];
if (timer != null) {
timer.stop();
final duration = timer.elapsedMilliseconds;
_logPerformance(operation, duration);
_timers.remove(operation);
}
}
void _logPerformance(String operation, int durationMs) {
print('Performance: $operation took ${durationMs}ms');
// Report to analytics service
// FirebaseAnalytics.instance.logEvent(
// name: 'performance_metric',
// parameters: {
// 'operation': operation,
// 'duration_ms': durationMs,
// },
// );
}
Future<T> measureAsync<T>(String operation, Future<T> Function() function) async {
startTimer(operation);
try {
return await function();
} finally {
stopTimer(operation);
}
}
}
// Usage
class PostService {
Future<List<Post>> getPosts() async {
return PerformanceMonitor().measureAsync('get_posts', () async {
final response = await _collection.get();
return response.data.map((json) => Post.fromJson(json)).toList();
});
}
}
2. Usage Analytics¶
class AnalyticsService {
static final AnalyticsService _instance = AnalyticsService._internal();
factory AnalyticsService() => _instance;
AnalyticsService._internal();
void trackEvent(String eventName, {Map<String, dynamic>? parameters}) {
print('Analytics: $eventName ${parameters ?? ''}');
// Send to analytics service
// FirebaseAnalytics.instance.logEvent(
// name: eventName,
// parameters: parameters,
// );
}
void trackScreenView(String screenName) {
trackEvent('screen_view', parameters: {'screen_name': screenName});
}
void trackUserAction(String action, {Map<String, dynamic>? context}) {
trackEvent('user_action', parameters: {
'action': action,
...?context,
});
}
void trackError(String error, {String? context}) {
trackEvent('error_occurred', parameters: {
'error': error,
if (context != null) 'context': context,
});
}
}
// Usage in widgets
class PostListScreen extends StatefulWidget {
@override
_PostListScreenState createState() => _PostListScreenState();
}
class _PostListScreenState extends State<PostListScreen> {
@override
void initState() {
super.initState();
AnalyticsService().trackScreenView('post_list');
}
void _onPostTapped(Post post) {
AnalyticsService().trackUserAction('post_tapped', context: {
'post_id': post.id,
'post_title': post.title,
});
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PostDetailScreen(post: post)),
);
}
}
Deployment Considerations¶
1. Environment Configuration¶
// config/environment.dart
enum Environment { development, staging, production }
class EnvironmentConfig {
static const Environment _environment = Environment.production; // Set via build flags
static Environment get environment => _environment;
static String get strapiUrl {
switch (_environment) {
case Environment.development:
return 'http://localhost:1337';
case Environment.staging:
return 'https://staging-api.yourapp.com';
case Environment.production:
return 'https://api.yourapp.com';
}
}
static bool get enableLogging => _environment != Environment.production;
static Duration get cacheTimeout {
switch (_environment) {
case Environment.development:
return Duration(minutes: 1);
case Environment.staging:
return Duration(minutes: 5);
case Environment.production:
return Duration(minutes: 30);
}
}
}
2. Build Configuration¶
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize LDK with environment-specific configuration
await LDK.initialize(
baseUrl: EnvironmentConfig.strapiUrl,
enableLogging: EnvironmentConfig.enableLogging,
cacheConfig: {
'strategies': {
'default': {
'ttl': EnvironmentConfig.cacheTimeout.inMilliseconds,
'maxSize': 100,
'evictionPolicy': 'lru',
},
},
},
);
runApp(MyApp());
}
Summary¶
Following these best practices will help you build robust, scalable, and maintainable applications with LDK:
- Architecture: Use service layers and repositories for clean separation of concerns
- Performance: Implement efficient data loading, caching, and optimistic updates
- Error Handling: Create comprehensive error handling with retry logic
- Security: Implement secure token management and input validation
- Testing: Write unit and integration tests for critical functionality
- Monitoring: Track performance and usage analytics
- Deployment: Use environment-specific configurations
These patterns will help you leverage LDK's full potential while maintaining code quality and user experience.