Top 20 Câu Hỏi Phỏng Vấn Flutter Dành Cho Lập Trình Viên Mobile

Chuẩn bị cho buổi phỏng vấn Flutter với 20 câu hỏi thường gặp nhất. Widget, quản lý state, Dart, kiến trúc và các best practice được giải thích chi tiết kèm code mẫu.

Câu hỏi phỏng vấn Flutter dành cho lập trình viên mobile

Buổi phỏng vấn Flutter đánh giá khả năng thành thạo framework, ngôn ngữ Dart và các mô hình kiến trúc mobile. Bài viết này tổng hợp 20 câu hỏi được hỏi thường xuyên nhất, từ kiến thức nền tảng đến các khái niệm nâng cao, kèm theo câu trả lời chi tiết và ví dụ code minh họa.

Mẹo Phỏng Vấn

Nhà tuyển dụng đánh giá cao ứng viên giải thích được "tại sao" bên cạnh "như thế nào". Với mỗi khái niệm, việc hiểu rõ các trường hợp sử dụng và sự đánh đổi về mặt kỹ thuật sẽ tạo nên sự khác biệt.

Câu Hỏi Cơ Bản về Flutter và Dart

1. Sự khác biệt giữa StatelessWidget và StatefulWidget là gì?

StatelessWidget đại diện cho một widget bất biến, giao diện của nó chỉ phụ thuộc vào cấu hình ban đầu. Sau khi được xây dựng, nó không bao giờ thay đổi. StatefulWidget duy trì state có thể biến đổi theo thời gian, kích hoạt việc rebuild widget.

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'),
    );
  }
}

Quy tắc chung: sử dụng StatelessWidget theo mặc định, chỉ dùng StatefulWidget khi cần quản lý state cục bộ trong widget.

2. Widget tree trong Flutter hoạt động như thế nào?

Flutter tổ chức giao diện thành ba cây liên kết với nhau: Widget Tree (khai báo bất biến), Element Tree (vòng đời và liên kết), và Render Tree (bố cục và vẽ giao diện).

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'),
            ],
          ),
        ),
      ),
    );
  }
}

Khi setState được gọi, Flutter so sánh cây widget cũ và mới để chỉ rebuild những phần tử đã thay đổi. Quá trình phân biệt này dựa trên key và kiểu widget.

3. Const constructor là gì và tại sao nên sử dụng?

Const constructor tạo widget tại thời điểm biên dịch thay vì thời điểm chạy. Flutter có thể tái sử dụng các instance này, cải thiện hiệu suất bằng cách tránh các lần rebuild không cần thiết.

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));
  }
}

Flutter analyzer sẽ cảnh báo các cơ hội bị bỏ lỡ thông qua lint rule prefer_const_constructors.

4. Giải thích các loại Key khác nhau trong Flutter

Key giữ nguyên state của widget trong quá trình sắp xếp lại. Khi không có key, Flutter dựa vào vị trí trong cây, điều này có thể gây ra lỗi khi thay đổi thứ tự danh sách.

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

GlobalKey cũng cho phép truy cập state của widget từ bất kỳ đâu, nhưng cần được sử dụng một cách tiết kiệm.

Câu Hỏi về Dart

5. Sự khác biệt giữa final và const trong Dart là gì?

final khai báo một biến chỉ được gán giá trị một lần, được đánh giá tại thời điểm chạy. const tạo một hằng số tại thời điểm biên dịch, bất biến và được đánh giá trước khi thực thi.

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)
}

Trong Flutter, ưu tiên sử dụng const cho các widget tĩnh và final cho các giá trị được tính toán.

6. Future và async/await hoạt động như thế nào?

Future đại diện cho một giá trị sẽ có sẵn trong tương lai. async/await cung cấp cú pháp dễ đọc để xử lý các thao tác bất đồng bộ mà không cần callback lồng nhau.

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);
    }
  }
}

Sử dụng Future.wait cho xử lý song song, vòng lặp async cho xử lý tuần tự.

FutureBuilder vs Riverpod

FutureBuilder phù hợp cho các trường hợp đơn giản, nhưng Riverpod (AsyncValue) mang lại khả năng quản lý cache, xử lý lỗi và refresh tốt hơn cho các ứng dụng phức tạp.

7. Giải thích Stream và cách sử dụng

Stream đại diện cho chuỗi giá trị bất đồng bộ, lý tưởng cho các sự kiện thời gian thực: WebSocket, dữ liệu cảm biến, hoặc tương tác người dùng.

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!);
      },
    );
  }
}

Stream .broadcast() cho phép nhiều listener lắng nghe cùng lúc, khác với Stream đăng ký đơn (single-subscription).

Sẵn sàng chinh phục phỏng vấn Flutter?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Câu Hỏi về Kiến Trúc và Quản Lý State

8. So sánh các giải pháp quản lý state: Provider, Riverpod, Bloc

Mỗi giải pháp đáp ứng nhu cầu khác nhau. Provider mang lại sự đơn giản và tích hợp tự nhiên. Riverpod cung cấp type-safety và khả năng test tốt. Bloc áp dụng kiến trúc event-driven nghiêm ngặt.

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 được khuyến nghị cho các dự án mới nhờ API hiện đại và khả năng viết test dễ dàng.

9. Clean Architecture trong Flutter là gì?

Clean Architecture phân tách code thành các tầng độc lập: Domain (logic nghiệp vụ), Data (nguồn dữ liệu), và Presentation (giao diện). Sự phân tách này giúp việc testing và bảo trì trở nên dễ dàng hơn.

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;
  }
}

