Best Practices2024-01-0810 min read
Building Cross-Platform Apps with Flutter: Best Practices
By Hasnain Makada
Building Cross-Platform Apps with Flutter: Best Practices
Flutter's promise of "write once, run anywhere" is compelling, but building truly successful cross-platform apps requires following established best practices. This guide covers the essential practices for creating high-quality Flutter applications.
1. Project Structure and Organization
Recommended Folder Structure
lib/
├── core/
│ ├── constants/
│ ├── errors/
│ ├── network/
│ └── utils/
├── features/
│ ├── authentication/
│ │ ├── data/
│ │ ├── domain/
│ │ └── presentation/
│ └── home/
│ ├── data/
│ ├── domain/
│ └── presentation/
├── shared/
│ ├── widgets/
│ ├── themes/
│ └── services/
└── main.dart
Feature-Based Architecture
Organize your code by features rather than by technical layers:
// Good - Feature-based
features/
├── user_profile/
│ ├── models/
│ ├── services/
│ ├── widgets/
│ └── screens/
// Avoid - Layer-based
├── models/
├── services/
├── widgets/
└── screens/
2. Responsive Design
Use Flexible Layouts
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget tablet;
final Widget desktop;
const ResponsiveLayout({
Key? key,
required this.mobile,
required this.tablet,
required this.desktop,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return mobile;
} else if (constraints.maxWidth < 1200) {
return tablet;
} else {
return desktop;
}
},
);
}
}
MediaQuery Best Practices
class AdaptiveButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isSmallScreen = screenWidth < 600;
return SizedBox(
width: isSmallScreen ? double.infinity : 200,
height: isSmallScreen ? 48 : 56,
child: ElevatedButton(
onPressed: onPressed,
child: Text(text),
),
);
}
}
3. Platform-Specific Adaptations
Use Platform-Aware Widgets
import 'dart:io';
Widget buildPlatformSpecificButton() {
if (Platform.isIOS) {
return CupertinoButton(
child: Text('iOS Button'),
onPressed: () {},
);
} else {
return ElevatedButton(
child: Text('Android Button'),
onPressed: () {},
);
}
}
Adaptive Widgets
import 'package:flutter/foundation.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class AdaptiveProgressIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
return CupertinoActivityIndicator();
case TargetPlatform.android:
default:
return CircularProgressIndicator();
}
}
}
4. Error Handling and Logging
Global Error Handling
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details);
// Send to crash reporting service
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(MyApp());
}
Structured Error Handling
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
Result.success(this.data) : error = null, isSuccess = true;
Result.error(this.error) : data = null, isSuccess = false;
}
class ApiService {
Future<Result<User>> getUser(String id) async {
try {
final response = await http.get(Uri.parse('/users/$id'));
if (response.statusCode == 200) {
final user = User.fromJson(json.decode(response.body));
return Result.success(user);
} else {
return Result.error('Failed to load user');
}
} catch (e) {
return Result.error('Network error: $e');
}
}
}
5. Testing Best Practices
Unit Testing
import 'package:flutter_test/flutter_test.dart';
void main() {
group('UserService', () {
late UserService userService;
late MockApiClient mockApiClient;
setUp(() {
mockApiClient = MockApiClient();
userService = UserService(mockApiClient);
});
test('should return user when API call is successful', () async {
// Arrange
const userId = '123';
final expectedUser = User(id: userId, name: 'John Doe');
when(mockApiClient.getUser(userId))
.thenAnswer((_) async => expectedUser);
// Act
final result = await userService.getUser(userId);
// Assert
expect(result.isSuccess, true);
expect(result.data, expectedUser);
});
});
}
Widget Testing
testWidgets('UserProfile displays user information', (tester) async {
// Arrange
const user = User(id: '1', name: 'John Doe', email: 'john@example.com');
// Act
await tester.pumpWidget(
MaterialApp(
home: UserProfile(user: user),
),
);
// Assert
expect(find.text('John Doe'), findsOneWidget);
expect(find.text('john@example.com'), findsOneWidget);
});
6. Performance Optimization
Lazy Loading and Pagination
class LazyListView extends StatefulWidget {
@override
_LazyListViewState createState() => _LazyListViewState();
}
class _LazyListViewState extends State<LazyListView> {
final List<Item> _items = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadMoreItems();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _items.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == _items.length) {
return Center(child: CircularProgressIndicator());
}
if (index >= _items.length - 5 && !_isLoading) {
_loadMoreItems();
}
return ListTile(title: Text(_items[index].title));
},
);
}
Future<void> _loadMoreItems() async {
if (_isLoading) return;
setState(() => _isLoading = true);
final newItems = await ApiService.getItems(
offset: _items.length,
limit: 20,
);
setState(() {
_items.addAll(newItems);
_isLoading = false;
});
}
}
7. Security Best Practices
Secure Storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static const _storage = FlutterSecureStorage();
static Future<void> storeToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
static Future<String?> getToken() async {
return await _storage.read(key: 'auth_token');
}
static Future<void> deleteToken() async {
await _storage.delete(key: 'auth_token');
}
}
Network Security
class ApiClient {
static final Dio _dio = Dio();
static void setupInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await SecureStorageService.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// Handle unauthorized access
_handleUnauthorized();
}
handler.next(error);
},
),
);
}
}
8. Accessibility
Making Your App Accessible
class AccessibleButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: 'Tap to $text',
child: ElevatedButton(
onPressed: onPressed,
child: Text(text),
),
);
}
}
Conclusion
Following these best practices will help you build robust, maintainable, and scalable cross-platform applications with Flutter. Remember to:
- Structure your project for maintainability
- Design responsively for all screen sizes
- Handle platform differences appropriately
- Implement comprehensive error handling
- Write thorough tests
- Optimize for performance
- Prioritize security
- Ensure accessibility
Start with these fundamentals and adapt them to your specific project needs. The key is consistency and continuous improvement based on user feedback and performance metrics.
Tags:
FlutterCross-PlatformBest PracticesMobile Development