Топ-20 Питань на Співбесіді з Flutter для Мобільних Розробників
Підготовка до співбесіди з Flutter: 20 найпоширеніших питань. Віджети, управління станом, Dart, архітектура та найкращі практики з детальними поясненнями та прикладами коду.

Співбесіди з Flutter перевіряють рівень володіння фреймворком, мовою Dart та патернами мобільної архітектури. Цей посібник охоплює 20 найчастіших питань — від основ до просунутих концепцій — з детальними відповідями та прикладами коду.
Інтерв'юери цінують кандидатів, які пояснюють "чому", а не лише "як". Для кожної концепції розуміння сценаріїв використання та технічних компромісів створює вирішальну різницю.
Основні Питання про Flutter та Dart
1. Яка різниця між StatelessWidget і StatefulWidget?
StatelessWidget — це незмінний віджет, зовнішній вигляд якого залежить виключно від початкової конфігурації. Після побудови він ніколи не змінюється. StatefulWidget зберігає змінний стан, що може еволюціонувати з часом, ініціюючи перебудову віджета.
// StatelessWidget: static display, no change after construction
class WelcomeMessage extends StatelessWidget {
// Final parameter - never changes
final String username;
const WelcomeMessage({super.key, required this.username});
Widget build(BuildContext context) {
// Build called once (unless parent rebuilds)
return Text('Welcome, $username');
}
}// StatefulWidget: mutable state, can rebuild itself
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
// Mutable local state
int _likeCount = 0;
void _incrementLike() {
// setState triggers rebuild with new state
setState(() {
_likeCount++;
});
}
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _incrementLike,
child: Text('Likes: $_likeCount'),
);
}
}Правило таке: StatelessWidget використовується за замовчуванням, StatefulWidget — лише для локального стану віджета.
2. Як працює дерево віджетів Flutter?
Flutter організовує інтерфейс у три взаємопов'язані дерева: Widget Tree (незмінна декларація), Element Tree (життєвий цикл і прив'язка) та Render Tree (компонування і малювання).
// Declarative structure - widgets describe the UI
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
// Each widget creates an Element in the Element Tree
return MaterialApp(
home: Scaffold(
// Scaffold creates multiple RenderObjects
body: Center(
// Center modifies its child's layout
child: Column(
children: [
// Each Text has its own RenderParagraph
Text('First'),
Text('Second'),
],
),
),
),
);
}
}При виклику setState Flutter порівнює старе та нове дерева віджетів, щоб перебудувати лише змінені елементи. Це порівняння базується на ключах та типах віджетів.
3. Що таке const конструктор і навіщо його використовувати?
Const конструктори створюють віджети під час компіляції, а не під час виконання. Flutter може повторно використовувати ці екземпляри, покращуючи продуктивність за рахунок уникнення зайвих перебудов.
class OptimizedScreen extends StatelessWidget {
const OptimizedScreen({super.key});
Widget build(BuildContext context) {
return Column(
children: [
// ✅ const: same instance reused on each build
const Icon(Icons.star, size: 48),
const SizedBox(height: 16),
const Text('Static title'),
// ❌ Non-const: new instance on each build
Icon(Icons.star, color: Theme.of(context).primaryColor),
],
);
}
}
// Custom widget with const constructor
class StaticCard extends StatelessWidget {
final String title;
// All fields must be final for const
const StaticCard({super.key, required this.title});
Widget build(BuildContext context) {
return Card(child: Text(title));
}
}Аналізатор Flutter вказує на пропущені можливості за допомогою лінт-правила prefer_const_constructors.
4. Поясніть різні типи Keys у Flutter
Keys зберігають стан віджета під час реорганізацій. Без ключів Flutter покладається на позицію в дереві, що може спричинити баги при перестановці елементів списку.
class TodoList extends StatelessWidget {
final List<Todo> todos;
const TodoList({super.key, required this.todos});
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
// ✅ ValueKey: preserves state if order changes
return TodoTile(
key: ValueKey(todo.id),
todo: todo,
);
},
);
}
}
// Different key types for different contexts
class KeyExamples extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
// ValueKey: based on a unique value
Container(key: ValueKey('unique-id')),
// ObjectKey: based on object identity
Container(key: ObjectKey(myObject)),
// UniqueKey: new key on each build
Container(key: UniqueKey()),
// GlobalKey: access state from outside
Form(key: _formKey),
],
);
}
}GlobalKeys також дозволяють отримати доступ до стану віджета з будь-якого місця, але їх варто використовувати помірно.
Питання про Dart
5. Яка різниця між final і const у Dart?
final визначає змінну, якій можна присвоїти значення лише один раз; обчислюється під час виконання. const створює константу часу компіляції — незмінну та обчислену до виконання.
class DateExample {
// final: value assigned at runtime
final DateTime createdAt = DateTime.now();
// const: value known at compile time
static const int maxItems = 100;
static const String appName = 'MyApp';
// ❌ Error: DateTime.now() not const (runtime value)
// static const DateTime timestamp = DateTime.now();
}
void demonstrateDifference() {
// final: each call can have different value
final timestamp1 = DateTime.now();
final timestamp2 = DateTime.now();
print(timestamp1 == timestamp2); // false (different)
// const: same instance reused
const list1 = [1, 2, 3];
const list2 = [1, 2, 3];
print(identical(list1, list2)); // true (same instance)
}У Flutter рекомендовано використовувати const для статичних віджетів і final для обчислюваних значень.
6. Як працюють Future та async/await?
Future представляє значення, яке стане доступним пізніше. async/await забезпечує зручний синтаксис для обробки асинхронних операцій без вкладених зворотних викликів.
class UserRepository {
final ApiClient _client;
UserRepository(this._client);
// Future: promise of a future value
Future<User> fetchUser(String id) async {
try {
// await suspends execution until resolution
final response = await _client.get('/users/$id');
return User.fromJson(response);
} catch (e) {
// Errors propagate normally with async/await
throw UserNotFoundException(id);
}
}
// Parallel execution with Future.wait
Future<List<User>> fetchUsers(List<String> ids) async {
// All requests start simultaneously
final futures = ids.map((id) => fetchUser(id));
// Wait for all to complete
return Future.wait(futures);
}
// Sequential processing
Future<void> processSequentially(List<String> ids) async {
for (final id in ids) {
// Each request waits for the previous one
await fetchUser(id);
}
}
}Future.wait — для паралелізму, асинхронні цикли — для послідовної обробки.
FutureBuilder підходить для простих випадків, проте Riverpod (AsyncValue) забезпечує краще управління кешем, обробку помилок та можливості оновлення для складних застосунків.
7. Поясніть Streams та їх використання
Streams представляють асинхронні послідовності значень, ідеальні для подій у реальному часі: WebSockets, дані сенсорів або взаємодія з користувачем.
class MessageService {
// StreamController manages creation and broadcasting
final _messageController = StreamController<Message>.broadcast();
// Expose only the Stream (not the Sink)
Stream<Message> get messages => _messageController.stream;
void addMessage(Message message) {
_messageController.sink.add(message);
}
void dispose() {
_messageController.close();
}
}
// Usage with StreamBuilder
class MessageList extends StatelessWidget {
final MessageService service;
const MessageList({super.key, required this.service});
Widget build(BuildContext context) {
return StreamBuilder<Message>(
stream: service.messages,
builder: (context, snapshot) {
// Handle all possible states
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('No messages');
}
return MessageCard(message: snapshot.data!);
},
);
}
}.broadcast() Streams дозволяють кілька слухачів, на відміну від Streams з одноразовою підпискою.
Готовий до співбесід з Flutter?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Питання про Архітектуру та Управління Станом
8. Порівняйте рішення для управління станом: Provider, Riverpod, Bloc
Кожне рішення відповідає різним потребам. Provider пропонує простоту та нативну інтеграцію. Riverpod забезпечує типобезпеку та тестованість. Bloc нав'язує сувору подієво-орієнтовану архітектуру.
// Riverpod: declarative and type-safe approach
final userProvider = FutureProvider.autoDispose<User>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchCurrentUser();
});
class UserProfile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// AsyncValue handles loading/error/data
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (user) => Text(user.displayName),
);
}
}// Bloc: explicit events/states separation
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested(this.email, this.password);
}
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
final User user;
AuthSuccess(this.user);
}
class AuthFailure extends AuthState {
final String error;
AuthFailure(this.error);
}
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await _repository.login(event.email, event.password);
emit(AuthSuccess(user));
} catch (e) {
emit(AuthFailure(e.toString()));
}
}
}Riverpod рекомендується для нових проєктів завдяки сучасному API та зручності тестування.
9. Що таке Clean Architecture у Flutter?
Clean Architecture розділяє код на незалежні шари: Domain (бізнес-логіка), Data (джерела даних) та Presentation (UI). Таке розділення спрощує тестування та підтримку.
// Entity: pure business object, no framework dependency
class User {
final String id;
final String email;
final String name;
const User({
required this.id,
required this.email,
required this.name,
});
}
// domain/repositories/user_repository.dart
// Interface: abstract contract, implementation in Data
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> updateUser(User user);
}
// domain/usecases/get_user_usecase.dart
// Use case: isolated business logic
class GetUserUseCase {
final UserRepository _repository;
GetUserUseCase(this._repository);
Future<User> call(String userId) {
return _repository.getUser(userId);
}
}// Concrete implementation with data sources
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource _remoteDataSource;
final UserLocalDataSource _localDataSource;
UserRepositoryImpl(this._remoteDataSource, this._localDataSource);
Future<User> getUser(String id) async {
try {
// Try local cache first
final cachedUser = await _localDataSource.getUser(id);
if (cachedUser != null) return cachedUser;
} catch (_) {}
// Fallback to API
final user = await _remoteDataSource.fetchUser(id);
await _localDataSource.cacheUser(user);
return user;
}
}Шар Domain не залежить від нічого, Data залежить від Domain, Presentation залежить від обох.
10. Як реалізувати ін'єкцію залежностей?
Ін'єкція залежностей розв'язує компоненти, надаючи їхні залежності ззовні. Riverpod чудово справляється з цим завдяки провайдерам.
// Provider definitions (dependencies)
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(baseUrl: Environment.apiUrl);
});
final userRepositoryProvider = Provider<UserRepository>((ref) {
// Automatically injects ApiClient
final client = ref.watch(apiClientProvider);
return UserRepositoryImpl(client);
});
final getUserUseCaseProvider = Provider<GetUserUseCase>((ref) {
final repository = ref.watch(userRepositoryProvider);
return GetUserUseCase(repository);
});
// Usage in a widget
class UserScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final getUserUseCase = ref.watch(getUserUseCaseProvider);
// Use the use case...
}
}
// Tests: easy dependency override
void main() {
testWidgets('displays user', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Replace with mock for tests
userRepositoryProvider.overrideWithValue(MockUserRepository()),
],
child: const MyApp(),
),
);
});
}Головна перевага: тести можуть замінити будь-яку залежність мок-об'єктом.
Питання про Продуктивність
11. Як оптимізувати продуктивність перебудов?
Мінімізація перебудов покращує продуктивність. Ключові техніки: const віджети, гранулярне розділення та селектори Riverpod.
// ❌ Bad: everything rebuilds on each change
class BadExample extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return Column(
children: [
Text(user.name),
Text(user.email),
const ExpensiveWidget(), // Rebuilds unnecessarily
],
);
}
}
// ✅ Good: selector to rebuild only if name changes
class GoodExample extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// select: rebuilds only if name changes
final name = ref.watch(userProvider.select((u) => u.name));
return Text(name);
}
}
// ✅ Good: splitting into smaller widgets
class OptimizedScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
const Header(), // Static, never rebuilds
const UserNameWidget(), // Rebuilds if name changes
const UserEmailWidget(), // Rebuilds if email changes
const Footer(), // Static
],
);
}
}DevTools Performance допомагає ідентифікувати надмірні перебудови.
12. Як оптимізувати довгі списки?
Довгі списки потребують відкладеного завантаження через ListView.builder. Для більш ніж 20 елементів слід уникати ListView з прямими дочірніми елементами.
class OptimizedList extends StatelessWidget {
final List<Item> items;
const OptimizedList({super.key, required this.items});
Widget build(BuildContext context) {
// ✅ ListView.builder: builds on demand
return ListView.builder(
// Fixed height improves scrolling
itemExtent: 72,
itemCount: items.length,
itemBuilder: (context, index) {
return ItemTile(
// Key to preserve state during scroll
key: ValueKey(items[index].id),
item: items[index],
);
},
);
}
}
// List widget with infinite loading
class InfiniteList extends ConsumerStatefulWidget {
ConsumerState<InfiniteList> createState() => _InfiniteListState();
}
class _InfiniteListState extends ConsumerState<InfiniteList> {
final _scrollController = ScrollController();
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
// Load more data near the end
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
ref.read(itemsProvider.notifier).loadMore();
}
}
Widget build(BuildContext context) {
final items = ref.watch(itemsProvider);
return ListView.builder(
controller: _scrollController,
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
);
}
}Для дуже довгих списків (1000+ елементів) варто розглянути ListView.separated або спеціалізовані пакети на кшталт scrollable_positioned_list.
Для списків із зображеннями слід використовувати cached_network_image, щоб уникнути повторних завантажень. Некешовані зображення спричиняють гальмування при прокрутці.
13. Поясніть, як працює рушій рендерингу Impeller
Impeller замінює Skia як рушій рендерингу за замовчуванням (Flutter 3.16+). Він попередньо компілює шейдери, щоб усунути "jank" при першому відображенні.
// Complex animations benefit from Impeller
class SmoothAnimation extends StatefulWidget {
State<SmoothAnimation> createState() => _SmoothAnimationState();
}
class _SmoothAnimationState extends State<SmoothAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// Impeller: no runtime shader compilation
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
child: const FlutterLogo(size: 100),
);
}
}Impeller увімкнено за замовчуванням на iOS. На Android сумісність перевіряється командою flutter run --enable-impeller.
Питання про Навігацію та Форми
14. Як реалізувати навігацію з deep linking?
Deep linking дозволяє відкривати застосунок на конкретному екрані за URL. GoRouter нативно підтримує цю функціональність.
final router = GoRouter(
routes: [
GoRoute(
path: '/products/:productId',
builder: (context, state) {
// Extract URL parameter
final productId = state.pathParameters['productId']!;
return ProductScreen(productId: productId);
},
),
GoRoute(
path: '/search',
builder: (context, state) {
// Query parameters (?query=flutter)
final query = state.uri.queryParameters['query'] ?? '';
return SearchScreen(initialQuery: query);
},
),
],
);
// Programmatic navigation
void navigateToProduct(BuildContext context, String id) {
// go: replaces navigation stack
context.go('/products/$id');
// push: adds to stack (allows back)
context.push('/products/$id');
// pushNamed with extra for complex data
context.pushNamed(
'productDetail',
pathParameters: {'productId': id},
extra: ProductData(id: id),
);
}Для системного deep linking необхідно налаштувати нативні файли (AndroidManifest.xml, Info.plist).
15. Як ефективно валідувати форми?
Валідація поєднує Form, TextFormField та кастомні валідатори. Zod-подібні пакети, такі як formz, додають структурованість.
class RegistrationForm extends StatefulWidget {
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmController = TextEditingController();
Widget build(BuildContext context) {
return Form(
key: _formKey,
// Real-time validation
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: _validateEmail,
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: _validatePassword,
),
TextFormField(
controller: _confirmController,
decoration: const InputDecoration(labelText: 'Confirm'),
obscureText: true,
validator: _validateConfirmPassword,
),
ElevatedButton(
onPressed: _submit,
child: const Text('Create account'),
),
],
),
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Invalid email format';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.length < 8) {
return 'Minimum 8 characters';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'At least one uppercase letter required';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'At least one number required';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
}
void _submit() {
if (_formKey.currentState!.validate()) {
// Form is valid, proceed
}
}
}Режим AutovalidateMode.onUserInteraction забезпечує найкращий UX: помилки з'являються лише після взаємодії користувача.
Питання про Тестування
16. Як тестувати віджети Flutter?
Тести віджетів перевіряють UI без залежності від пристрою. Пакет flutter_test надає необхідні інструменти.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
testWidgets('displays welcome message', (tester) async {
// Arrange: build the widget
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: WelcomeScreen(username: 'Alice'),
),
),
);
// Assert: verify content
expect(find.text('Welcome, Alice'), findsOneWidget);
});
testWidgets('button increments counter', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterScreen()),
);
// Initial state
expect(find.text('0'), findsOneWidget);
// Act: tap the button
await tester.tap(find.byType(ElevatedButton));
await tester.pump(); // Rebuild after setState
// Assert: counter incremented
expect(find.text('1'), findsOneWidget);
});
testWidgets('form validates email', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: LoginForm()),
);
// Enter invalid email
await tester.enterText(
find.byKey(const Key('email-field')),
'invalid-email',
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify error message
expect(find.text('Invalid email format'), findsOneWidget);
});
}pump() просуває один кадр, pumpAndSettle() очікує завершення всіх анімацій.
17. Як тестувати провайдери Riverpod?
Riverpod спрощує тестування за допомогою ProviderContainer та перевизначень.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
// Repository mock
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepository;
late ProviderContainer container;
setUp(() {
mockRepository = MockUserRepository();
container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
);
});
tearDown(() {
container.dispose();
});
test('loads user from repository', () async {
// Arrange
final expectedUser = User(id: '1', name: 'Test');
when(() => mockRepository.getUser('1'))
.thenAnswer((_) async => expectedUser);
// Act
final user = await container.read(userProvider('1').future);
// Assert
expect(user, expectedUser);
verify(() => mockRepository.getUser('1')).called(1);
});
test('handles repository error', () async {
when(() => mockRepository.getUser(any()))
.thenThrow(Exception('Network error'));
expect(
() => container.read(userProvider('1').future),
throwsException,
);
});
}Тести провайдерів працюють швидко, оскільки не потребують рендерингу UI.
Питання про Деплой
18. Як керувати різними середовищами (dev, staging, prod)?
Середовища налаштовуються через .env файли або константи часу компіляції за допомогою --dart-define.
enum Environment { dev, staging, prod }
class AppConfig {
final Environment environment;
final String apiUrl;
final bool enableAnalytics;
const AppConfig._({
required this.environment,
required this.apiUrl,
required this.enableAnalytics,
});
// Predefined configurations
static const dev = AppConfig._(
environment: Environment.dev,
apiUrl: 'https://api-dev.example.com',
enableAnalytics: false,
);
static const staging = AppConfig._(
environment: Environment.staging,
apiUrl: 'https://api-staging.example.com',
enableAnalytics: true,
);
static const prod = AppConfig._(
environment: Environment.prod,
apiUrl: 'https://api.example.com',
enableAnalytics: true,
);
// Read from --dart-define
static AppConfig fromEnvironment() {
const env = String.fromEnvironment('ENV', defaultValue: 'dev');
switch (env) {
case 'prod':
return prod;
case 'staging':
return staging;
default:
return dev;
}
}
}# terminal
# Launch with specific environment
flutter run --dart-define=ENV=staging
# Production build
flutter build apk --dart-define=ENV=prod --release19. Як реалізувати flavoring для кількох варіантів застосунку?
Flavoring створює кілька варіантів (client1, client2) з окремими конфігураціями (іконка, назва, API).
import 'package:flutter/material.dart';
import 'config/flavor_config.dart';
import 'app.dart';
void main() {
FlavorConfig(
flavor: Flavor.client1,
name: 'App Client 1',
apiUrl: 'https://api.client1.com',
primaryColor: Colors.blue,
);
runApp(const App());
}
// config/flavor_config.dart
enum Flavor { client1, client2, internal }
class FlavorConfig {
final Flavor flavor;
final String name;
final String apiUrl;
final Color primaryColor;
// Singleton for global access
static FlavorConfig? _instance;
static FlavorConfig get instance => _instance!;
FlavorConfig({
required this.flavor,
required this.name,
required this.apiUrl,
required this.primaryColor,
}) {
_instance = this;
}
bool get isProduction => flavor != Flavor.internal;
}Для кожного флейвору необхідно налаштувати нативні файли Android (build.gradle) та iOS (xcconfig).
20. Які найкращі практики безпеки Flutter?
Безпека охоплює зберігання чутливих даних, валідацію вводу та захист від зворотного інжинірингу.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
// Encrypted storage for sensitive data
final _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
Future<void> saveToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
Future<String?> getToken() async {
return _storage.read(key: 'auth_token');
}
Future<void> deleteToken() async {
await _storage.delete(key: 'auth_token');
}
}
// Input validation
class InputValidator {
// Injection prevention
static String sanitize(String input) {
return input
.replaceAll(RegExp(r'[<>"\']'), '')
.trim();
}
// Length validation
static bool isValidLength(String input, int min, int max) {
return input.length >= min && input.length <= max;
}
}
// SSL pinning protection
class SecureApiClient {
Dio createSecureClient() {
final dio = Dio();
// Add certificate pinning
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) {
// Verify certificate fingerprint
return _isValidCertificate(cert);
};
return client;
};
return dio;
}
}Секрети ніколи не зберігаються в коді. Для токенів використовується flutter_secure_storage, а для продакшену — прапорець --obfuscate.
Висновок
Ці 20 питань охоплюють ключові аспекти співбесід з Flutter: основи фреймворку, володіння Dart, патерни архітектури та найкращі практики для продакшену. Успіх полягає в глибокому розумінні внутрішніх механізмів, а не лише синтаксису.
Чеклист Підготовки
- ✅ Опанувати різницю між StatelessWidget/StatefulWidget та сценарії їх використання
- ✅ Розуміти дерево віджетів та оптимізацію перебудов
- ✅ Практикувати async/await, Future та Streams на вправах
- ✅ Реалізувати проєкт із Riverpod для управління станом
- ✅ Знати GoRouter та deep linking
- ✅ Писати юніт-тести та тести віджетів
- ✅ Розуміти налаштування кількох середовищ
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Регулярна практика на власних проєктах залишається найкращим способом закріплення цих знань. Кожне розглянуте питання заслуговує поглибленого вивчення з реальним кодом для опанування тонкощів фреймворку Flutter.
Теги
Поділитися
Пов'язані статті

Флаттер: створення першого кросплатформного застосунку
Повний посібник зі створення мобільного кросплатформного застосунку з Flutter та Dart. Віджети, керування станом, навігація та кращі практики для початківців.

Управління станом у Flutter у 2026 році: Riverpod vs Bloc vs GetX
Практичне порівняння рішень для управління станом у Flutter у 2026 році. Riverpod 3.0, Bloc 9.0 та GetX оцінені на основі реальних прикладів коду, продуктивності та стратегій міграції.

25 запитань на співбесіді з Laravel та PHP у 2026 році
25 найпоширеніших запитань на співбесіді з Laravel: Service Container, Eloquent ORM, middleware, черги, безпека, тестування та архітектурні патерни з прикладами коду.