Câu hỏi phỏng vấn Flutter thường gặp 2026 và cách trả lời

Mục lục bài viết (86)
- Một buổi phỏng vấn Flutter thường gồm những vòng nào?
- Nhà tuyển dụng thực sự đánh giá điều gì?
- Nhóm 1 — Nền tảng Dart (10 câu)
- 1. final và const khác nhau thế nào?
- 2. Null safety hoạt động thế nào?
- 3. late dùng khi nào, và rủi ro của nó là gì?
- 4. Future và Stream khác nhau ở đâu?
- 5. async/await có tạo luồng mới không?
- 6. Khi nào dùng Future.wait, khi nào dùng vòng lặp await?
- 7. Mixin là gì và khác gì interface?
- 8. factory constructor dùng để làm gì?
- 9. Generics giúp gì?
- 10. Shallow copy và deep copy của collection khác nhau thế nào?
- Nhóm 2 — Widget, lifecycle và BuildContext (8 câu)
- 1. Flutter vẽ giao diện qua mấy cây?
- 2. StatelessWidget và StatefulWidget khác nhau ở đâu?
- 3. Kể tên vòng đời của State.
- 4. BuildContext là gì?
- 5. Vì sao dùng context sau await là nguy hiểm?
- 6. Key dùng để làm gì? Khi nào cần GlobalKey?
- 7. const widget có tác dụng gì?
- 8. hot reload và hot restart khác gì nhau?
- Nhóm 3 — State management (8 câu)
- 1. Khi nào thì không cần thư viện quản lý trạng thái?
- 2. Các cách quản lý state phổ biến khác nhau gì?
- 3. Vì sao dùng Bloc mà không dùng Provider?
- 4. Khi nào nên đưa state lên trên (lift state up)?
- 5. Làm sao tránh rebuild toàn bộ cây?
- 6. ChangeNotifier hoạt động thế nào?
- 7. Xử lý state sau khi đăng xuất thì thế nào?
- 8. Test state management thế nào?
- Nhóm 4 — Gọi API, bất đồng bộ và xử lý lỗi (7 câu)
- 1. Bạn dùng gì để gọi API và vì sao?
- 2. Xử lý 401 hết hạn token thế nào cho đúng?
- 3. try/catch quanh await bắt được gì?
- 4. Phân biệt lỗi mạng và lỗi nghiệp vụ?
- 5. Bạn mô hình hoá kết quả trả về thế nào?
- 6. Có nên parse JSON trên luồng chính?
- 7. Hủy request khi người dùng rời màn hình?
- Nhóm 5 — Lưu trữ dữ liệu và hoạt động ngoại tuyến (5 câu)
- 1. Phân biệt các lựa chọn lưu trữ cục bộ?
- 2. Token đăng nhập nên lưu ở đâu?
- 3. App hoạt động ngoại tuyến thì thiết kế thế nào?
- 4. Vì sao cần tầng cache chứ không gọi API mọi lúc?
- 5. Xử lý xung đột dữ liệu khi đồng bộ?
- Nhóm 6 — Hiệu năng và debug (6 câu)
- 1. App bị giật khi cuộn danh sách, bạn làm gì?
- 2. ListView.builder khác ListView thường thế nào?
- 3. RepaintBoundary dùng khi nào?
- 4. Làm sao phát hiện rò rỉ bộ nhớ?
- 5. Kích thước app quá lớn, giảm thế nào?
- 6. Làm sao debug một lỗi chỉ xảy ra trên máy thật, không xảy ra trên máy ảo?
- Nhóm 7 — Testing và quy trình phát hành (5 câu)
- 1. Các loại test trong Flutter?
- 2. Bạn test cái gì trước khi test cái gì?
- 3. Mock service gọi API thế nào?
- 4. Quy trình phát hành app của bạn gồm những gì?
- 5. Tự động hoá build và phát hành thế nào?
- Nhóm 8 — Kiến trúc và tổ chức code (6 câu)
- 1. Bạn tổ chức thư mục dự án thế nào?
- 2. MVVM và Clean Architecture khác nhau thế nào?
- 3. Khi nào kiến trúc trở thành "quá đà"?
- 4. Repository pattern để làm gì?
- 5. Tiêm phụ thuộc (DI) trong Flutter?
- 6. Dự án nhiều người làm thì quy ước gì quan trọng?
- Nhóm 9 — Câu hỏi tình huống và thiết kế (5 câu)
- 1. Thiết kế màn hình danh sách sản phẩm có tìm kiếm, lọc, phân trang?
- 2. Ứng dụng chat thời gian thực cần chú ý gì?
- 3. Cần đăng nhập nhiều nền tảng (email, Google, Apple) thì tổ chức thế nào?
- 4. App phải hỗ trợ cả điện thoại nhỏ và tablet, bạn làm gì?
- 5. Bạn xử lý khi cần thêm tính năng phải gọi API native chưa có package?
- Nhóm 10 — Câu hỏi hành vi và câu hỏi ngược lại
- 1. Kể về một bug khó nhất bạn từng gặp?
- 2. Bạn xử lý thế nào khi không đồng ý với quyết định kỹ thuật của team?
- 3. Bạn học công nghệ mới thế nào?
- 4. Bạn nên hỏi ngược nhà tuyển dụng những gì?
- Live coding: các dạng bài thường gặp và cách luyện
- 10 lỗi khiến ứng viên Flutter bị loại
- Lộ trình ôn phỏng vấn Flutter trong 3 tuần
- Học Flutter để đi làm: đi theo lộ trình nào?
- Câu hỏi thường gặp
- Phỏng vấn Flutter có cần bằng đại học không?
- Cần biết native (Kotlin/Swift) ở mức nào?
- Không có kinh nghiệm công ty thì có được nhận không?
- Phỏng vấn Flutter có hỏi thuật toán không?
- Nên học thêm gì sau khi đã đi làm Flutter?

