# 20 คำถามสัมภาษณ์งาน Flutter ที่พบบ่อยที่สุดสำหรับนักพัฒนาแอปมือถือ > เตรียมตัวสัมภาษณ์งาน Flutter ด้วยคำถาม 20 ข้อที่ถูกถามบ่อยที่สุด ครอบคลุม Widget, State Management, Dart, สถาปัตยกรรม และแนวทางปฏิบัติที่ดีที่สุด พร้อมคำอธิบายอย่างละเอียดและตัวอย่างโค้ด - Published: 2026-01-24 - Updated: 2026-04-12 - Author: SharpSkill - Tags: flutter, dart, interview, mobile development, technical interview - Reading time: 15 min --- การสัมภาษณ์งาน Flutter จะประเมินความเชี่ยวชาญใน Framework, ภาษา Dart และรูปแบบสถาปัตยกรรมแอปมือถือ บทความนี้รวบรวม 20 คำถามที่ถูกถามบ่อยที่สุด ตั้งแต่พื้นฐานไปจนถึงแนวคิดขั้นสูง พร้อมคำตอบอย่างละเอียดและตัวอย่างโค้ดที่ใช้งานได้จริง > **เคล็ดลับสัมภาษณ์** > > ผู้สัมภาษณ์จะให้คะแนนสูงกับผู้สมัครที่สามารถอธิบาย "เหตุผล" ควบคู่กับ "วิธีการ" ได้ สำหรับแต่ละแนวคิด การเข้าใจกรณีการใช้งานและข้อแลกเปลี่ยนทางเทคนิคจะเป็นตัวตัดสินที่สำคัญ ## คำถามพื้นฐาน Flutter และ Dart ### 1. StatelessWidget และ StatefulWidget แตกต่างกันอย่างไร? **StatelessWidget** เป็น Widget ที่ไม่สามารถเปลี่ยนแปลงได้ (immutable) ซึ่งรูปลักษณ์ขึ้นอยู่กับค่าเริ่มต้นที่กำหนดเท่านั้น เมื่อสร้างแล้วจะไม่มีการเปลี่ยนแปลงใด ๆ ส่วน **StatefulWidget** จะเก็บ State ที่สามารถเปลี่ยนแปลงได้ตามเวลา และทำให้ Widget ถูก Rebuild ใหม่ ```dart // stateless_example.dart // 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}); @override Widget build(BuildContext context) { // Build called once (unless parent rebuilds) return Text('Welcome, $username'); } } ``` ```dart // stateful_example.dart // StatefulWidget: mutable state, can rebuild itself class LikeButton extends StatefulWidget { const LikeButton({super.key}); @override State createState() => _LikeButtonState(); } class _LikeButtonState extends State { // Mutable local state int _likeCount = 0; void _incrementLike() { // setState triggers rebuild with new state setState(() { _likeCount++; }); } @override Widget build(BuildContext context) { return ElevatedButton( onPressed: _incrementLike, child: Text('Likes: $_likeCount'), ); } } ``` หลักการคือ ให้ใช้ StatelessWidget เป็นค่าเริ่มต้น และใช้ StatefulWidget เฉพาะเมื่อจำเป็นต้องจัดการ State ภายใน Widget เท่านั้น ### 2. Widget Tree ใน Flutter ทำงานอย่างไร? Flutter จัดระเบียบ Interface โดยแบ่งเป็น 3 Tree ที่เชื่อมโยงกัน ได้แก่ **Widget Tree** (การประกาศแบบ immutable), **Element Tree** (Lifecycle และ Binding) และ **Render Tree** (Layout และ Painting) ```dart // widget_tree_example.dart // Declarative structure - widgets describe the UI class MyApp extends StatelessWidget { @override 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 จะเปรียบเทียบ Widget Tree เก่าและใหม่เพื่อ Rebuild เฉพาะ Element ที่มีการเปลี่ยนแปลง กระบวนการ Differentiation นี้อาศัย Key และประเภทของ Widget ### 3. Const Constructor คืออะไร และทำไมต้องใช้? **Const Constructor** สร้าง Widget ในขั้นตอน Compile Time แทนที่จะเป็น Runtime ทำให้ Flutter สามารถนำ Instance เดิมกลับมาใช้ซ้ำได้ ซึ่งช่วยเพิ่มประสิทธิภาพโดยหลีกเลี่ยงการ Rebuild ที่ไม่จำเป็น ```dart // const_example.dart class OptimizedScreen extends StatelessWidget { const OptimizedScreen({super.key}); @override 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}); @override Widget build(BuildContext context) { return Card(child: Text(title)); } } ``` Flutter Analyzer จะแจ้งเตือนเมื่อมีโอกาสใช้ const แต่ไม่ได้ใช้ผ่าน lint rule `prefer_const_constructors` ### 4. อธิบายประเภทต่าง ๆ ของ Key ใน Flutter **Key** ทำหน้าที่รักษา State ของ Widget เมื่อมีการจัดเรียงใหม่ หากไม่มี Key Flutter จะอาศัยตำแหน่งใน Tree ซึ่งอาจทำให้เกิด Bug เมื่อมีการเปลี่ยนลำดับของรายการ ```dart // keys_example.dart class TodoList extends StatelessWidget { final List todos; const TodoList({super.key, required this.todos}); @override 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 { @override 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 ยังช่วยให้สามารถเข้าถึง State ของ Widget จากภายนอกได้ แต่ควรใช้อย่างระมัดระวัง ## คำถามเกี่ยวกับ Dart ### 5. final และ const ใน Dart ต่างกันอย่างไร? **final** กำหนดตัวแปรที่สามารถกำหนดค่าได้เพียงครั้งเดียว โดยประเมินค่าในขณะ Runtime ส่วน **const** สร้างค่าคงที่ในขั้นตอน Compile Time ซึ่งเป็น Immutable และถูกประเมินค่าก่อนการรัน ```dart // final_const_example.dart 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 สำหรับ Widget ที่เป็น Static และใช้ final สำหรับค่าที่ต้องคำนวณ ### 6. Future และ async/await ทำงานอย่างไร? **Future** แทนค่าที่จะพร้อมใช้งานในภายหลัง ส่วน **async/await** ทำให้ไวยากรณ์อ่านง่ายขึ้นในการจัดการกับ Asynchronous Operation โดยไม่ต้องซ้อน Callback ```dart // async_example.dart class UserRepository { final ApiClient _client; UserRepository(this._client); // Future: promise of a future value Future 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> fetchUsers(List 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 processSequentially(List ids) async { for (final id in ids) { // Each request waits for the previous one await fetchUser(id); } } } ``` ใช้ `Future.wait` สำหรับการประมวลผลแบบขนาน และใช้ async loop สำหรับการประมวลผลตามลำดับ > **FutureBuilder vs Riverpod** > > FutureBuilder เหมาะสำหรับกรณีที่ไม่ซับซ้อน แต่ Riverpod (AsyncValue) ให้ความสามารถในการจัดการ Cache, Error Handling และ Refresh ที่ดีกว่าสำหรับแอปพลิเคชันที่มีความซับซ้อนสูง ### 7. อธิบายเรื่อง Stream และการใช้งาน **Stream** แทนลำดับค่าแบบ Asynchronous ซึ่งเหมาะสำหรับ Event แบบ Real-time เช่น WebSocket, ข้อมูลเซ็นเซอร์ หรือการโต้ตอบของผู้ใช้ ```dart // stream_example.dart class MessageService { // StreamController manages creation and broadcasting final _messageController = StreamController.broadcast(); // Expose only the Stream (not the Sink) Stream 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}); @override Widget build(BuildContext context) { return StreamBuilder( 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()` อนุญาตให้มี Listener หลายตัว ต่างจาก Single-subscription Stream ที่รับ Listener ได้เพียงตัวเดียว ## คำถามเกี่ยวกับสถาปัตยกรรมและ State Management ### 8. เปรียบเทียบ State Management: Provider, Riverpod และ Bloc แต่ละโซลูชันตอบโจทย์ความต้องการที่แตกต่างกัน **Provider** เน้นความเรียบง่ายและรองรับ Native Integration ส่วน **Riverpod** มาพร้อม Type Safety และความสามารถในการทดสอบ ขณะที่ **Bloc** บังคับใช้สถาปัตยกรรมแบบ Event-driven อย่างเข้มงวด ```dart // riverpod_example.dart // Riverpod: declarative and type-safe approach final userProvider = FutureProvider.autoDispose((ref) async { final repository = ref.watch(userRepositoryProvider); return repository.fetchCurrentUser(); }); class UserProfile extends ConsumerWidget { @override 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), ); } } ``` ```dart // bloc_example.dart // 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 { AuthBloc() : super(AuthInitial()) { on(_onLoginRequested); } Future _onLoginRequested( LoginRequested event, Emitter 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** แบ่งโค้ดออกเป็น Layer ที่เป็นอิสระจากกัน ได้แก่ Domain (Business Logic), Data (แหล่งข้อมูล) และ Presentation (UI) การแบ่งแยกนี้ช่วยให้การทดสอบและการดูแลรักษาทำได้ง่ายขึ้น ```dart // domain/entities/user.dart // 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 getUser(String id); Future updateUser(User user); } // domain/usecases/get_user_usecase.dart // Use case: isolated business logic class GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future call(String userId) { return _repository.getUser(userId); } } ``` ```dart // data/repositories/user_repository_impl.dart // Concrete implementation with data sources class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource _remoteDataSource; final UserLocalDataSource _localDataSource; UserRepositoryImpl(this._remoteDataSource, this._localDataSource); @override Future 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 Layer ไม่พึ่งพา Layer อื่น, Data Layer พึ่งพา Domain และ Presentation Layer พึ่งพาทั้งสอง ### 10. การทำ Dependency Injection ทำอย่างไร? **Dependency Injection** แยก Component ออกจากกันโดยการส่ง Dependency จากภายนอก Riverpod ทำเรื่องนี้ได้อย่างยอดเยี่ยมผ่านระบบ Provider ```dart // di_example.dart // Provider definitions (dependencies) final apiClientProvider = Provider((ref) { return ApiClient(baseUrl: Environment.apiUrl); }); final userRepositoryProvider = Provider((ref) { // Automatically injects ApiClient final client = ref.watch(apiClientProvider); return UserRepositoryImpl(client); }); final getUserUseCaseProvider = Provider((ref) { final repository = ref.watch(userRepositoryProvider); return GetUserUseCase(repository); }); // Usage in a widget class UserScreen extends ConsumerWidget { @override 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(), ), ); }); } ``` ข้อได้เปรียบหลักคือ การทดสอบสามารถแทนที่ Dependency ใด ๆ ด้วย Mock ได้อย่างง่ายดาย ## คำถามเกี่ยวกับประสิทธิภาพ ### 11. การเพิ่มประสิทธิภาพการ Rebuild ทำอย่างไร? การลดจำนวน Rebuild ที่ไม่จำเป็นช่วยเพิ่มประสิทธิภาพของแอปได้อย่างมาก เทคนิคหลักประกอบด้วย const Widget, การแบ่ง Widget ให้เล็กลง และ Riverpod Selector ```dart // rebuild_optimization.dart // ❌ Bad: everything rebuilds on each change class BadExample extends ConsumerWidget { @override 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 { @override 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 { @override 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 เพื่อตรวจจับ Rebuild ที่มากเกินไป ### 12. การเพิ่มประสิทธิภาพรายการยาว ๆ ทำอย่างไร? รายการที่มีจำนวนมากต้องใช้ **Lazy Loading** ด้วย ListView.builder ควรหลีกเลี่ยงการใช้ ListView ที่มี Children โดยตรงเมื่อมีรายการมากกว่า 20 รายการ ```dart // list_optimization.dart class OptimizedList extends StatelessWidget { final List items; const OptimizedList({super.key, required this.items}); @override 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 { @override ConsumerState createState() => _InfiniteListState(); } class _InfiniteListState extends ConsumerState { final _scrollController = ScrollController(); @override 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(); } } @override Widget build(BuildContext context) { final items = ref.watch(itemsProvider); return ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) => ItemTile(item: items[index]), ); } } ``` สำหรับรายการที่ยาวมาก (1,000 รายการขึ้นไป) ควรพิจารณาใช้ `ListView.separated` หรือ Package เฉพาะทาง เช่น `scrollable_positioned_list` > **แคชรูปภาพ** > > สำหรับรายการที่มีรูปภาพ ควรใช้ `cached_network_image` เพื่อหลีกเลี่ยงการโหลดซ้ำ รูปภาพที่ไม่ได้แคชจะทำให้เกิดอาการกระตุกขณะเลื่อนหน้าจอ ### 13. อธิบายการทำงานของ Impeller Rendering Engine **Impeller** เข้ามาแทนที่ Skia ในฐานะ Rendering Engine เริ่มต้น (Flutter 3.16+) โดยทำการ Precompile Shader เพื่อกำจัดอาการ "Jank" ในการแสดงผลครั้งแรก ```dart // impeller_benefits.dart // Complex animations benefit from Impeller class SmoothAnimation extends StatefulWidget { @override State createState() => _SmoothAnimationState(); } class _SmoothAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(); } @override 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. การจัดการ Navigation ด้วย Deep Linking ทำอย่างไร? **Deep Linking** ช่วยให้สามารถเปิดแอปไปยังหน้าจอที่กำหนดผ่าน URL ได้โดยตรง GoRouter รองรับฟังก์ชันนี้แบบ Native ```dart // deep_linking.dart 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), ); } ``` จำเป็นต้องตั้งค่าไฟล์ Native (AndroidManifest.xml, Info.plist) เพื่อเปิดใช้งาน Deep Linking ในระดับระบบปฏิบัติการ ### 15. การ Validate ฟอร์มอย่างมีประสิทธิภาพทำอย่างไร? การ Validate รวม Form, TextFormField และ Custom Validator เข้าด้วยกัน Package อย่าง `formz` ช่วยเพิ่มโครงสร้างที่ชัดเจนคล้ายกับ Zod ```dart // form_validation.dart class RegistrationForm extends StatefulWidget { @override State createState() => _RegistrationFormState(); } class _RegistrationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmController = TextEditingController(); @override 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 ที่ดีที่สุด เพราะแสดง Error หลังจากผู้ใช้โต้ตอบกับฟิลด์แล้วเท่านั้น ## คำถามเกี่ยวกับการทดสอบ ### 16. การทดสอบ Widget ใน Flutter ทำอย่างไร? Widget Test ช่วยตรวจสอบ UI โดยไม่ต้องพึ่งพาอุปกรณ์จริง Package `flutter_test` มีเครื่องมือที่จำเป็นทั้งหมด ```dart // widget_test.dart 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()` รอจนกว่า Animation ทั้งหมดจะเสร็จสมบูรณ์ ### 17. การทดสอบ Riverpod Provider ทำอย่างไร? Riverpod ช่วยให้การทดสอบทำได้ง่ายผ่าน `ProviderContainer` และระบบ Override ```dart // provider_test.dart 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 ทำงานได้รวดเร็วเพราะไม่จำเป็นต้อง Render UI ## คำถามเกี่ยวกับการ Deploy ### 18. การจัดการ Environment (dev, staging, prod) ทำอย่างไร? Environment กำหนดค่าผ่านไฟล์ `.env` หรือ Compile-time Constant ด้วย `--dart-define` ```dart // environment.dart 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. การทำ Flavoring สำหรับแอปหลายเวอร์ชันทำอย่างไร? **Flavoring** สร้างแอปหลายเวอร์ชัน (client1, client2) ที่มีการตั้งค่าแตกต่างกัน (ไอคอน, ชื่อ, API) ```dart // main_client1.dart 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; } ``` จำเป็นต้องตั้งค่าไฟล์ Native ทั้ง Android (build.gradle) และ iOS (xcconfig) สำหรับแต่ละ Flavor ### 20. แนวทางปฏิบัติด้านความปลอดภัยของ Flutter มีอะไรบ้าง? ความปลอดภัยครอบคลุมการจัดเก็บข้อมูลที่ละเอียดอ่อน, การ Validate Input และการป้องกัน Reverse Engineering ```dart // security_example.dart 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 saveToken(String token) async { await _storage.write(key: 'auth_token', value: token); } Future getToken() async { return _storage.read(key: 'auth_token'); } Future 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; } } ``` ไม่ควรเก็บ Secret ไว้ในโค้ดโดยเด็ดขาด ใช้ `flutter_secure_storage` สำหรับ Token และใช้ `--obfuscate` สำหรับ Production Build ## สรุป คำถาม 20 ข้อเหล่านี้ครอบคลุมทุกแง่มุมที่สำคัญของการสัมภาษณ์งาน Flutter ตั้งแต่พื้นฐานของ Framework, ความเชี่ยวชาญด้าน Dart, รูปแบบสถาปัตยกรรม ไปจนถึงแนวทางปฏิบัติสำหรับ Production ความสำเร็จในการสัมภาษณ์อยู่ที่ความเข้าใจเชิงลึกของกลไกเบื้องหลัง ไม่ใช่แค่ไวยากรณ์ ### รายการตรวจสอบก่อนสัมภาษณ์ - ✅ เข้าใจความแตกต่างระหว่าง StatelessWidget กับ StatefulWidget และกรณีการใช้งาน - ✅ เข้าใจ Widget Tree และการเพิ่มประสิทธิภาพการ Rebuild - ✅ ฝึกฝน async/await, Future และ Stream ด้วยแบบฝึกหัดจริง - ✅ สร้างโปรเจกต์ด้วย Riverpod สำหรับ State Management - ✅ รู้จัก GoRouter และ Deep Linking - ✅ เขียน Unit Test และ Widget Test - ✅ เข้าใจการตั้งค่า Multi-environment การฝึกฝนอย่างสม่ำเสมอในโปรเจกต์ส่วนตัวยังคงเป็นวิธีที่ดีที่สุดในการเสริมสร้างความรู้เหล่านี้ แต่ละคำถามที่กล่าวมาควรได้รับการศึกษาเพิ่มเติมในเชิงลึกด้วยโค้ดจริงเพื่อให้เข้าใจรายละเอียดปลีกย่อยของ Flutter Framework อย่างถ่องแท้ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/flutter/flutter-interview-questions