engineering

Flutter Cross-Platform Development: Building Beautiful Mobile Apps

Master Flutter development with modern patterns, state management, and best practices for building high-performance iOS and Android applications.

S

Sambo Chea

Staff Engineer

October 5, 2025
9 min read

Flutter Cross-Platform Development: Building Beautiful Mobile Apps

Flutter has transformed mobile development at CUBIS. With a single codebase, we deliver pixel-perfect, high-performance applications for iOS, Android, Web, and Desktop. In this comprehensive guide, we'll share our battle-tested patterns and best practices.

Why Flutter?

Advantages We've Experienced

  • Single codebase - 90% code sharing across platforms
  • Hot reload - See changes instantly during development
  • Native performance - Compiled to ARM native code
  • Beautiful UI - Material Design and Cupertino widgets out of the box
  • Strong typing - Dart's type system catches errors at compile time
  • Growing ecosystem - 30,000+ packages on pub.dev

When to Choose Flutter

Perfect for:

  • Multi-platform apps (iOS + Android + Web)
  • MVP development with tight timelines
  • Apps requiring custom UI designs
  • Real-time interactive applications
  • Teams familiar with OOP languages

⚠️ Consider alternatives for:

  • Simple CRUD apps (React Native might be faster)
  • Heavy native integrations (consider native development)
  • Apps requiring bleeding-edge platform features

Project Setup

Installation

# macOS brew install flutter # Verify installation flutter doctor # Create new project flutter create my_app cd my_app # Run on iOS simulator flutter run

Project Structure

lib/
├── main.dart                 # Entry point
├── app/
│   ├── app.dart             # App widget
│   └── router.dart          # Navigation configuration
├── features/
│   ├── auth/
│   │   ├── models/
│   │   ├── providers/
│   │   ├── repositories/
│   │   ├── screens/
│   │   └── widgets/
│   └── products/
│       ├── models/
│       ├── providers/
│       ├── repositories/
│       ├── screens/
│       └── widgets/
├── core/
│   ├── theme/
│   ├── utils/
│   ├── constants/
│   └── widgets/
└── services/
    ├── api_service.dart
    ├── storage_service.dart
    └── notification_service.dart

State Management with Riverpod

Riverpod is our preferred state management solution. It's type-safe, testable, and handles dependency injection elegantly.

Setup

# pubspec.yaml dependencies: flutter: sdk: flutter flutter_riverpod: ^2.5.1 riverpod_annotation: ^2.3.5 dev_dependencies: riverpod_generator: ^2.4.0 build_runner: ^2.4.9

Providers

// lib/features/products/providers/products_provider.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../models/product.dart'; import '../repositories/products_repository.dart'; part 'products_provider.g.dart'; class Products extends _$Products { FutureOr<List<Product>> build() { return _fetchProducts(); } Future<List<Product>> _fetchProducts() async { final repository = ref.read(productsRepositoryProvider); return repository.getProducts(); } Future<void> addProduct(Product product) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final repository = ref.read(productsRepositoryProvider); await repository.addProduct(product); return _fetchProducts(); }); } Future<void> deleteProduct(String id) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final repository = ref.read(productsRepositoryProvider); await repository.deleteProduct(id); return _fetchProducts(); }); } }

Generate Code

dart run build_runner watch --delete-conflicting-outputs

Using Providers in Widgets