Bạn đã làm được vài app Flutter, đọc hiểu tài liệu, nhưng đến buổi phỏng vấn vẫn bị hỏi những câu khiến mình lúng túng: "Vì sao dùng Bloc mà không dùng Provider?", "BuildContext trong hàm async có vấn đề gì?", "App bị giật khi cuộn thì bạn debug thế nào?". Đây là những câu không thể học thuộc, nhưng có thể luyện được.
Bài này tổng hợp hơn 60 câu hỏi phỏng vấn Flutter thường gặp, chia theo 10 nhóm từ nền tảng đến kiến trúc và tình huống thực tế, kèm cách trả lời mẫu và điều nhà tuyển dụng thực sự muốn nghe ở mỗi câu. Cuối bài có danh sách lỗi khiến ứng viên bị loại và lộ trình ôn 3 tuần.
Một buổi phỏng vấn Flutter thường gồm những vòng nào?
Biết trước quy trình giúp bạn chuẩn bị đúng trọng tâm thay vì ôn tràn lan.
- Vòng sàng lọc (HR/recruiter): hỏi về kinh nghiệm, mức lương mong muốn, thời gian có thể bắt đầu. Ít câu kỹ thuật, nhưng đây là vòng loại nhiều ứng viên nhất vì trả lời mơ hồ.
- Bài test kỹ thuật (take-home): thường là làm một màn hình gọi API, có loading, error, empty state, hoặc một bài tập Dart thuần. Thời gian 1–3 ngày.
- Phỏng vấn kỹ thuật 1: nền tảng Dart, widget, lifecycle, state management.
- Phỏng vấn kỹ thuật 2 (live coding): viết code trực tiếp, thường là dựng một màn hình nhỏ hoặc sửa lỗi có sẵn. Đây là vòng phân biệt rõ nhất người "đã làm thật" và người "đã xem tutorial".
- Vòng kiến trúc/tình huống: với vị trí middle trở lên, sẽ có câu hỏi thiết kế: cấu trúc thư mục, chọn state management, xử lý offline, chiến lược test.
- Vòng văn hoá và đàm phán: trao đổi về cách làm việc nhóm, xử lý bất đồng, lương và phúc lợi.
Một lưu ý thực tế: nhiều công ty Việt Nam gộp vòng 3 và 4 làm một, hoặc thay live coding bằng việc hỏi sâu vào bài take-home bạn đã nộp. Vì vậy hãy chuẩn bị để giải thích được mọi dòng code bạn viết trong bài test.
Nhà tuyển dụng thực sự đánh giá điều gì?
Đây là phần quan trọng nhất của bài, và cũng là phần ứng viên hay bỏ qua.
Người phỏng vấn không kiểm tra bạn có thuộc lòng tên API. Họ kiểm tra ba thứ:
- Bạn hiểu nguyên nhân, không chỉ biết cách dùng. Ai cũng biết
setStateđể cập nhật giao diện. Câu hỏi thật là: vì saosetStateđôi khi làm cả màn hình build lại, và khi nào thì nên tách state ra ngoài. - Bạn biết trade-off. Mọi lựa chọn kỹ thuật đều đánh đổi thứ gì đó. Trả lời "Bloc là tốt nhất" nghe yếu hơn nhiều so với "với dự án nhỏ, 3 màn hình, tôi dùng Provider vì ít code boilerplate; khi team đông và cần test luồng trạng thái phức tạp tôi chuyển sang Bloc".
- Bạn đã gặp vấn đề thật. Câu hỏi về debug và hiệu năng gần như luôn xuất hiện, vì đó là phần chỉ có được khi đã tự tay sửa lỗi trong dự án thật.
Kinh nghiệm cho thấy: ứng viên trả lời đúng 60% nhưng giải thích được lý do thường được đánh giá cao hơn ứng viên "đúng" nhưng chỉ nêu định nghĩa. Cách luyện: với mỗi câu hỏi dưới đây, tự hỏi thêm hai câu nữa — "vì sao?" và "nếu không làm thế thì sao?".
Nhóm 1 — Nền tảng Dart (10 câu)
Nhóm này xuất hiện ở gần như mọi buổi phỏng vấn. Nền tảng Dart không vững thì các phần sau sẽ rất khó giải thích.
1. final và const khác nhau thế nào?
final là biến chỉ gán một lần, giá trị được xác định lúc chạy. const là hằng số biên dịch — giá trị phải biết trước khi chạy và được tính một lần duy nhất.
Điểm ăn tiền khi trả lời: hãy nói thêm tác động tới hiệu năng giao diện. Khi bạn viết const cho widget, Flutter biết widget đó không bao giờ thay đổi và bỏ qua việc rebuild nó. Đây là mẹo tối ưu đơn giản nhất và hiệu quả nhất trong mọi dự án Flutter.
2. Null safety hoạt động thế nào?
Dart phân biệt kiểu có thể null (String?) và kiểu không thể null (String). Trình biên dịch buộc bạn xử lý khả năng null trước khi dùng, nên nhiều lỗi kiểu NullPointerException bị chặn ngay lúc viết code thay vì lúc chạy.
Các toán tử cần nhớ: ?. (truy cập có kiểm tra null), ?? (giá trị mặc định), ! (khẳng định không null — dùng thật cẩn thận), late (khai báo sau, gán trước khi đọc).
3. late dùng khi nào, và rủi ro của nó là gì?
late hữu ích khi biến chưa thể khởi tạo lúc khai báo nhưng chắc chắn có giá trị trước lần đọc đầu tiên, ví dụ controller gán trong initState, hoặc biến tính lười.
Rủi ro: nếu bạn đọc trước khi gán, chương trình ném lỗi lúc chạy chứ không phải lúc biên dịch. Vì vậy late là cách "trì hoãn" việc kiểm tra, không phải cách bỏ qua nó.
4. Future và Stream khác nhau ở đâu?
Future đại diện cho một giá trị sẽ có trong tương lai. Stream đại diện cho chuỗi giá trị theo thời gian.
Theo kinh nghiệm làm app thật: gọi API một lần thì dùng Future; lắng nghe vị trí người dùng, dữ liệu realtime từ Firebase, hay trạng thái kết nối mạng thì dùng Stream. Trong giao diện tương ứng là FutureBuilder và StreamBuilder.
5. async/await có tạo luồng mới không?
Không. Đây là câu hỏi bẫy rất phổ biến.
async/await chỉ là cú pháp cho bất đồng bộ, chạy trên cùng một event loop với Future. Trong khi chờ, luồng chính vẫn có thể xử lý việc khác, nhưng không có luồng mới nào được tạo. Muốn chạy song song thật (ví dụ parse JSON lớn, tính toán nặng) thì phải dùng Isolate hoặc compute. Chi tiết hơn ở bài về Isolate và compute.
6. Khi nào dùng Future.wait, khi nào dùng vòng lặp await?
Future.wait chạy các tác vụ song song và chờ tất cả xong — dùng khi các tác vụ độc lập, ví dụ gọi ba API không liên quan nhau. Vòng lặp await chạy tuần tự — chỉ dùng khi tác vụ sau phụ thuộc kết quả tác vụ trước.
Nếu bạn dùng vòng lặp await cho ba API độc lập, thời gian chờ sẽ bằng tổng ba lần gọi thay vì lần lâu nhất. Đây là lỗi hiệu năng rất hay gặp trong code thật.
7. Mixin là gì và khác gì interface?
Mixin cho phép "trộn" các phương thức đã có phần thân vào class mà không dùng kế thừa. interface trong Dart (khai báo bằng implements) chỉ định nghĩa hợp đồng, không có phần thân.
Dùng mixin khi bạn muốn chia sẻ hành vi giữa nhiều class không có quan hệ cha–con, ví dụ logic log, logic cache, xử lý token.
8. factory constructor dùng để làm gì?
Nó cho phép constructor trả về một instance đã có thay vì luôn tạo mới. Hai ứng dụng hay gặp: viết singleton, và tạo object từ JSON khi cần chọn class con dựa trên dữ liệu đầu vào.
9. Generics giúp gì?
Generics giúp code dùng lại được mà vẫn giữ an toàn kiểu. List<String> chỉ cho thêm chuỗi, và trình biên dịch bắt lỗi nếu bạn thêm số. Trong Flutter, generics xuất hiện ở khắp nơi, ví dụ ValueNotifier<int>, Future<List<Post>>, Stream<AuthState>.
10. Shallow copy và deep copy của collection khác nhau thế nào?
Gán listB = listA chỉ tạo tham chiếu tới cùng một list — sửa list này sẽ thấy ở list kia. Muốn bản sao độc lập thì tạo list mới (List.of(listA) hoặc spread [...listA]). Với object lồng nhau, phải copy sâu từng cấp, hoặc dùng copyWith. Đây là nguồn gốc của rất nhiều bug "sửa chỗ này mà chỗ kia cũng đổi".
Nếu bạn muốn nắm chắc nền tảng trước khi phỏng vấn, bài Dart cho người mới: 10 khái niệm cần nắm đi sâu hơn vào từng khái niệm trên.
Nhóm 2 — Widget, lifecycle và BuildContext (8 câu)
1. Flutter vẽ giao diện qua mấy cây?
Ba cây: Widget tree (mô tả cấu hình, không giữ trạng thái), Element tree (đối tượng trung gian giữ vị trí trong cây, giúp Flutter biết phần nào cần cập nhật), RenderObject tree (chịu trách nhiệm layout và vẽ).
Điểm cốt lõi: widget là cấu hình bất biến, nên "widget bị rebuild" không có nghĩa là phần tử trên màn hình bị tạo lại. Đó là lý do Flutter có thể rebuild rất nhiều widget mà vẫn nhanh.
2. StatelessWidget và StatefulWidget khác nhau ở đâu?
StatelessWidget không giữ trạng thái thay đổi theo thời gian. StatefulWidget có một State object tồn tại giữa các lần build, cho phép giữ dữ liệu (controller, giá trị ô nhập liệu) và gọi setState để báo cần vẽ lại.
3. Kể tên vòng đời của State.
initState → didChangeDependencies → build → didUpdateWidget (khi widget cha truyền cấu hình mới) → deactivate → dispose. Trong đó dispose là nơi bắt buộc giải phóng controller, timer, subscription. Xem chi tiết ở bài vòng đời widget.
4. BuildContext là gì?
Nó là "vị trí" của widget trong cây widget, dùng để tìm các thứ nằm phía trên nó: Theme.of(context), Navigator.of(context), Provider.of(context). Hiểu nó như một con trỏ tới nút trong cây, không phải một object dữ liệu.
5. Vì sao dùng context sau await là nguy hiểm?
Vì giữa lúc bắt đầu await và lúc nó hoàn thành, widget có thể đã bị xoá khỏi cây (người dùng bấm back). Khi đó context không còn hợp lệ, và bạn sẽ gặp lỗi kiểu "Looking up a deactivated widget's ancestor is unsafe".
Cách xử lý đúng: kiểm tra if (!mounted) return; trước khi dùng context sau await. Cách này thể hiện bạn đã từng gặp lỗi thật, không chỉ đọc lý thuyết.
6. Key dùng để làm gì? Khi nào cần GlobalKey?
Key giúp Flutter nhận diện widget nào là widget nào khi danh sách thay đổi thứ tự, thêm hoặc xoá phần tử. Thiếu key, trạng thái có thể "nhảy" sang phần tử khác — kinh điển là bug trong danh sách ô nhập liệu.
GlobalKey dùng khi cần truy cập trực tiếp vào state của widget ở nơi khác trong cây, hoặc lấy kích thước/vị trí của widget. Dùng tiết chế vì nó phá tính phân tách và khó test.
7. const widget có tác dụng gì?
Như đã nói ở nhóm Dart: widget const được tạo một lần, và khi widget cha rebuild, Flutter so sánh và bỏ qua việc build lại nhánh đó. Đặt const cho những phần tĩnh là cách tối ưu chi phí thấp nhất.
8. hot reload và hot restart khác gì nhau?
hot reload giữ nguyên state hiện tại và chỉ nạp lại code đã đổi — rất nhanh. hot restart khởi động lại app, xoá toàn bộ state. Khi bạn sửa initState, thêm biến toàn cục, hoặc thay đổi cấu trúc khởi tạo mà hot reload không thấy kết quả, đó chính là lúc cần hot restart.
Nhóm 3 — State management (8 câu)
Đây là nhóm câu hỏi phân biệt mạnh nhất giữa ứng viên junior và middle. Đừng học thuộc danh sách thư viện, hãy học lý do.
1. Khi nào thì không cần thư viện quản lý trạng thái?
Khi trạng thái chỉ phục vụ một màn hình và chỉ sống trong màn hình đó — ví dụ trạng thái đóng/mở của một bộ lọc trong trang. setState là đủ và là lựa chọn đúng.
Trả lời được câu này cho thấy bạn biết tiết chế, và đây chính là điều người phỏng vấn muốn nghe ở một ứng viên có kinh nghiệm.
2. Các cách quản lý state phổ biến khác nhau gì?
- Provider / InheritedWidget: đưa dữ liệu xuống cây, đơn giản, dễ học. Hợp dự án nhỏ và vừa.
- Riverpod: cùng triết lý như Provider nhưng không phụ thuộc
BuildContext, hỗ trợ compile-time safety, test thuận lợi hơn. - Bloc / Cubit: luồng rõ ràng theo hướng sự kiện → trạng thái, có tính ràng buộc cao, phù hợp dự án nhiều nghiệp vụ và cần test luồng.
- GetX: rất ít code, gộp nhiều thứ (state, điều hướng, tiêm phụ thuộc), nhưng dễ làm code khó lần theo nếu team không đặt quy ước.
Bảng so sánh chi tiết ở bài so sánh Provider, Riverpod, Bloc, GetX.
3. Vì sao dùng Bloc mà không dùng Provider?
Câu trả lời tốt là nêu bối cảnh, không phải danh sách ưu điểm suông: "Khi luồng trạng thái có nhiều bước và nhiều nhánh, ví dụ đăng ký nhiều bước, hoặc cần audit lại vì sao giao diện ở trạng thái này, tôi chọn Bloc vì luồng sự kiện ghi lại được và test được theo từng bước. Với màn hình CRUD đơn giản, Bloc lại tạo quá nhiều file không cần thiết".
4. Khi nào nên đưa state lên trên (lift state up)?
Khi có từ hai widget trở lên cần đọc/ghi cùng một dữ liệu, hoặc khi state phải tồn tại lâu hơn vòng đời một màn hình (ví dụ giỏ hàng, thông tin đăng nhập).
5. Làm sao tránh rebuild toàn bộ cây?
Chọn đúng phạm vi lắng nghe: dùng Consumer/Selector/context.select để chỉ widget cần thiết rebuild; tách nhỏ widget để phạm vi rebuild hẹp; đặt const cho phần tĩnh; và tránh gọi provider ở widget cấp cao nhất nếu chỉ một nhánh nhỏ cần.
6. ChangeNotifier hoạt động thế nào?
Nó là một class có thể phát thông báo khi dữ liệu đổi (notifyListeners()). Widget đăng ký lắng nghe sẽ được build lại. Đây là nền tảng của Provider và của cách quản lý state nhẹ với ValueNotifier + ValueListenableBuilder.
7. Xử lý state sau khi đăng xuất thì thế nào?
Đây là câu hỏi tình huống rất hay gặp. Trả lời: state của phiên cũ phải bị xoá hẳn, không chỉ đổi cờ đăng nhập, để tránh rò rỉ dữ liệu người dùng trước sang phiên sau. Cách làm: reset/xoá provider, xoá token lưu trữ an toàn, huỷ các stream đang lắng nghe và điều hướng về màn hình đăng nhập.
8. Test state management thế nào?
Với Provider/Riverpod: test trực tiếp class ChangeNotifier hoặc provider mà không cần widget. Với Bloc: dùng bloc_test để kiểm tra chuỗi sự kiện → trạng thái. Điểm cần nhấn: tách logic khỏi giao diện chính là để test được, đó là lý do kiến trúc quan trọng chứ không phải để cho đẹp.
Nhóm 4 — Gọi API, bất đồng bộ và xử lý lỗi (7 câu)
1. Bạn dùng gì để gọi API và vì sao?
http đủ cho nhu cầu đơn giản; dio mạnh hơn nhờ interceptor, timeout, huỷ request, theo dõi tiến trình tải. Với dự án thật, interceptor là lý do chính: bạn cần gắn token vào mọi request, và xử lý 401 ở một chỗ thay vì rải khắp code.
2. Xử lý 401 hết hạn token thế nào cho đúng?
Cách làm chuẩn: khi gặp 401, dùng refresh token để lấy token mới, hàng đợi các request đang chờ, sau khi có token mới thì chạy lại chúng. Điểm quan trọng cần nói: tránh việc nhiều request cùng lúc kích hoạt refresh nhiều lần dẫn tới vòng lặp. Đây là câu hỏi rất thực chiến.
3. try/catch quanh await bắt được gì?
try/catch bắt được exception ném ra trong await. Nhưng lưu ý: nếu bạn không await mà để Future chạy nền, lỗi sẽ không bị bắt ở đó. Đây là bug kinh điển khi refactor.
4. Phân biệt lỗi mạng và lỗi nghiệp vụ?
Lỗi mạng (timeout, mất kết nối) cần thông báo "kiểm tra kết nối" và cho thử lại. Lỗi nghiệp vụ (sai mật khẩu, hết hạn dùng thử) cần thông báo cụ thể từ server. Gộp hai loại vào một câu "Có lỗi xảy ra" là lỗi trải nghiệm rất hay gặp.
5. Bạn mô hình hoá kết quả trả về thế nào?
Nhiều team dùng một kiểu bao như Result<T> với hai nhánh thành công/thất bại (Dart hỗ trợ sealed class để trình biên dịch buộc bạn xử lý đủ nhánh). Cách này buộc xử lý lỗi ngay tại chỗ thay vì quên.
6. Có nên parse JSON trên luồng chính?
Với payload nhỏ thì không sao. Với JSON vài MB (danh sách dài), nên đẩy parse sang compute/Isolate, nếu không giao diện sẽ khựng vài khung hình.
7. Hủy request khi người dùng rời màn hình?
Nên, để tiết kiệm pin/dữ liệu và tránh cập nhật giao diện của màn hình đã bị xoá. dio có CancelToken; với http thì dùng http.Client và close().
Nền tảng gọi API, dựng lớp service và tách khỏi UI được trình bày ở bài tích hợp API trong Flutter.
Nhóm 5 — Lưu trữ dữ liệu và hoạt động ngoại tuyến (5 câu)
1. Phân biệt các lựa chọn lưu trữ cục bộ?
SharedPreferences: khoá–giá trị đơn giản, hợp lưu cờ cài đặt.Hive: khoá–giá trị nhanh, lưu object, không cần SQL.sqflite/drift: cơ sở dữ liệu quan hệ, hợp dữ liệu có truy vấn phức tạp.flutter_secure_storage: lưu token, dùng keychain/keystore của hệ điều hành.
So sánh chi tiết ở bài so sánh Hive, SharedPreferences, SQLite.
2. Token đăng nhập nên lưu ở đâu?
flutter_secure_storage, không phải SharedPreferences. Lý do: SharedPreferences là file văn bản, thiết bị đã root/jailbreak có thể đọc được. Cách lưu token bằng Hive có ví dụ ở bài lưu token đăng nhập, nhưng khi làm sản phẩm thật thì ưu tiên secure storage.
3. App hoạt động ngoại tuyến thì thiết kế thế nào?
Chiến lược thường dùng: hiển thị dữ liệu đã lưu trước (cache-first), gọi API ở nền để cập nhật, đánh dấu dữ liệu cũ nếu quá hạn. Với thao tác ghi khi mất mạng, cần hàng đợi và cơ chế đồng bộ, kèm quy tắc giải quyết xung đột.
4. Vì sao cần tầng cache chứ không gọi API mọi lúc?
Ba lý do: trải nghiệm nhanh hơn, tiết kiệm dữ liệu người dùng, và giảm tải cho server. Cách làm phổ biến là repository cache theo thời gian sống (TTL).
5. Xử lý xung đột dữ liệu khi đồng bộ?
Chọn một quy tắc rõ ràng: ưu tiên bản mới nhất theo thời gian cập nhật, hoặc ưu tiên server, hoặc ưu tiên người dùng. Quan trọng là nói được rằng quy tắc này phải được thống nhất với nghiệp vụ, không phải tự chọn trong code.
Nhóm 6 — Hiệu năng và debug (6 câu)
1. App bị giật khi cuộn danh sách, bạn làm gì?
Quy trình trả lời theo thứ tự:
- Mở công cụ phân tích hiệu năng (DevTools, tab Performance) để tìm khung hình vượt ngưỡng 16ms, không đoán.
- Kiểm tra
ListView.builderđã được dùng chưa thay vìColumnchứa toàn bộ phần tử. - Kiểm tra ảnh: kích thước ảnh so với kích thước hiển thị, đã có cache chưa.
- Kiểm tra widget nào rebuild quá nhiều, thu hẹp phạm vi bằng
const,RepaintBoundary,Selector. - Nếu có tính toán nặng trong
build, chuyển ra ngoài hoặc chạy trên Isolate.
Điểm quan trọng nhất là bước 1: nói rằng bạn đo trước khi sửa. Đây là điều phân biệt kỹ sư với người thử–sai.
2. ListView.builder khác ListView thường thế nào?
ListView thường dựng toàn bộ phần tử con ngay. ListView.builder chỉ dựng phần tử khi sắp hiển thị, nên danh sách dài không tốn bộ nhớ.
3. RepaintBoundary dùng khi nào?
Dùng để cô lập một vùng giao diện vẽ lại độc lập, tránh việc một phần tử động làm vẽ lại cả vùng lớn. Nhưng đừng rải khắp nơi: mỗi boundary tốn bộ nhớ và có chi phí, chỉ đặt khi đo thấy cần.
4. Làm sao phát hiện rò rỉ bộ nhớ?
Tìm các tài nguyên đáng lẽ phải giải phóng trong dispose: AnimationController, TextEditingController, ScrollController, Timer, subscription Firebase, listener. Nếu màn hình bị mở/đóng nhiều lần và bộ nhớ tăng liên tục, đó là dấu hiệu rò rỉ.
5. Kích thước app quá lớn, giảm thế nào?
Các hướng chính: dùng App Bundle thay APK, bật thu nhỏ và loại bỏ code không dùng khi build bản phát hành, nén và chọn đúng định dạng ảnh, tách cấu hình theo từng loại thiết bị (ABI split), và rà lại các thư viện ít dùng nhưng nặng.
6. Làm sao debug một lỗi chỉ xảy ra trên máy thật, không xảy ra trên máy ảo?
Các hướng: xem log của bản phát hành (Crashlytics/Sentry), kiểm tra khác biệt về quyền truy cập và phiên bản hệ điều hành, kiểm tra cấu hình obfuscate/ProGuard có thể làm hỏng reflection, và dùng bản build release để tái hiện vì một số lỗi chỉ xuất hiện khi tắt chế độ debug.
Kỹ thuật tối ưu chi tiết hơn ở bài tối ưu hiệu năng Flutter.
Nhóm 7 — Testing và quy trình phát hành (5 câu)
1. Các loại test trong Flutter?
unit test cho logic thuần, widget test cho giao diện và tương tác trong môi trường mô phỏng, golden test so sánh ảnh để phát hiện thay đổi giao diện ngoài ý muốn, integration test chạy trên thiết bị cho luồng đầu–cuối. Tổng quan ở bài giới thiệu các loại test.
2. Bạn test cái gì trước khi test cái gì?
Ưu tiên test logic nghiệp vụ và các hàm biến đổi dữ liệu trước, vì đó là nơi bug gây thiệt hại thật và test lại rẻ, chạy nhanh. Test giao diện chọn lọc ở những luồng quan trọng (đăng nhập, thanh toán).
3. Mock service gọi API thế nào?
Tách lớp service sau một interface, rồi trong test thay bằng bản giả. Ví dụ cụ thể ở bài mock API để test và bài viết unit test cho service gọi API.
4. Quy trình phát hành app của bạn gồm những gì?
Trả lời theo các bước thật: tăng số phiên bản, build App Bundle, chạy test, đóng gói và ký bằng keystore (không commit keystore vào repo), tải lên cửa hàng, phát hành theo tỷ lệ để theo dõi lỗi, xem báo cáo sự cố rồi mở rộng dần. Các bước đóng gói có ở bài build APK/AAB.
5. Tự động hoá build và phát hành thế nào?
Dùng CI (ví dụ GitHub Actions) để mỗi lần merge vào nhánh chính sẽ chạy kiểm tra, test và build bản thử để nội bộ cài. Quy trình đầy đủ ở bài Continuous Deployment với GitHub Actions.
Nhóm 8 — Kiến trúc và tổ chức code (6 câu)
1. Bạn tổ chức thư mục dự án thế nào?
Cách phổ biến và dễ mở rộng: chia theo tính năng (features/auth, features/order), mỗi tính năng có presentation, domain, data. Với dự án nhỏ, chia theo lớp (screens, widgets, services, models) là đủ — và nói được rằng bạn chọn theo quy mô dự án sẽ ghi điểm hơn là khẳng định một cách duy nhất.
2. MVVM và Clean Architecture khác nhau thế nào?
MVVM tách giao diện khỏi trạng thái hiển thị (ViewModel), gọn và dễ áp dụng. Clean Architecture chia ba lớp với quy tắc phụ thuộc một chiều, kiểm soát tốt hơn khi nghiệp vụ phức tạp, đổi nguồn dữ liệu, hoặc cần test kỹ — nhưng nhiều lớp và nhiều code hơn. So sánh ở bài MVVM và Clean Architecture.
3. Khi nào kiến trúc trở thành "quá đà"?
Khi bạn tạo nhiều lớp chỉ để chuyển dữ liệu mà không có logic, hoặc khi một màn hình đơn giản cần 6 file mới thêm một trường. Dấu hiệu nhận biết: chi phí thay đổi một tính năng nhỏ tăng lên nhưng tỷ lệ lỗi không giảm.
4. Repository pattern để làm gì?
Nó tạo một điểm truy cập dữ liệu duy nhất cho tầng trên, giấu chuyện dữ liệu đến từ API, cache hay cơ sở dữ liệu cục bộ. Nhờ vậy đổi nguồn dữ liệu hoặc viết test giả không ảnh hưởng giao diện.
5. Tiêm phụ thuộc (DI) trong Flutter?
Dùng get_it hoặc provider để đăng ký và lấy phụ thuộc. Lợi ích thật: test thay được bản giả, và tránh việc một màn hình tự tạo Dio() hay ApiClient() bên trong.
6. Dự án nhiều người làm thì quy ước gì quan trọng?
Quy ước đặt tên, lint và format tự động trong CI, quy tắc review, và kiểm soát được sự phụ thuộc giữa các tính năng. Xem thêm bài kiến trúc module.
Nhóm 9 — Câu hỏi tình huống và thiết kế (5 câu)
Với vị trí từ middle trở lên, đây là nhóm quyết định mức lương.
1. Thiết kế màn hình danh sách sản phẩm có tìm kiếm, lọc, phân trang?
Hướng trả lời: mô tả lớp dữ liệu (repository với API + cache), trạng thái màn hình (đang tải, có dữ liệu, rỗng, lỗi), luồng phân trang (con trỏ hoặc số trang, chống gọi trùng, có tải thêm), tìm kiếm có chống dội (debounce), và điểm cần chú ý là huỷ request cũ khi người dùng gõ tiếp.
2. Ứng dụng chat thời gian thực cần chú ý gì?
Dữ liệu realtime, trạng thái gửi tin (đang gửi/đã gửi/đã nhận/đã đọc), xử lý mất mạng và gửi lại, sắp xếp theo thời gian, phân trang lịch sử, thông báo đẩy, và dọn subscription khi rời màn hình. Ví dụ ở bài chat realtime với Firebase.
3. Cần đăng nhập nhiều nền tảng (email, Google, Apple) thì tổ chức thế nào?
Tách lớp xác thực sau một interface chung, mỗi phương thức là một triển khai; luồng sau đăng nhập dùng chung. Nhớ yêu cầu riêng của từng nền tảng, ví dụ quy định bắt buộc với đăng nhập Apple nếu app có đăng nhập mạng xã hội khác. Tham khảo bài xác thực Firebase.
4. App phải hỗ trợ cả điện thoại nhỏ và tablet, bạn làm gì?
Dùng layout thích ứng theo kích thước (MediaQuery, LayoutBuilder), chọn breakpoint theo nội dung, và kiểm tra bằng cách đổi kích thước. Xem bài LayoutBuilder và MediaQuery.
5. Bạn xử lý khi cần thêm tính năng phải gọi API native chưa có package?
Viết platform channel (MethodChannel) hoặc dùng plugin, trao đổi dữ liệu qua kiểu đơn giản, và xử lý lỗi ở cả hai phía. Điểm cộng khi nói được rằng bạn sẽ tìm package có sẵn trước, vì tự viết cầu nối native làm tăng chi phí bảo trì.
Nhóm 10 — Câu hỏi hành vi và câu hỏi ngược lại
1. Kể về một bug khó nhất bạn từng gặp?
Trả lời theo cấu trúc: bối cảnh → triệu chứng → cách bạn khoanh vùng → giả thuyết → xác minh → bài học. Đừng chỉ kể "em sửa được". Người phỏng vấn muốn thấy quy trình suy luận.
2. Bạn xử lý thế nào khi không đồng ý với quyết định kỹ thuật của team?
Nêu dữ liệu thay vì cảm giác: đo thời gian build, đo số dòng phải sửa khi thêm tính năng, chỉ ra rủi ro cụ thể. Và nêu rõ bạn chấp nhận quyết định chung sau khi đã trình bày, trừ khi có rủi ro nghiêm trọng.
3. Bạn học công nghệ mới thế nào?
Trả lời cụ thể: đọc tài liệu chính thức, làm một dự án nhỏ, rồi áp dụng vào dự án thật ở mức rủi ro thấp. Nói được một ví dụ bạn đã làm sẽ thuyết phục hơn mô tả chung.
4. Bạn nên hỏi ngược nhà tuyển dụng những gì?
Hỏi những câu cho thấy bạn quan tâm chất lượng sản phẩm: quy trình phát hành hiện tại thế nào, có CI không, viết test đến mức nào, ai review code, quy trình xử lý lỗi từ người dùng ra sao, và cấu trúc dự án hiện tại theo hướng nào.
Tránh hỏi lương ở vòng kỹ thuật, và tránh câu "công ty có gì vui không". Vài câu hỏi tốt ở cuối buổi phỏng vấn thường để lại ấn tượng tốt hơn cả phần đầu.
Live coding: các dạng bài thường gặp và cách luyện
Ba dạng bài xuất hiện nhiều nhất:
- Dựng một màn hình từ dữ liệu: cho một API giả, yêu cầu hiển thị danh sách có trạng thái đang tải, lỗi, rỗng và kéo để làm mới. Hãy viết theo thứ tự: model → service → trạng thái → giao diện, và nói ra bạn đang làm gì.
- Bài tập Dart thuần: đảo ngược chuỗi, đếm ký tự, xử lý danh sách, sắp xếp. Mục tiêu là kiểm tra bạn viết code sạch và biết xử lý trường hợp biên (danh sách rỗng, dữ liệu null).
- Sửa lỗi có sẵn: đưa một đoạn code có bug (thiếu
dispose, dùngcontextsauawait,ListViewtrongColumn). Đây là dạng dễ mất điểm nhất, vì bạn phải vừa giải thích vừa sửa.
Ba nguyên tắc khi live coding: nói ra suy nghĩ của mình, chia việc thành bước nhỏ rồi làm xong từng bước, và chủ động nêu điểm cần cải thiện sau khi chạy được. Không ai kỳ vọng code hoàn hảo ngay lần đầu; họ đánh giá cách bạn xử lý khi bị kẹt.
10 lỗi khiến ứng viên Flutter bị loại
- Chỉ trả lời định nghĩa, không giải thích vì sao hoặc khi nào dùng.
- Không biết
dispose, để rò rỉ controller và subscription. - Dùng
contextsauawaitmà không kiểm tra widget còn tồn tại. - Chọn thư viện theo phong trào, không nêu được lý do theo bối cảnh dự án.
- Không đo trước khi tối ưu, phỏng đoán hiệu năng.
- Code dồn hết vào một file, không tách được phần nào gọi dữ liệu, phần nào hiển thị.
- Không có kiến thức phát hành, không biết ký app, không biết App Bundle là gì.
- Không có gì để giới thiệu: không repo, không app đã publish, không bài viết kỹ thuật.
- Không hỏi ngược, thể hiện thiếu quan tâm đến sản phẩm và quy trình của công ty.
- Giấu việc mình không biết, thay vì nói "phần này em chưa làm, nhưng em sẽ tìm theo hướng...". Ứng viên nói thẳng mà có hướng suy luận luôn được đánh giá cao hơn.
Lộ trình ôn phỏng vấn Flutter trong 3 tuần
Tuần 1 — Nền tảng: ôn Dart (null safety, async, collections, isolate), vòng đời widget, ba cây widget, BuildContext. Với mỗi chủ đề, viết một đoạn code nhỏ để chứng minh mình hiểu.
Tuần 2 — Ứng dụng thật: chọn một bài tập dạng "gọi API, có cache, có phân trang", làm xong và viết README giải thích lựa chọn kiến trúc. Đây sẽ là thứ bạn kể trong phỏng vấn.
Tuần 3 — Mô phỏng và sửa lỗ hổng: tự trả lời thành tiếng toàn bộ câu hỏi trong bài này, ghi âm lại và nghe. Chuẩn bị 3 câu chuyện về bug đã sửa, 5 câu hỏi ngược nhà tuyển dụng, và một bản giải thích 2 phút về dự án tâm đắc nhất.
Nếu bạn muốn đi nhanh hơn, cách hiệu quả nhất là có người phản biện code mình viết hằng tuần. Luyện một mình rất dễ bỏ qua lỗi mà người phỏng vấn nhìn ra trong 5 phút.
Học Flutter để đi làm: đi theo lộ trình nào?
Phỏng vấn chỉ là phần cuối. Điều quyết định bạn vượt qua được vòng kỹ thuật vẫn là nền tảng và việc đã tự làm sản phẩm hoàn chỉnh.
Các cấp độ học tại Hướng Nghiệp Flutter:
- Flutter Cơ Bản — Dart, widget, giao diện, dự án đầu tiên.
- Flutter Nâng Cao — quản lý trạng thái, gọi API, kiến trúc dự án thật.
- Flutter Chuyên Sâu — tối ưu hiệu năng, testing, phát hành, tích hợp AI.
- Lịch khai giảng — các lớp sắp mở và đăng ký.
- Cam kết đầu ra & cơ hội việc làm — hỗ trợ thực tập và tuyển dụng sau khóa học.
Nếu bạn muốn tìm hiểu thêm về hướng đi nghề nghiệp, xem học lập trình mobile ở đâu và lộ trình fullstack Flutter + AI và lập trình viên mobile cần học những gì. Toàn bộ bài viết kỹ thuật nằm ở mục Bài viết, và thông tin đội ngũ giảng viên ở trang giảng viên.
Câu hỏi thường gặp
Phỏng vấn Flutter có cần bằng đại học không?
Phần lớn công ty tuyển theo năng lực và sản phẩm đã làm. Bằng cấp giúp ở một số công ty lớn hoặc khi xét lương bậc, nhưng bài test kỹ thuật và phần giải thích kiến trúc vẫn là yếu tố quyết định.
Cần biết native (Kotlin/Swift) ở mức nào?
Ở mức đủ để debug: đọc được log native, biết cách thêm quyền trong manifest/plist, biết sửa lỗi build liên quan tới Gradle hoặc CocoaPods, và biết khi nào cần viết cầu nối. Không cần viết được app native hoàn chỉnh.
Không có kinh nghiệm công ty thì có được nhận không?
Được, nếu bạn có sản phẩm đã hoàn thành. Hai thứ tạo khác biệt: một app đã publish lên cửa hàng (dù nhỏ) và một repo có README giải thích lựa chọn kỹ thuật. Nhà tuyển dụng cần thấy bạn từng làm xong, không chỉ từng học.
Phỏng vấn Flutter có hỏi thuật toán không?
Thường ở mức cơ bản: xử lý chuỗi, danh sách, độ phức tạp đơn giản. Một số công ty hỏi sâu hơn nhưng thường vẫn gắn với bài toán thực tế như sắp xếp dữ liệu lớn, tìm kiếm trong danh sách dài.
Nên học thêm gì sau khi đã đi làm Flutter?
Ba hướng đáng đầu tư nhất: kiến trúc và khả năng test (để nhận vai trò senior), tích hợp AI vào sản phẩm (nhu cầu đang tăng), và tối ưu phát hành (để làm được cả vai trò build/release). Ví dụ tích hợp AI có ở bài tích hợp Gemini vào app Flutter.
Tóm lại: câu hỏi phỏng vấn Flutter gần như luôn xoay quanh việc bạn hiểu nguyên nhân, biết trade-off và đã gặp vấn đề thật hay chưa. Học thuộc định nghĩa không giúp được gì nhiều. Hãy chọn một dự án nhỏ, làm cho xong, rồi tự hỏi "vì sao?" với từng lựa chọn bạn đã đưa ra — đó chính là phần lớn nội dung của một buổi phỏng vấn kỹ thuật.
