Top 20 Flutter Interview Questions for Mobile Developers

Prepare for Flutter interviews with the 20 most common questions. Widgets, state management, Dart, architecture and best practices explained in detail.

Flutter interview questions for mobile developers

Flutter interviews assess mastery of the framework, Dart language, and mobile architecture patterns. This guide covers the 20 most frequently asked questions, from fundamentals to advanced concepts, with detailed answers and code examples.

Interview Advice

Interviewers appreciate candidates who explain "why" in addition to "how". For each concept, understanding use cases and technical trade-offs makes the difference.

Fundamental Flutter and Dart Questions

1. What is the difference between StatelessWidget and StatefulWidget?

StatelessWidget represents an immutable widget whose appearance depends solely on its initial configuration. Once built, it never changes. StatefulWidget maintains mutable state that can evolve over time, triggering widget rebuilds.

stateless_example.dartdart
// 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');
  }
}
stateful_example.dartdart
// 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'),
    );
  }
}

The rule: use StatelessWidget by default, StatefulWidget only for widget-local state.

2. How does Flutter's widget tree work?

Flutter organizes the interface into three interconnected trees: the Widget Tree (immutable declaration), the Element Tree (lifecycle and binding), and the Render Tree (layout and painting).

widget_tree_example.dartdart
// 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'),
            ],
          ),
        ),
      ),
    );
  }
}

When setState is called, Flutter compares the old and new widget trees to rebuild only the modified elements. This differentiation relies on keys and widget types.

3. What is a const constructor and why use it?

Const constructors create widgets at compile time rather than runtime. Flutter can reuse these instances, improving performance by avoiding unnecessary rebuilds.

const_example.dartdart
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));
  }
}

The Flutter analyzer flags missed opportunities with the prefer_const_constructors lint.

4. Explain the different types of Keys in Flutter

Keys preserve widget state during reorganizations. Without keys, Flutter relies on position in the tree, which can cause bugs when reordering lists.

keys_example.dartdart
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 also allow accessing a widget's state from anywhere, but should be used sparingly.

Dart Questions

5. What is the difference between final and const in Dart?

final defines a variable assignable only once, evaluated at runtime. const creates a compile-time constant, immutable and evaluated before execution.

final_const_example.dartdart
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)
}

In Flutter, prefer const for static widgets and final for computed values.

6. How do Futures and async/await work?

Future represents a value that will be available later. async/await provides readable syntax for handling these asynchronous operations without nested callbacks.

async_example.dartdart
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 for parallelism, async loops for sequential processing.

FutureBuilder vs Riverpod

FutureBuilder works for simple cases, but Riverpod (AsyncValue) offers better cache management, error handling and refresh capabilities for complex applications.

7. Explain Streams and their usage

Streams represent asynchronous sequences of values, ideal for real-time events: WebSockets, sensor data, or user interactions.

stream_example.dartdart
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 allow multiple listeners, unlike single-subscription Streams.

Ready to ace your Flutter interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Architecture and State Management Questions

8. Compare state management solutions: Provider, Riverpod, Bloc

Each solution addresses different needs. Provider offers simplicity and native integration. Riverpod brings type-safety and testability. Bloc enforces strict event-driven architecture.

riverpod_example.dartdart
// 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_example.dartdart
// 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 is recommended for new projects thanks to its modern API and ease of testing.

9. What is Clean Architecture in Flutter?

Clean Architecture separates code into independent layers: Domain (business logic), Data (data sources), and Presentation (UI). This separation facilitates testing and maintenance.

domain/entities/user.dartdart
// 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);
  }
}
data/repositories/user_repository_impl.dartdart
// 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;
  }
}

The Domain layer depends on nothing, Data depends on Domain, Presentation depends on both.

10. How to implement dependency injection?

Dependency injection decouples components by providing their dependencies from outside. Riverpod excels at this with its providers.

di_example.dartdart
// 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(),
      ),
    );
  });
}

The major advantage: tests can replace any dependency with a mock.

Performance Questions

11. How to optimize rebuild performance?

Minimizing rebuilds improves performance. Key techniques: const widgets, granular splitting, and Riverpod selectors.

rebuild_optimization.dartdart
// ❌ 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
      ],
    );
  }
}

Use DevTools Performance to identify excessive rebuilds.

12. How to optimize long lists?

Long lists require lazy loading with ListView.builder. Avoid ListView with direct children for more than 20 items.

list_optimization.dartdart
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]),
    );
  }
}

For very long lists (1000+ items), consider ListView.separated or specialized packages like scrollable_positioned_list.

Image caching

For lists with images, use cached_network_image to avoid reloads. Uncached images cause scroll stutters.

13. Explain how the Impeller rendering engine works

Impeller replaces Skia as the default rendering engine (Flutter 3.16+). It precompiles shaders to eliminate first-display "jank".

impeller_benefits.dartdart
// 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 is enabled by default on iOS. On Android, check compatibility with flutter run --enable-impeller.

14. How to handle navigation with deep linking?

Deep linking allows opening the app on a specific screen via URL. GoRouter natively handles this functionality.

deep_linking.dartdart
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),
  );
}

Configure native files (AndroidManifest.xml, Info.plist) to enable system deep linking.

15. How to validate forms effectively?

Validation combines Form, TextFormField and custom validators. Zod-like packages such as formz add structure.

form_validation.dartdart
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
    }
  }
}

The AutovalidateMode.onUserInteraction mode offers the best UX: errors shown after interaction.

Testing Questions

16. How to test Flutter widgets?

Widget tests verify UI without depending on a device. The flutter_test package provides necessary utilities.

widget_test.dartdart
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() advances one frame, pumpAndSettle() waits for all animations to complete.

17. How to test Riverpod providers?

Riverpod facilitates testing through ProviderContainer and overrides.

provider_test.dartdart
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,
    );
  });
}

Provider tests are fast because they don't require UI rendering.

Deployment Questions

18. How to manage different environments (dev, staging, prod)?

Environments are configured via .env files or compile-time constants with --dart-define.

environment.dartdart
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;
    }
  }
}
bash
# terminal
# Launch with specific environment
flutter run --dart-define=ENV=staging

# Production build
flutter build apk --dart-define=ENV=prod --release

19. How to implement flavoring for multiple app variants?

Flavoring creates multiple variants (client1, client2) with distinct configurations (icon, name, API).

main_client1.dartdart
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;
}

Configure native files Android (build.gradle) and iOS (xcconfig) for each flavor.

20. What are Flutter security best practices?

Security covers sensitive data storage, input validation, and protection against reverse engineering.

security_example.dartdart
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;
  }
}

Never store secrets in code. Use flutter_secure_storage for tokens and --obfuscate for production.

Conclusion

These 20 questions cover essential aspects of Flutter interviews: framework fundamentals, Dart mastery, architecture patterns, and production best practices. Success lies in deep understanding of underlying mechanisms, not just syntax.

Preparation Checklist

  • ✅ Master the StatelessWidget/StatefulWidget difference and use cases
  • ✅ Understand the widget tree and rebuild optimization
  • ✅ Practice async/await, Futures and Streams with exercises
  • ✅ Implement a project with Riverpod for state management
  • ✅ Know GoRouter and deep linking
  • ✅ Write unit tests and widget tests
  • ✅ Understand multi-environment configuration

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Regular practice on personal projects remains the best way to consolidate this knowledge. Each question covered here deserves deeper exploration with real code to master the subtleties of the Flutter framework.

Tags

#flutter
#dart
#interview
#mobile development
#technical interview

Share

Related articles