# Флаттер: створення першого кросплатформного застосунку > Повний посібник зі створення мобільного кросплатформного застосунку з Flutter та Dart. Віджети, керування станом, навігація та кращі практики для початківців. - Published: 2026-01-20 - Updated: 2026-04-10 - Author: SharpSkill - Tags: flutter, dart, mobile development, cross-platform, android ios - Reading time: 18 min --- Flutter revolutionizes mobile development by enabling the creation of iOS and Android applications from a single codebase. This framework developed by Google combines native performance with exceptional productivity thanks to its Skia rendering engine and declarative widget system. This guide covers building a complete application, from installation to production best practices. > **Flutter 3.27 — Січень 2026** > > Flutter 3.27 приносить значні поліпшення: нативну підтримку Material 3 за замовчуванням, нові оптимізовані анімації Impeller та інтеграцію з Dart 3.6 з експериментальними макросами. Фреймворк тепер компілюється в нативний ARM64 для оптимальної продуктивності. ## Налаштування середовища та встановлення Налаштування Flutter потребує кількох кроків конфігурації. SDK Flutter містить усе необхідне: фреймворк, інструменти збірки та менеджер пакетів Dart. ```bash # terminal # Download Flutter SDK (macOS/Linux) git clone https://github.com/flutter/flutter.git -b stable export PATH="$PATH:`pwd`/flutter/bin" # Verify installation and dependencies flutter doctor # Create a new project flutter create --org com.example my_app cd my_app # Run in development mode flutter run ``` Команда `flutter doctor` перевіряє, що всі залежності встановлені: Android Studio, Xcode (macOS) та налаштовані емулятори. ```yaml # pubspec.yaml name: my_app description: Cross-platform Flutter application publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=3.6.0 <4.0.0' dependencies: flutter: sdk: flutter # UI and design cupertino_icons: ^1.0.8 google_fonts: ^6.2.1 # State management flutter_riverpod: ^2.6.1 # Navigation go_router: ^14.6.2 # HTTP and API dio: ^5.7.0 # Local storage shared_preferences: ^2.3.4 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 flutter: uses-material-design: true ``` Цей файл `pubspec.yaml` налаштовує основні залежності для сучасного застосунку Flutter. ## Архітектура проєкту Flutter Чітка структура проєкту спрощує підтримку та розвиток застосунку. Ця організація чітко розмежовує відповідальності. ``` lib/ ├── main.dart # Entry point ├── app.dart # App configuration ├── core/ │ ├── constants/ # Colors, dimensions, strings │ ├── theme/ # Material 3 theme │ └── utils/ # Utility functions ├── features/ │ ├── auth/ # Authentication feature │ │ ├── data/ # Repositories, data sources │ │ ├── domain/ # Models, use cases │ │ └── presentation/ # Screens, widgets, providers │ └── home/ # Home feature │ ├── data/ │ ├── domain/ │ └── presentation/ ├── shared/ │ ├── widgets/ # Reusable widgets │ └── providers/ # Shared providers └── routing/ └── app_router.dart # Route configuration ``` Ця архітектура "feature-first" групує весь код, пов'язаний з певною функціональністю, в одній теці, що спрощує навігацію та рефакторинг. > **Архітектура Feature-First** > > Підхід feature-first організовує код за бізнес-функціональністю, а не за технічним типом. Кожна feature містить власні моделі, віджети та логіку, що робить код більш модульним та придатним для тестування. ## Розуміння віджетів Flutter Віджети — це фундаментальні будівельні блоки Flutter. Все є віджетом: кнопки, текст, макети, навіть сам застосунок. Flutter надає два типи віджетів: StatelessWidget (без стану) та StatefulWidget (з локальним станом). ```dart // lib/shared/widgets/custom_button.dart import 'package:flutter/material.dart'; /// Custom reusable button widget throughout the application. /// Automatically handles loading and disabled states. class CustomButton extends StatelessWidget { // Required and optional widget parameters final String label; final VoidCallback? onPressed; final bool isLoading; final bool isOutlined; // Constructor with named parameters for clarity const CustomButton({ super.key, required this.label, this.onPressed, this.isLoading = false, this.isOutlined = false, }); @override Widget build(BuildContext context) { // Access theme for consistent styles final theme = Theme.of(context); // Conditional build based on button type if (isOutlined) { return OutlinedButton( // Disable button during loading onPressed: isLoading ? null : onPressed, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _buildChild(theme), ); } return FilledButton( onPressed: isLoading ? null : onPressed, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _buildChild(theme), ); } /// Builds button content with loading state handling. Widget _buildChild(ThemeData theme) { if (isLoading) { return const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ); } return Text(label); } } ``` Цей віджет інкапсулює логіку відображення кнопок з автоматичною обробкою стану завантаження. ```dart // lib/shared/widgets/user_card.dart import 'package:flutter/material.dart'; import '../../features/auth/domain/models/user.dart'; /// Card displaying user information. /// Uses Material 3 with consistent elevation and shape. class UserCard extends StatelessWidget { final User user; final VoidCallback? onTap; const UserCard({ super.key, required this.user, this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); // Material 3 Card with InkWell for ripple effect return Card( // Adaptive elevation based on theme elevation: 2, // Consistent rounded shape shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), // Clipping so ripple respects borders clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // Avatar with image or initials CircleAvatar( radius: 28, backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null, child: user.avatarUrl == null ? Text(user.initials) : null, ), const SizedBox(width: 16), // User information Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.displayName, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( user.email, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), // Navigation icon Icon( Icons.chevron_right, color: theme.colorScheme.onSurfaceVariant, ), ], ), ), ), ); } } ``` Композиція віджетів дозволяє будувати складні інтерфейси з простих та повторно використовуваних блоків. ## Керування станом з Riverpod Riverpod — це сучасне рішення для керування станом у Flutter. Ця бібліотека пропонує декларативний, типізований і тестований підхід до спільного використання стану між віджетами. ```dart // lib/features/auth/domain/models/user.dart /// Immutable user model with fromJson factory. class User { final String id; final String email; final String displayName; final String? avatarUrl; final DateTime createdAt; const User({ required this.id, required this.email, required this.displayName, this.avatarUrl, required this.createdAt, }); /// Generates initials from display name. String get initials { final parts = displayName.split(' '); if (parts.length >= 2) { return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); } return displayName.substring(0, 2).toUpperCase(); } /// Creates instance from JSON (API response). factory User.fromJson(Map json) { return User( id: json['id'] as String, email: json['email'] as String, displayName: json['display_name'] as String, avatarUrl: json['avatar_url'] as String?, createdAt: DateTime.parse(json['created_at'] as String), ); } /// Converts to JSON for API submission. Map toJson() { return { 'id': id, 'email': email, 'display_name': displayName, 'avatar_url': avatarUrl, 'created_at': createdAt.toIso8601String(), }; } } ``` ```dart // lib/features/auth/presentation/providers/auth_provider.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/repositories/auth_repository.dart'; import '../../domain/models/user.dart'; /// Authentication state representing different possible cases. sealed class AuthState { const AuthState(); } class AuthInitial extends AuthState { const AuthInitial(); } class AuthLoading extends AuthState { const AuthLoading(); } class AuthAuthenticated extends AuthState { final User user; const AuthAuthenticated(this.user); } class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); } class AuthError extends AuthState { final String message; const AuthError(this.message); } /// Provider for authentication repository. final authRepositoryProvider = Provider((ref) { return AuthRepository(); }); /// Main provider managing authentication state. final authProvider = StateNotifierProvider((ref) { final repository = ref.watch(authRepositoryProvider); return AuthNotifier(repository); }); /// Notifier handling authentication logic. class AuthNotifier extends StateNotifier { final AuthRepository _repository; AuthNotifier(this._repository) : super(const AuthInitial()) { // Check initial state on startup checkAuthStatus(); } /// Checks if a user is already logged in. Future checkAuthStatus() async { state = const AuthLoading(); try { final user = await _repository.getCurrentUser(); if (user != null) { state = AuthAuthenticated(user); } else { state = const AuthUnauthenticated(); } } catch (e) { state = const AuthUnauthenticated(); } } /// Signs in user with email and password. Future signIn(String email, String password) async { state = const AuthLoading(); try { final user = await _repository.signIn(email, password); state = AuthAuthenticated(user); } catch (e) { state = AuthError(e.toString()); } } /// Creates a new user account. Future signUp(String email, String password, String displayName) async { state = const AuthLoading(); try { final user = await _repository.signUp(email, password, displayName); state = AuthAuthenticated(user); } catch (e) { state = AuthError(e.toString()); } } /// Signs out the user. Future signOut() async { state = const AuthLoading(); await _repository.signOut(); state = const AuthUnauthenticated(); } } ``` Патерн sealed-класів (Dart 3.0+) гарантує, що всі випадки стану обробляються при використанні виразів switch. ## Навігація з GoRouter GoRouter спрощує декларативну навігацію у Flutter. Ця бібліотека керує маршрутами, параметрами, перенаправленнями та вкладеною навігацією. ```dart // lib/routing/app_router.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../features/auth/presentation/providers/auth_provider.dart'; import '../features/auth/presentation/screens/login_screen.dart'; import '../features/auth/presentation/screens/register_screen.dart'; import '../features/home/presentation/screens/home_screen.dart'; import '../features/home/presentation/screens/profile_screen.dart'; import '../shared/widgets/shell_scaffold.dart'; /// Provider for router with authentication handling. final routerProvider = Provider((ref) { // Listen to authentication state changes final authState = ref.watch(authProvider); return GoRouter( // Initial route initialLocation: '/', // Refresh router when auth changes refreshListenable: GoRouterRefreshStream(ref, authProvider), // Handle redirects based on authentication redirect: (context, state) { final isAuthenticated = authState is AuthAuthenticated; final isAuthRoute = state.matchedLocation.startsWith('/auth'); // Not authenticated on protected route → login if (!isAuthenticated && !isAuthRoute) { return '/auth/login'; } // Authenticated on auth route → home if (isAuthenticated && isAuthRoute) { return '/'; } return null; // No redirect }, routes: [ // Auth routes (without shell) GoRoute( path: '/auth/login', name: 'login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/auth/register', name: 'register', builder: (context, state) => const RegisterScreen(), ), // Protected routes with shell (bottom navigation) ShellRoute( builder: (context, state, child) { return ShellScaffold(child: child); }, routes: [ GoRoute( path: '/', name: 'home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/profile', name: 'profile', builder: (context, state) => const ProfileScreen(), ), GoRoute( path: '/profile/:userId', name: 'userProfile', builder: (context, state) { // Extract route parameter final userId = state.pathParameters['userId']!; return ProfileScreen(userId: userId); }, ), ], ), ], // Custom error page errorBuilder: (context, state) => Scaffold( body: Center( child: Text('Page not found: ${state.error}'), ), ), ); }); /// Stream to trigger router refresh. class GoRouterRefreshStream extends ChangeNotifier { GoRouterRefreshStream(Ref ref, StateNotifierProvider provider) { ref.listen(provider, (previous, next) { notifyListeners(); }); } } ``` Автоматичне перенаправлення на основі стану аутентифікації забезпечує недоступність захищених маршрутів для неаутентифікованих користувачів. ```dart // lib/shared/widgets/shell_scaffold.dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; /// Scaffold with bottom navigation bar for protected routes. class ShellScaffold extends StatelessWidget { final Widget child; const ShellScaffold({ super.key, required this.child, }); @override Widget build(BuildContext context) { return Scaffold( body: child, bottomNavigationBar: NavigationBar( // Determine active index based on route selectedIndex: _calculateSelectedIndex(context), onDestinationSelected: (index) => _onItemTapped(index, context), destinations: const [ NavigationDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home', ), NavigationDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile', ), ], ), ); } /// Calculates navigation index based on current route. int _calculateSelectedIndex(BuildContext context) { final location = GoRouterState.of(context).matchedLocation; if (location.startsWith('/profile')) return 1; return 0; } /// Navigates to route corresponding to index. void _onItemTapped(int index, BuildContext context) { switch (index) { case 0: context.goNamed('home'); case 1: context.goNamed('profile'); } } } ``` NavigationBar з Material 3 автоматично адаптується до світлої та темної тем. ## Екрани та форми Створення інтерактивних екранів поєднує віджети, керування станом та валідацію форм. ```dart // lib/features/auth/presentation/screens/login_screen.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../providers/auth_provider.dart'; import '../../../../shared/widgets/custom_button.dart'; /// Login screen with validated form. class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { // Global key for the form final _formKey = GlobalKey(); // Controllers for text fields final _emailController = TextEditingController(); final _passwordController = TextEditingController(); // Local state for password visibility bool _obscurePassword = true; @override void dispose() { // Resource cleanup _emailController.dispose(); _passwordController.dispose(); super.dispose(); } /// Submits the login form. Future _submit() async { // Validate all fields if (_formKey.currentState!.validate()) { await ref.read(authProvider.notifier).signIn( _emailController.text.trim(), _passwordController.text, ); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final authState = ref.watch(authProvider); final isLoading = authState is AuthLoading; // Listen for errors to display snackbar ref.listen(authProvider, (previous, next) { if (next is AuthError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(next.message), backgroundColor: theme.colorScheme.error, ), ); } }); return Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Logo or title Icon( Icons.flutter_dash, size: 80, color: theme.colorScheme.primary, ), const SizedBox(height: 16), Text( 'Sign In', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 32), // Email field with validation TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'Email', hintText: 'example@email.com', prefixIcon: Icon(Icons.email_outlined), ), validator: (value) { if (value == null || value.isEmpty) { return 'Email is required'; } // Basic email format validation if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') .hasMatch(value)) { return 'Invalid email format'; } return null; }, ), const SizedBox(height: 16), // Password field with visibility toggle TextFormField( controller: _passwordController, obscureText: _obscurePassword, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(), decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock_outlined), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Minimum 8 characters'; } return null; }, ), const SizedBox(height: 24), // Sign in button CustomButton( label: 'Sign In', isLoading: isLoading, onPressed: _submit, ), const SizedBox(height: 16), // Link to registration TextButton( onPressed: () => context.goNamed('register'), child: const Text('No account? Create one'), ), ], ), ), ), ), ), ); } } ``` Використання `ConsumerStatefulWidget` поєднує локальний стан (контролери форм) з глобальним станом (Riverpod). > **Керування контролерами** > > Інстанції `TextEditingController` повинні бути звільнені в методі `dispose()` для запобігання витоків пам'яті. Це правило стосується всіх контролерів та слухачів, створених вручну. ## HTTP-комунікація з Dio Dio надає потужний HTTP API з інтерцепторами, обробкою помилок та автоматичним перетворенням відповідей. ```dart // lib/core/network/api_client.dart import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../constants/api_constants.dart'; /// Provider for Dio HTTP client. final apiClientProvider = Provider((ref) { return ApiClient(); }); /// HTTP client configured with interceptors. class ApiClient { late final Dio _dio; ApiClient() { _dio = Dio( BaseOptions( baseUrl: ApiConstants.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); // Add interceptors _dio.interceptors.addAll([ _AuthInterceptor(), _LoggingInterceptor(), ]); } /// Generic GET request. Future get( String path, { Map? queryParameters, T Function(dynamic)? fromJson, }) async { try { final response = await _dio.get( path, queryParameters: queryParameters, ); if (fromJson != null) { return fromJson(response.data); } return response.data as T; } on DioException catch (e) { throw _handleError(e); } } /// Generic POST request. Future post( String path, { dynamic data, T Function(dynamic)? fromJson, }) async { try { final response = await _dio.post(path, data: data); if (fromJson != null) { return fromJson(response.data); } return response.data as T; } on DioException catch (e) { throw _handleError(e); } } /// Sets the authentication token. void setAuthToken(String token) { _dio.options.headers['Authorization'] = 'Bearer $token'; } /// Clears the authentication token. void clearAuthToken() { _dio.options.headers.remove('Authorization'); } /// Converts Dio errors to readable exceptions. Exception _handleError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return Exception('Connection timeout. Check your connection.'); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode; final message = error.response?.data['message'] ?? 'Server error'; return Exception('Error $statusCode: $message'); case DioExceptionType.cancel: return Exception('Request cancelled'); default: return Exception('Network error: ${error.message}'); } } } /// Interceptor to automatically add token. class _AuthInterceptor extends Interceptor { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) { // Token is added via setAuthToken() handler.next(options); } @override void onError( DioException err, ErrorInterceptorHandler handler, ) { // Handle 401 for token refresh if (err.response?.statusCode == 401) { // TODO: Implement token refresh } handler.next(err); } } /// Logging interceptor for development. class _LoggingInterceptor extends Interceptor { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) { print('→ ${options.method} ${options.path}'); handler.next(options); } @override void onResponse( Response response, ResponseInterceptorHandler handler, ) { print('← ${response.statusCode} ${response.requestOptions.path}'); handler.next(response); } @override void onError( DioException err, ErrorInterceptorHandler handler, ) { print('✗ ${err.response?.statusCode} ${err.requestOptions.path}'); handler.next(err); } } ``` Інтерцептори централізують логіку аутентифікації та логування без забруднення кожного запиту. ## Кастомна тема з Material 3 Консистентна тема забезпечує уніфікований користувацький досвід. Material 3 (Material You) адаптується до налаштувань системи. ```dart // lib/core/theme/app_theme.dart import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; /// Application theme configuration. class AppTheme { // Brand primary color static const _primaryColor = Color(0xFF6750A4); /// Material 3 light theme. static ThemeData get lightTheme { // Generate color scheme from primary color final colorScheme = ColorScheme.fromSeed( seedColor: _primaryColor, brightness: Brightness.light, ); return ThemeData( useMaterial3: true, colorScheme: colorScheme, // Custom typography textTheme: GoogleFonts.interTextTheme(), // AppBar configuration appBarTheme: AppBarTheme( centerTitle: true, elevation: 0, backgroundColor: colorScheme.surface, foregroundColor: colorScheme.onSurface, ), // Form field configuration inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colorScheme.primary, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colorScheme.error), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), // Button configuration filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 52), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 52), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), // Card configuration cardTheme: CardTheme( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), ); } /// Material 3 dark theme. static ThemeData get darkTheme { final colorScheme = ColorScheme.fromSeed( seedColor: _primaryColor, brightness: Brightness.dark, ); return ThemeData( useMaterial3: true, colorScheme: colorScheme, textTheme: GoogleFonts.interTextTheme( ThemeData.dark().textTheme, ), appBarTheme: AppBarTheme( centerTitle: true, elevation: 0, backgroundColor: colorScheme.surface, foregroundColor: colorScheme.onSurface, ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colorScheme.primary, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colorScheme.error), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 52), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 52), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), cardTheme: CardTheme( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), ); } } ``` `ColorScheme.fromSeed` автоматично генерує повну та доступну палітру з одного кольору. ## Конфігурація застосунку Точка входу застосунку ініціалізує Riverpod та налаштовує теми. ```dart // lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app.dart'; void main() { // Ensure Flutter bindings are initialized WidgetsFlutterBinding.ensureInitialized(); // Launch application with Riverpod scope runApp( const ProviderScope( child: App(), ), ); } ``` ```dart // lib/app.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/theme/app_theme.dart'; import 'routing/app_router.dart'; /// Root application widget. class App extends ConsumerWidget { const App({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // Get configured router final router = ref.watch(routerProvider); return MaterialApp.router( title: 'My Flutter App', debugShowCheckedModeBanner: false, // Theme configuration theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.system, // Router configuration routerConfig: router, ); } } ``` `ThemeMode.system` автоматично адаптує тему до налаштувань користувача. ## Висновок Flutter надає повну екосистему для створення високопродуктивних кросплатформних застосунків. Поєднання декларативних віджетів, Riverpod для керування станом та GoRouter для навігації дозволяє створювати підтримувані та масштабовані застосунки. ### Контрольний список для початку роботи з Flutter - ✅ Встановити Flutter SDK та налаштувати середовище за допомогою `flutter doctor` - ✅ Структурувати проєкт за features для кращої організації - ✅ Використовувати Riverpod для типізованого та безпечного керування станом - ✅ Налаштувати GoRouter з перенаправленнями аутентифікації - ✅ Створювати багаторазові та компоновані віджети - ✅ Реалізувати консистентну тему Material 3 - ✅ Централізувати HTTP-запити за допомогою Dio та інтерцепторів Декларативний підхід Flutter сприяє композиції та повторному використанню. Кожний віджет можна тестувати незалежно, а hot-reload значно прискорює цикл розробки. Ця міцна основа дозволяє розширювати застосунок до просунутих функцій, таких як складні анімації, автоматизоване тестування та розгортання в магазинах додатків. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/flutter/flutter-building-first-cross-platform-app