Tầng Domain không phụ thuộc vào bất kỳ thành phần nào khác, Data phụ thuộc vào Domain, Presentation phụ thuộc vào cả hai.

10. Làm thế nào để triển khai dependency injection?

Dependency injection tách rời các thành phần bằng cách cung cấp các phụ thuộc từ bên ngoài. Riverpod xử lý rất tốt điều này thông qua hệ thống provider.

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(),
      ),
    );
  });
}

Lợi thế lớn nhất: các bài test có thể thay thế bất kỳ dependency nào bằng mock.

Câu Hỏi về Hiệu Suất

11. Làm thế nào để tối ưu hiệu suất rebuild?

Giảm thiểu số lần rebuild giúp cải thiện hiệu suất đáng kể. Các kỹ thuật chính bao gồm: widget const, chia nhỏ widget, và Riverpod selector.

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

Sử dụng DevTools Performance để xác định các lần rebuild quá mức.

12. Làm thế nào để tối ưu danh sách dài?

Danh sách dài yêu cầu lazy loading với ListView.builder. Cần tránh sử dụng ListView với children trực tiếp khi có hơn 20 item.

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

Với danh sách rất dài (trên 1000 item), cân nhắc sử dụng ListView.separated hoặc các package chuyên dụng như scrollable_positioned_list.

Cache Hình Ảnh

Với danh sách có chứa hình ảnh, hãy sử dụng cached_network_image để tránh tải lại. Hình ảnh không được cache sẽ gây giật lag khi cuộn.

13. Giải thích cách hoạt động của rendering engine Impeller

Impeller thay thế Skia làm rendering engine mặc định (Flutter 3.16+). Nó biên dịch trước shader để loại bỏ hiện tượng "jank" khi hiển thị lần đầu.

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 được bật mặc định trên iOS. Trên Android, có thể kiểm tra tương thích bằng lệnh flutter run --enable-impeller.

Câu Hỏi về Điều Hướng và Form

14. Làm thế nào để xử lý điều hướng với deep linking?

Deep linking cho phép mở ứng dụng tại một màn hình cụ thể thông qua URL. GoRouter xử lý tính năng này một cách tự nhiên và hiệu quả.

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

Cần cấu hình các file native (AndroidManifest.xml, Info.plist) để kích hoạt deep linking ở cấp hệ thống.

15. Làm thế nào để validate form hiệu quả?

Việc validate kết hợp Form, TextFormField và các validator tùy chỉnh. Các package kiểu Zod như formz giúp thêm cấu trúc cho quá trình này.

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
    }
  }
}

Chế độ AutovalidateMode.onUserInteraction mang lại trải nghiệm người dùng tốt nhất: lỗi chỉ hiển thị sau khi người dùng tương tác với trường nhập liệu.

Câu Hỏi về Testing

16. Làm thế nào để test widget trong Flutter?

Widget test xác minh giao diện mà không cần thiết bị thực. Package flutter_test cung cấp các tiện ích cần thiết cho việc này.

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() tiến thêm một frame, pumpAndSettle() chờ đến khi tất cả animation hoàn thành.

17. Làm thế nào để test Riverpod provider?

Riverpod hỗ trợ testing thuận tiện thông qua ProviderContainer và cơ chế override.

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 test chạy rất nhanh vì không cần render giao diện.

Câu Hỏi về Triển Khai

18. Làm thế nào để quản lý các môi trường khác nhau (dev, staging, prod)?

Các môi trường được cấu hình thông qua file .env hoặc hằng số compile-time với --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. Làm thế nào để triển khai flavoring cho nhiều biến thể ứng dụng?

Flavoring tạo nhiều biến thể (client1, client2) với cấu hình riêng biệt (icon, tên, API) từ cùng một codebase.

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;
}

Cần cấu hình các file native Android (build.gradle) và iOS (xcconfig) cho từng flavor.

20. Các best practice về bảo mật trong Flutter là gì?

Bảo mật bao gồm lưu trữ dữ liệu nhạy cảm, validate input, và bảo vệ chống 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;
  }
}

Không bao giờ lưu secret trực tiếp trong code. Sử dụng flutter_secure_storage cho token và flag --obfuscate cho bản production.

Kết Luận

20 câu hỏi này bao quát các khía cạnh thiết yếu của buổi phỏng vấn Flutter: nền tảng framework, thành thạo Dart, mô hình kiến trúc, và các best practice trong production. Thành công nằm ở việc hiểu sâu các cơ chế bên dưới, không chỉ là cú pháp.

Danh Sách Kiểm Tra Trước Phỏng Vấn

  • Nắm vững sự khác biệt StatelessWidget/StatefulWidget và các trường hợp sử dụng
  • Hiểu rõ widget tree và tối ưu hóa rebuild
  • Luyện tập async/await, Future và Stream với các bài tập thực hành
  • Triển khai một dự án với Riverpod để quản lý state
  • Nắm rõ GoRouter và deep linking
  • Viết unit test và widget test
  • Hiểu cấu hình đa môi trường

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Việc luyện tập thường xuyên trên các dự án cá nhân vẫn là cách tốt nhất để củng cố kiến thức này. Mỗi câu hỏi được đề cập ở đây đều xứng đáng được khám phá sâu hơn với code thực tế để nắm bắt các tinh tế của Flutter framework.

Thẻ

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

Chia sẻ

Bài viết liên quan