import 'package:flutter_riverpod/flutter_riverpod.dart'; class ProductsScreen extends ConsumerWidget { const ProductsScreen({super.key}); Widget build(BuildContext context, WidgetRef ref) { final productsAsync = ref.watch(productsProvider); return Scaffold( appBar: AppBar(title: const Text('Products')), body: productsAsync.when( data: (products) => ListView.builder( itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return ProductCard(product: product); }, ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => ErrorView(error: error), ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddProductDialog(context, ref), child: const Icon(Icons.add), ), ); } void _showAddProductDialog(BuildContext context, WidgetRef ref) { // Show dialog to add product } }

Clean Architecture Pattern

Models

// lib/features/products/models/product.dart import 'package:freezed_annotation/freezed_annotation.dart'; part 'product.freezed.dart'; part 'product.g.dart'; class Product with _$Product { const factory Product({ required String id, required String name, required String description, required double price, required String imageUrl, required String category, (true) bool inStock, ([]) List<String> tags, }) = _Product; factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json); }

Repository Pattern

// lib/features/products/repositories/products_repository.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../services/api_service.dart'; import '../models/product.dart'; part 'products_repository.g.dart'; abstract class ProductsRepository { Future<List<Product>> getProducts(); Future<Product> getProduct(String id); Future<void> addProduct(Product product); Future<void> updateProduct(Product product); Future<void> deleteProduct(String id); } class ProductsRepositoryImpl implements ProductsRepository { final ApiService _apiService; ProductsRepositoryImpl(this._apiService); Future<List<Product>> getProducts() async { final response = await _apiService.get('/products'); return (response['data'] as List) .map((json) => Product.fromJson(json)) .toList(); } Future<Product> getProduct(String id) async { final response = await _apiService.get('/products/$id'); return Product.fromJson(response['data']); } Future<void> addProduct(Product product) async { await _apiService.post('/products', product.toJson()); } Future<void> updateProduct(Product product) async { await _apiService.put('/products/${product.id}', product.toJson()); } Future<void> deleteProduct(String id) async { await _apiService.delete('/products/$id'); } } ProductsRepository productsRepository(ProductsRepositoryRef ref) { final apiService = ref.read(apiServiceProvider); return ProductsRepositoryImpl(apiService); }

Navigation with GoRouter

// lib/app/router.dart import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../features/auth/screens/login_screen.dart'; import '../features/products/screens/products_screen.dart'; import '../features/products/screens/product_detail_screen.dart'; part 'router.g.dart'; GoRouter router(RouterRef ref) { return GoRouter( initialLocation: '/products', routes: [ GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/products', builder: (context, state) => const ProductsScreen(), routes: [ GoRoute( path: ':id', builder: (context, state) { final id = state.pathParameters['id']!; return ProductDetailScreen(productId: id); }, ), ], ), ], redirect: (context, state) { final isAuthenticated = ref.read(authStateProvider).isAuthenticated; final isLoggingIn = state.uri.path == '/login'; if (!isAuthenticated && !isLoggingIn) { return '/login'; } if (isAuthenticated && isLoggingIn) { return '/products'; } return null; }, ); }

API Integration

HTTP Client with Dio

// lib/services/api_service.dart import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'api_service.g.dart'; class ApiService { final Dio _dio; ApiService(this._dio) { _dio.options.baseUrl = 'https://api.example.com/v1'; _dio.options.connectTimeout = const Duration(seconds: 10); _dio.options.receiveTimeout = const Duration(seconds: 10); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { // Add auth token final token = await _getAuthToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, onError: (error, handler) async { if (error.response?.statusCode == 401) { // Handle token refresh await _refreshToken(); return handler.resolve(await _retry(error.requestOptions)); } return handler.next(error); }, ), ); } Future<Map<String, dynamic>> get(String path, {Map<String, dynamic>? params}) async { final response = await _dio.get(path, queryParameters: params); return response.data; } Future<Map<String, dynamic>> post(String path, dynamic data) async { final response = await _dio.post(path, data: data); return response.data; } Future<Map<String, dynamic>> put(String path, dynamic data) async { final response = await _dio.put(path, data: data); return response.data; } Future<void> delete(String path) async { await _dio.delete(path); } Future<String?> _getAuthToken() async { // Get token from secure storage return null; } Future<void> _refreshToken() async { // Implement token refresh logic } Future<Response> _retry(RequestOptions options) async { return _dio.request( options.path, data: options.data, queryParameters: options.queryParameters, ); } } ApiService apiService(ApiServiceRef ref) { return ApiService(Dio()); }

Custom Widgets

Reusable Components

// lib/core/widgets/custom_button.dart import 'package:flutter/material.dart'; class CustomButton extends StatelessWidget { final String text; final VoidCallback? onPressed; final bool isLoading; final bool outlined; final IconData? icon; const CustomButton({ super.key, required this.text, this.onPressed, this.isLoading = false, this.outlined = false, this.icon, }); Widget build(BuildContext context) { final theme = Theme.of(context); Widget child = isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 20), const SizedBox(width: 8), ], Text(text), ], ); if (outlined) { return OutlinedButton( onPressed: isLoading ? null : onPressed, child: child, ); } return ElevatedButton( onPressed: isLoading ? null : onPressed, child: child, ); } }

Theme Management

// lib/core/theme/app_theme.dart import 'package:flutter/material.dart'; class AppTheme { static ThemeData lightTheme = ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF2563EB), brightness: Brightness.light, ), appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, ), cardTheme: CardTheme( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, ), ); static ThemeData darkTheme = ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF2563EB), brightness: Brightness.dark, ), appBarTheme: const AppBarTheme( centerTitle: true, elevation: 0, ), ); }

Testing

Unit Tests

// test/features/products/providers/products_provider_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:riverpod/riverpod.dart'; void main() { group('ProductsProvider', () { test('should fetch products successfully', () async { final container = ProviderContainer( overrides: [ productsRepositoryProvider.overrideWithValue( MockProductsRepository(), ), ], ); final products = await container.read(productsProvider.future); expect(products, isA<List<Product>>()); expect(products.length, greaterThan(0)); }); }); }

Widget Tests

// test/features/products/screens/products_screen_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('ProductsScreen displays products', (tester) async { await tester.pumpWidget( ProviderScope( child: MaterialApp( home: ProductsScreen(), ), ), ); // Wait for async data to load await tester.pumpAndSettle(); expect(find.byType(ProductCard), findsWidgets); }); }

Integration Tests

// integration_test/app_test.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(); testWidgets('complete user flow', (tester) async { app.main(); await tester.pumpAndSettle(); // Login await tester.enterText(find.byKey(Key('email')), 'test@example.com'); await tester.enterText(find.byKey(Key('password')), 'password'); await tester.tap(find.byKey(Key('loginButton'))); await tester.pumpAndSettle(); // Verify navigation to products screen expect(find.text('Products'), findsOneWidget); }); }

Performance Optimization

1. Use Const Constructors

// ✅ Good const Text('Hello'); // ❌ Bad Text('Hello');

2. Lazy Loading

ListView.builder( itemCount: items.length, itemBuilder: (context, index) => ItemCard(items[index]), );

3. Image Caching

CachedNetworkImage( imageUrl: product.imageUrl, placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.error), );

4. Debouncing

Timer? _debounce; void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { _performSearch(query); }); }

Production Deployment

Build Commands

# Android flutter build apk --release flutter build appbundle --release # iOS flutter build ipa --release # Web flutter build web --release

Flavors Configuration

// lib/main_dev.dart void main() { runApp(MyApp(environment: Environment.development)); } // lib/main_prod.dart void main() { runApp(MyApp(environment: Environment.production)); }
flutter run --flavor dev --target lib/main_dev.dart flutter run --flavor prod --target lib/main_prod.dart

Best Practices

  1. Use Riverpod for state management - Type-safe and testable
  2. Follow feature-based structure - Organize by feature, not by type
  3. Leverage code generation - Use freezed, json_serializable
  4. Write tests - Unit, widget, and integration tests
  5. Use const constructors - Improve performance
  6. Implement proper error handling - Don't let app crash
  7. Optimize images - Use appropriate formats and sizes
  8. Monitor performance - Use Flutter DevTools

Conclusion

Flutter enables us at CUBIS to deliver beautiful, performant cross-platform applications faster than ever. With modern patterns like Riverpod, clean architecture, and comprehensive testing, we build maintainable apps that scale.

Resources


Building a mobile app? Contact our engineering team:

Tags

#flutter#dart#mobile#cross-platform#ios#android#riverpod