Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
Flutter
var cho phép Dart tự suy luận kiểu dữ liệu một lần lúc khai báo và không thể thay đổi kiểu sau đó. dynamic cho phép kiểu thay đổi trong runtime, bỏ qua kiểm tra kiểu tĩnh. final tạo biến không thể gán lại sau khi khởi tạo.
Dùng var cho biến cục bộ rõ ràng, hạn chế dynamic vì phá vỡ type safety, dùng final cho giá trị không đổi.
Null safety nghĩa là các biến không thể chứa null trừ khi được đánh dấu nullable bằng ?.
- Điều này giúp phòng ngừa lỗi
NullPointerExceptionở runtime bằng cách phát hiện lỗi ngay lúc compile. - Với null safety, lập trình viên phải có chủ đích rõ ràng về biến nào có thể null, giúp code an toàn hơn và trình biên dịch tối ưu hiệu quả hơn.
Toán tử ! yêu cầu Dart xử lý biến nullable như non-nullable mà không kiểm tra.
Ví dụ: String name = nullableName! — điều này khẳng định nullableName không phải null. Chỉ dùng khi bạn chắc chắn 100% biến không null; nếu sai sẽ ném lỗi runtime. Lạm dụng ! thường cho thấy thiết kế chưa tốt.
Toán tử null-aware giúp xử lý giá trị nullable an toàn. ?. truy cập thuộc tính chỉ khi đối tượng khác null: person?.name. ?? cung cấp giá trị mặc định nếu bên trái là null: name ?? "Unknown". ??= chỉ gán nếu biến đang là null: count ??= 0.
Các toán tử này giúp code ngắn gọn và tránh lỗi null.
List là collection có thứ tự và cho phép trùng lặp: [1, 2, 2, 3]. Set là collection không có thứ tự với các phần tử duy nhất: {1, 2, 3}. Map lưu trữ cặp key-value: {"name": "John", "age": 30}.
Chọn List khi thứ tự quan trọng, Set cho giá trị duy nhất và tra cứu O(1), Map cho truy xuất theo key.
Future đại diện cho một giá trị bất đồng bộ duy nhất sẽ có trong tương lai — giống như một lời hứa cho một kết quả. Stream đại diện cho nhiều sự kiện bất đồng bộ theo thời gian — như một chuỗi các giá trị liên tục.
Dùng Future cho tác vụ một lần (gọi API), dùng Stream cho dữ liệu liên tục (cảm biến, WebSocket).
async đánh dấu hàm là bất đồng bộ; await tạm dừng thực thi cho đến khi Future hoàn thành.
Điều này giúp code bất đồng bộ đọc dễ như code đồng bộ. await chỉ hoạt động trong hàm async.
Future<String> fetchData() async {
String data = await api.get();
return data;
}Type promotion là khi Dart tự động thu hẹp kiểu của biến dựa trên luồng điều khiển.
- Nếu bạn có
String?và kiểm traif (name != null), trong khối đó Dart coinamelàStringnon-nullable mà không cần!. - Điều này xảy ra tự động với kiểm tra null,
ischecks, và toán tử logic, giúp giảm boilerplate.
Positional parameters được truyền theo thứ tự: void greet(String name, int age) gọi là greet("John", 30).
- Named parameters dùng dấu ngoặc nhọn:
void greet({String? name, int? age})gọi làgreet(name: "John", age: 30). - Named parameters có thể tùy chọn; kết hợp từ khóa
requiredđể bắt buộc chúng.
Closure là một hàm "đóng gói" (capture) các biến từ phạm vi bao quanh và có thể truy cập chúng ngay cả sau khi hàm ngoài đã kết thúc.
Ví dụ: int makeAdder(int x) { return (int y) => x + y; } — hàm bên trong "giữ lại" biến x. Closure rất hữu ích cho callbacks và lập trình hàm trong Dart.
extends kế thừa từ lớp cha, tái sử dụng code: class Dog extends Animal. implements coi lớp như một interface và buộc override tất cả method (không tái sử dụng code): class Dog implements Animal. with thêm hành vi mixin mà không cần kế thừa: class Dog with Sound.
Dùng extends cho "là một", implements cho "hành động như", with để chia sẻ code.
StatelessWidget là immutable — sau khi build xong, nó không thể thay đổi.
- Hàm
build()chỉ được gọi một lần trừ khi parent rebuild.StatefulWidgetduy trì state có thể thay đổi quasetState(), kích hoạt rebuild. - Dùng
StatelessWidgetcho UI tĩnh (nhãn văn bản, icon),StatefulWidgetcho component tương tác (form, toggle). - Luôn ưu tiên
StatelessWidgetvì hiệu năng tốt hơn.
BuildContext là tham chiếu đến vị trí của widget trong cây widget, cung cấp quyền truy cập vào các dịch vụ như Theme, MediaQuery, Navigator và ScaffoldState.
- Mọi widget đều có
BuildContextđược truyền vào hàmbuild(). - Nó cần thiết để điều hướng, hiển thị dialog, truy cập dữ liệu theme và đọc thuộc tính thiết bị.
Tree-shaking là quá trình tự động loại bỏ code, class, method không được dùng trong quá trình build. Flutter chỉ giữ lại code reachable từ main().
Ví dụ: nếu bạn import một package lớn nhưng chỉ dùng một hàm, chỉ hàm đó được đưa vào app. Tree-shaking chỉ chạy ở release mode (flutter build apk --release). Mức độ giảm phụ thuộc vào số package lớn có nhiều code không dùng — app dùng ít package nhỏ có thể không thấy nhiều khác biệt. Để phân tích xem code nào còn lại, dùng --analyze-size hoặc DevTools Size Analyzer.
Widget tree mô tả cấu trúc UI (blueprint bất biến).
- Element tree theo dõi các widget tồn tại và vòng đời của chúng (có thể thay đổi).
- Render tree xử lý layout và vẽ lên màn hình.
- Khi gọi
setState(), widget rebuild, element cập nhật tham chiếu, và render tree chỉ vẽ lại vùng bị ảnh hưởng. - Hiểu sự tách biệt này giải thích tại sao Flutter hiệu quả.
Key giúp bảo tồn state của widget khi thứ tự danh sách con thay đổi.
- Không có key, Flutter khớp widget theo kiểu và vị trí, dẫn đến lẫn lộn state.
- Dùng
ValueKeycho giá trị đơn giản,ObjectKeycho đối tượng phức tạp. - Cần thiết cho
ListView.builder, animated lists hoặc drag-drop. - Không có key khi reorder danh sách
StatefulWidgetsẽ hoán đổi state của chúng.
ValueKey xác định widget bằng một giá trị cụ thể; hai widget cùng giá trị được coi là giống nhau. ObjectKey dùng tham chiếu danh tính của đối tượng; mỗi đối tượng duy nhất có key riêng. UniqueKey luôn tạo danh tính duy nhất, hữu ích khi muốn mỗi instance khác biệt.
Tránh tạo UniqueKey trong build() — điều đó phá vỡ mục đích bảo tồn state.
const constructor tạo hằng số compile-time, bất biến và có thể tái sử dụng.
- Widget
constbỏ qua rebuild nếu tham số không đổi, cải thiện hiệu năng đáng kể. - Flutter có thể gộp các đối tượng
constgiống nhau thành một instance, giảm bộ nhớ. - Luôn dùng
constcho widget với tham số cố định:const Text("Hello"). - Tránh tạo
UniqueKeytrongconstvì sẽ mất lợi ích.
Column, Row, Expanded và Flexible dùng ra sao?Row xếp widget theo chiều ngang; Column xếp theo chiều dọc.
- Mặc định chúng chiếm không gian tối thiểu.
Expandedbuộc con lấp đầy không gian còn lại đều nhau.Flexiblecho phép con chiếm thêm không gian nhưng có thể nhỏ hơn nếu cần. - Dùng
MainAxisAlignmentvàCrossAxisAlignmentđể kiểm soát khoảng cách và căn chỉnh.
setState() lên lịch rebuild subtree cho frame tiếp theo — chỉ dùng cho state UI cục bộ đơn giản, không dùng cho animation hay stream.
- Nó đánh dấu widget cần rebuild và thông báo cho Flutter framework; thay đổi state bên trong callback được phản ánh trong lần build tiếp theo.
- Không dùng
setState()cho cập nhật liên tục (animation, stream); dùngAnimatedBuilderhoặc state management thay thế.
initState() — gọi một lần khi widget được thêm vào (khởi tạo controller, listener). didChangeDependencies() — gọi sau initState và khi dependency thay đổi. build() — gọi thường xuyên, trả về widget tree. didUpdateWidget() — gọi khi parent rebuild với thuộc tính khác. dispose() — gọi một lần khi xóa (dọn dẹp tài nguyên, đóng stream).
Nắm rõ vòng đời này để tránh memory leak.
dispose() được gọi khi widget bị xóa vĩnh viễn khỏi cây.
- Cần dọn dẹp tài nguyên để tránh memory leak: đóng stream, dispose animation controller, hủy đăng ký change notifier, hủy timer.
- Không dispose đúng cách khiến app giữ tham chiếu đến object đã chết, dần dần tiêu tốn bộ nhớ cho đến khi app crash.
Hot reload bảo tồn state của app và tải lại code nhanh, chỉ chạy lại build() mà không chạy lại initState() hay main().
- Hot restart phá hủy toàn bộ state, chạy lại
main()vàinitState(), biên dịch lại app hoàn toàn. - Dùng hot reload cho chỉnh sửa UI (~100ms), hot restart khi thay đổi khởi tạo state hoặc định nghĩa class (~1-2s).
RepaintBoundary cô lập một subtree để nó vẽ lại độc lập mà không ảnh hưởng toàn bộ cây.
- Dùng nó xung quanh widget vẽ lại thường xuyên (animation, progress indicator).
- Khi con của
RepaintBoundarythay đổi, chỉ subtree đó mới vẽ lại. - Lạm dụng tạo quá nhiều boundary và giảm hiệu năng; chỉ dùng tiết kiệm cho các điểm hot đã được xác định.
State management là cách xử lý dữ liệu thay đổi trong app (input người dùng, kết quả API).
- Quản lý state kém gây ra bug, memory leak và code khó bảo trì.
- Quản lý state tốt tách UI khỏi logic, giúp test dễ dàng và tái sử dụng code.
- Khi app lớn, chọn đúng chiến lược (
setState, Provider, Riverpod, BLoC) trở nên thiết yếu.
setState() rebuild toàn bộ cây con của widget đó, gây vấn đề hiệu năng với cây widget lớn.
- Logic nghiệp vụ trộn lẫn với UI làm code khó test.
- Không scale được — nhiều cập nhật state làm code rối rắm khó debug.
- State bị giới hạn trong một widget — chia sẻ state giữa các widget ở xa trở nên cực kỳ phức tạp.
- Với bất kỳ thứ gì ngoài widget đơn giản, hãy dùng state management chuyên dụng.
Provider là thư viện state management dùng ChangeNotifier để thông báo listener khi state thay đổi.
- Widget lắng nghe qua
Consumerhoặccontext.watch<T>()và chỉ rebuild khi dữ liệu chúng phụ thuộc thay đổi. - Provider nhẹ và phù hợp cho app nhỏ đến vừa, nhưng Flutter team hiện nay khuyến nghị Riverpod cho dự án mới vì type-safe hơn và không phụ thuộc
BuildContext. - Bọc app với
MultiProvider, định nghĩa provider cho data, và consume trong widget.
Riverpod (kế thừa tinh thần của Provider, cùng tác giả) hoàn toàn type-safe, không phụ thuộc BuildContext, và dùng functional provider.
- Riverpod sinh code lúc compile-time giúp phòng ngừa nhiều bug.
- Riverpod hỗ trợ parameterized provider, test tốt hơn và auto-dispose.
- Dùng Riverpod cho project mới; hiện đại hơn Provider nhưng learning curve hơi cao hơn.
BLoC (Business Logic Component) tách logic nghiệp vụ khỏi UI thông qua kiến trúc event-in/state-out.
- Bạn dispatch
Eventvào BLoC, BLoC xử lý và emit raStatemới. - BLoC có thể test được, đảm bảo kiến trúc nhất quán, và scale tốt trong team lớn.
- BLoC cần boilerplate nhiều hơn Provider nhưng cung cấp cấu trúc và khả năng truy vết tốt hơn cho app phức tạp.
Provider: app đơn giản, setup nhanh, hỗ trợ chính thức.
- Riverpod: type-safe, hiện đại, test tốt hơn, parameterized provider, xu hướng ngày càng phổ biến.
- BLoC: team lớn, app phức tạp, cần truy vết event, các ngành có quy định chặt (ngân hàng).
- Không có lựa chọn "tốt nhất" — phải phù hợp giải pháp với vấn đề.
- Interviewer muốn bạn hiểu đánh đổi.
ChangeNotifier là class đơn giản thông báo listener khi state thay đổi thông qua notifyListeners(). extend ChangeNotifier để tạo đối tượng observable.
- Provider theo dõi
ChangeNotifiervà rebuild khinotifyListeners()được gọi. - Nó nhẹ nhưng cần quản lý thông báo thủ công.
- Cho state đơn giản (theme, auth, user data),
ChangeNotifierthường đủ dùng.
Consumer theo dõi toàn bộ provider và rebuild bất cứ khi nào nó thay đổi. Selector cho phép chỉ theo dõi một phần cụ thể của state: Selector<UserProvider, String>(selector: (_, user) => user.name, ...) chỉ rebuild khi name thay đổi, không phải khi age thay đổi.
Kiểm soát chi tiết này ngăn rebuild không cần thiết và cải thiện hiệu năng trong provider phức tạp.
GetIt là service locator cho dependency injection.
- Bạn đăng ký singleton hoặc factory:
getIt.registerSingleton<Repository>(Repository()), rồi truy cập ở bất kỳ đâu:getIt<Repository>(). - Bản thân nó không phải state management mà giúp clean architecture bằng cách quản lý dependency.
- Thường dùng kết hợp BLoC hoặc Riverpod để inject repository và service vào business logic.
Provider: Mock ChangeNotifier, dùng ProviderContainer để test.
- Riverpod: Dùng
ProviderContainer, override provider bằng mock. - BLoC: Test trực tiếp với
bloc_test, xác minh event → state transition. - Không bao giờ phụ thuộc
BuildContexttrong code cần test; tách logic nghiệp vụ vào service class. - State management tốt thì test dễ; nếu test khó thì kiến trúc cần xem lại.
Navigator v1 dùng lệnh imperative Navigator.push() và Navigator.pop() — bạn quản lý navigation stack thủ công.
- Navigator 2.0 giới thiệu declarative routing qua Router API.
- GoRouter (được khuyến nghị hiện nay) xây trên đó, cung cấp URL-based, type-safe declarative routing.
- GoRouter đơn giản hơn và tự động xử lý deep linking, trở thành lựa chọn hiện đại cho project mới.
GoRouter là routing package được Flutter chính thức khuyến nghị.
- Cung cấp URL-based declarative routing với path/query parameter.
- Tự động xử lý deep linking, hỗ trợ nested navigation với
ShellRoute, thêm redirect logic cho auth guard, và cho phép type-safe route với code generation. - Thay vì quản lý Navigator stack thủ công, bạn khai báo tất cả route trước và để GoRouter xử lý navigation.
Cách 1 (Navigator cũ): Truyền qua constructor: Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(item: item))).
- Cách 2 (GoRouter): Dùng path parameter:
route: "/detail/:id"và truy cập quaGoRouterState. - Cách 3 (State Management): Lưu data trong provider/BLoC, truy cập từ bất kỳ màn hình nào.
- GoRouter với path parameter là sạch nhất và hỗ trợ deep linking.
Deep linking là mở app tại màn hình cụ thể thông qua URL (như myapp://detail/123).
- Deep link đến từ push notification, web link hoặc system intent.
- GoRouter hỗ trợ deep linking tự động — định nghĩa route như path:
GoRoute(path: '/detail/:id', ...)và GoRouter khớp URL với màn hình đúng. - Điều này cho phép universal links (iOS) và app links (Android) mà không cần setup thêm.
GoRouter cung cấp GoRouterState với location, parameter, extra data: String id = state.pathParameters['id']!.
- Với nested navigation (bottom tab có stack độc lập), dùng
ShellRoute. - Với navigation dựa trên auth, dùng
redirect()để kiểm tra state auth trước khi build page:if (!isLoggedIn) return '/login'. - Điều này tập trung hóa logic navigation.
ShellRoute bọc nhiều route với một parent chia sẻ (như bottom navigation bar).
- Route bên trong
ShellRoutebuild trong context của parent đó, duy trì navigation stack độc lập cho mỗi tab. - Đây là cách đúng để triển khai bottom tab navigation: mỗi tab có lịch sử điều hướng riêng, nhấn tab hiển thị lịch sử của nó thay vì bắt đầu mới.
- Không có
ShellRoute, chuyển tab sẽ làm nút back hoạt động sai.
Dùng callback redirect để kiểm tra state auth trước khi build route.
Ví dụ: nếu user chưa đăng nhập và cố truy cập route được bảo vệ, redirect về /login. Kết hợp với Riverpod để redirect tự động phản ứng khi auth state thay đổi. Cách này xử lý tất cả route tập trung, không cần kiểm tra auth trong từng widget.
Navigator 1.0 yêu cầu quản lý navigation state và back stack thủ công.
- Named route hoạt động nhưng xử lý deep linking kém — phải thêm nhiều boilerplate.
- Navigation phức tạp (nested stack, conditional routing) trở nên rối.
- Toàn bộ navigation tree là global, khó để lý luận.
- GoRouter giải quyết tất cả bằng cách cung cấp declarative, URL-based routing tự động xử lý back stack và deep linking.
Clean architecture tách code thành các layer: Presentation (UI, widget), Domain (logic nghiệp vụ, entity, use case), Data (repository, data source).
- Mỗi layer độc lập và có thể test.
- Sự tách biệt này cho phép thay đổi UI framework, hoán đổi data source, và test logic nghiệp vụ mà không động vào UI.
- App lớn không có clean architecture sẽ trở thành spaghetti code khó bảo trì khi phát triển.
Repository là lớp abstraction giữa domain logic và data source.
- Thay vì UI gọi trực tiếp API, bạn gọi
Repository.getUser(id)— method này ẩn đi việc data đến từ network, cache hay database. - Interface repository được định nghĩa trong domain layer; implementation trong data layer.
- Điều này giúp dễ dàng hoán đổi data source, hỗ trợ offline-first, và test bằng cách mock repository.
Dependency injection là truyền dependency (service, repository) vào class thay vì tạo chúng bên trong.
- Thay vì
class UserBloc { final repo = UserRepository(); }, hãy inject:class UserBloc { UserBloc(this.repo); final UserRepository repo; }. - Điều này cho phép test (mock repository) và linh hoạt (hoán đổi implementation).
- Dùng
GetItcho service locator pattern hoặc truyền dependency qua constructor.
Singleton tạo một instance cho toàn bộ app: getIt.registerSingleton<Repository>(Repository()).
- Dùng cho tài nguyên chia sẻ (database, API client, repository).
- Factory tạo instance mới mỗi lần:
getIt.registerFactory<UserBloc>(() => UserBloc(repo)). - Dùng cho BLoC (mỗi màn hình cần state độc lập).
- Dùng sai gây state leak (chia sẻ state có thể thay đổi) hoặc lãng phí bộ nhớ.
MVC (Model-View-Controller): Controller xử lý input và cập nhật Model, View hiển thị Model.
- MVVM (Model-View-ViewModel): ViewModel expose data và logic UI cần.
- UI bind vào ViewModel.
- ViewModel không biết về UI.
- MVVM testable hơn MVC vì ViewModel không có dependency vào UI framework.
- Flutter không bắt buộc MVVM nhưng Provider + ViewModel class đạt được pattern này rất gọn.
Use case đóng gói logic nghiệp vụ cho một tính năng: GetUserUseCase, LoginUseCase.
- Mỗi use case là một class với method
call()nhận tham số và trả về kết quả. - Use case nằm ở domain layer, không phụ thuộc framework, và dễ test cao.
- Chúng nối cầu giữa UI (trigger use case) và repository (lấy data).
- Use case được thiết kế tốt có thể tái sử dụng và test độc lập.
Cấu trúc phổ biến: lib/presentation/ (UI, widget, BLoC/ViewModel), lib/domain/ (entity, repository interface, use case), lib/data/ (API client, local DB, repository implementation), lib/config/ (cấu hình app, constant).
- Mỗi feature có thể có domain/data/presentation riêng.
- Điều này giữ code có tổ chức, dễ điều hướng và test.
Entity đại diện cho khái niệm domain (đối tượng nghiệp vụ cốt lõi): User(id, name, email).
- Entity là class Dart thuần, không phụ thuộc framework và dễ test, nằm ở domain layer.
- Model là biểu diễn API/database với serialization:
UserModel.toJson(). - Model nằm ở data layer.
- Dùng entity trong business logic, model trong API/DB, và mapper function để chuyển đổi giữa chúng.
Records là kiểu dữ liệu nhẹ, bất biến, cho phép nhóm nhiều giá trị lại mà không cần tạo class.
- Bạn có thể dùng cú pháp vị trí
(String, int)hoặc tên trường({String name, int age}). - Để trả về từ hàm:
(String, int) fetchUser() => ('Alice', 30);và destructure bằngvar (name, age) = fetchUser();. - Records thay thế các workaround dùng Map hay List khi cần trả nhiều giá trị, code ngắn hơn và type-safe hơn.
Pattern matching cho phép khớp giá trị và destructure cùng lúc.
Ví dụ với switch: switch (shape) { case Circle(radius: var r) => print('Circle r=$r'); case Rectangle(width: var w, height: var h) => print('Rect ${w}x${h}'); }.
- Bạn cũng có thể thêm guard clause:
case (int a, int b) when a > b => print('$a lớn hơn $b');. - Patterns còn dùng được trong khai báo biến:
var (x, y) = getPoint();. - Giúp code declarative hơn hẳn so với chuỗi if/else.
Switch expression trả về giá trị trực tiếp và bắt buộc phải exhaustive (phủ hết mọi trường hợp).
- Cú pháp dùng
=>:String grade = score switch { > 90 => 'A', > 80 => 'B', _ => 'F' };. - Switch statement thì không trả về giá trị, phù hợp khi cần thực thi side effect.
- Dùng switch expression khi bạn muốn transform một giá trị sang giá trị khác—code gọn và compiler sẽ báo lỗi nếu thiếu case.
Impeller là rendering engine mới của Flutter, thay thế Skia.
- Mặc định trên iOS từ Flutter 3.10, và trên Android (API 29+) từ Flutter 3.27.
- Điểm khác biệt chính: Impeller pre-compile toàn bộ shader lúc build, trong khi Skia compile shader lần đầu tiên khi chạy (JIT), gây ra "shader jank"—giật hình khi người dùng lần đầu chạm vào UI.
- Impeller dùng Metal trên iOS và Vulkan trên Android, tận dụng GPU API hiện đại.
- Kết quả: frame time ổn định hơn, không còn giật đột ngột, trải nghiệm người dùng mượt hơn đáng kể.
Mặc định Flutter Web compile Dart sang JavaScript, phải qua parser và interpreter của trình duyệt.
- Với WASM, Dart compile ra native bytecode chạy trực tiếp trên VM của browser, bỏ qua parsing overhead.
- Build bằng
flutter build web --wasm. - Kết quả: tác vụ CPU-intensive chạy nhanh hơn ~1.5–3x tùy workload.
- Giới hạn: cần trình duyệt hỗ trợ WasmGC (Chrome 119+, Firefox 120+), bundle size lớn hơn JS đáng kể.
- Đây là bước ngoặt để Flutter Web cạnh tranh với React/Vue về hiệu năng.
Signals là hệ thống reactive nguyên bản lấy cảm hứng từ SolidJS, cho phép cập nhật UI theo kiểu fine-grained—chỉ widget nào phụ thuộc vào signal đó mới rebuild, không phải cả cây widget.
- Khai báo:
final count = signal(0);, dùng trong widget:Watch((context) => Text('${count.value}')). - Package
signals_flutterđã có từ 2023 và đạt 1.0; awareness rộng rãi hơn vào 2023-2024. - Riverpod phù hợp hơn cho global/shared state phức tạp, cần dependency injection.
- Signals lý tưởng cho local state performance-critical, ít boilerplate, phạm vi component nhỏ.
Flutter Web compile Dart ra JS (hoặc WASM) và render vào Canvas/HTML, không phải DOM thực. Lợi thế: tái sử dụng code với mobile, không cần học JS ecosystem.
Nhược điểm: bundle size ~3MB+ (nặng hơn SPA thông thường), SEO khó vì nội dung trong Canvas, tích hợp với thư viện DOM JS phức tạp hơn. Không dùng Flutter Web cho trang marketing cần SEO tốt. Phù hợp nhất cho app nội bộ, dashboard, hoặc tool cần chia sẻ code với mobile—nơi code reuse quan trọng hơn SEO.
Desktop yêu cầu xử lý keyboard, mouse, window management—không có trên mobile.
- Responsive layout quan trọng hơn vì màn hình lớn và DPI khác nhau.
- Truy cập native API phải qua platform channel (file system, menu bar, notifications).
- Packaging khác nhau theo platform: MSIX (Windows), DMG (macOS), tar.gz (Linux).
- Test trên phần cứng thật vì emulator không phản ánh DPI scaling thực tế.
- Tính thêm ~30% thời gian phát triển so với mobile do phức tạp hơn về UX.
Package http là HTTP client đơn giản, đủ dùng cho request cơ bản.
- Dio là HTTP client mạnh hơn với: interceptor (middleware để log, thêm token tự động, retry), request/response transformer, quản lý timeout chi tiết, upload file multipart, download file với progress tracking, và cancel token.
- Kết hợp với Retrofit để tạo type-safe API client từ annotation.
- Dùng
httpkhi app nhỏ, đơn giản. - Dùng Dio khi cần interceptor cho auth, logging, hoặc error handling nhất quán trên toàn app.
Dùng package firebase_auth: await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password).
Lắng nghe trạng thái auth bằng authStateChanges() stream—đây là cách đúng để biết user đã đăng nhập chưa.
Pitfall phổ biến:
- không lắng nghe
authStateChanges()mà kiểm tracurrentUserngay lập tức—có thể null khi app mới mở; - quên bật Email/Password provider trong Firebase Console;
- quên thêm entitlement trên iOS;
- không wrap trong try/catch dẫn đến crash khi mất mạng
Lưu ý: SHA-1 chỉ cần thiết cho Google Sign-In và OAuth provider, KHÔNG cần cho email/password auth.
Luôn test cả trường hợp offline.
Isolate là các luồng thực thi riêng biệt không chia sẻ bộ nhớ — mỗi isolate có heap và event loop riêng.
- Dùng isolate cho công việc tốn CPU (parse JSON lớn, mã hóa, xử lý ảnh) để tránh block main UI thread.
- API hiện đại (2025):
Isolate.run()(Dart 2.19+ / Flutter 3.7+) cho tác vụ one-shot ngắn gọn;compute()là Flutter helper wrapper.Isolate.spawn()vẫn dùng cho long-lived isolate cần giao tiếp liên tục qua SendPort/ReceivePort. - Isolate nhẹ hơn OS thread nhưng nặng hơn async/await, nên dùng tiết kiệm.
Platform channel cho phép giao tiếp giữa Dart và code native (Kotlin/Java cho Android, Swift/ObjC cho iOS).
- Dùng khi cần tính năng Flutter không hỗ trợ: truy cập phần cứng đặc thù, dùng thư viện native, hoặc kiểm soát OS chi tiết.
- Giao tiếp là bất đồng bộ thông qua message passing.
- Platform channel là lối thoát cho native functionality nhưng thêm độ phức tạp, nên ưu tiên dùng plugin có sẵn.
Dùng try-catch trong hàm async: try { await api.get(); } catch (e, st) { logger.error(e, st); }.
- Bọc
Futurechain bằng.catchError():future.catchError((e) => defaultValue). - Với stream, truyền
onErrorvào listen:stream.listen(onData, onError: (e) => handle(e)). - Lan truyền lỗi lên trên; không im lặng nuốt chúng.
- Trong production, dùng crash reporting (e.g.
- FirebaseCrashlytics) thay vì
print(e)— print chỉ phù hợp khi debug local. - Lỗi không được xử lý sẽ crash app.
Future.wait() là gì và khi nào dùng?Future.wait() chờ nhiều Future hoàn thành, sau đó trả về list kết quả: List results = await Future.wait([future1, future2, future3]).
- Nếu bất kỳ Future nào throw, toàn bộ
wait()sẽ throw. - Dùng cho các tác vụ song song: fetch nhiều API endpoint cùng lúc, load nhiều asset.
- Hiệu quả hơn việc gọi
awaittuần tự vì các tác vụ chạy đồng thời. - Với Dart 3, có thể dùng cú pháp record extension ngắn gọn hơn:
await (future1, future2).waitcho fixed-count parallel futures.
Dùng DevTools để profile CPU, memory và frame rendering.
- Kiểm tra jank (frame bị bỏ) qua tab Performance.
- Xác định frame chậm và dùng profiler xem function nào tốn thời gian.
- Với memory leak, chụp heap snapshot và so sánh trước/sau.
- Tối ưu bằng: dùng
const, lazy-load list, tránh widget tree quá sâu, chuyển công việc nặng sang isolate, và dùngRepaintBoundaryđúng chỗ.
ListView.builder và ListView cho danh sách dài là gì?ListView build tất cả con trước — cuộn qua danh sách 1000 item rất chậm và tốn bộ nhớ. ListView.builder lazily build item hiện trên màn hình: chỉ ~10 item tồn tại trong bộ nhớ mọi lúc.
- Luôn dùng
ListView.buildercho danh sách động.SliverListlà giải pháp nâng cao cho scrolling phức tạp (kết hợp list và grid). - Nhầm lẫn dùng
ListViewthayListView.builderlà bug hiệu năng phổ biến.
Dùng GitHub Actions, Fastlane hoặc Firebase App Distribution.
- Pipeline thông thường: lint code → chạy test → build APK/IPA → upload lên app store hoặc Firebase.
- Tự động hóa trên pull request để phát hiện vấn đề sớm.
- Các tool:
flutter analyzeđể lint,flutter testcho unit/widget test,flutter build apkcho release build,firebase-toolsđể phân phối. - CI/CD tốt ngăn các release bị lỗi.
Debug: chậm, không tối ưu, có debug symbol, bật hot reload, chạy JIT.
- Dùng khi phát triển.
- Profile: tối ưu như release nhưng giữ profiling tool; hot reload bị tắt.
- Dùng để test hiệu năng mà không có artifact của debug.
- Release: tối ưu hoàn toàn, minified, biên dịch native code với AOT, kích thước nhỏ nhất, hiệu năng tốt nhất.
- Dùng để phân phối.
- Chọn mode phù hợp cho từng tác vụ.
Unit test kiểm tra function/class độc lập: test('Math', () { expect(2+2, equals(4)); }).
- Widget test kiểm tra widget mà không render thực:
testWidgets('Button', (tester) { await tester.tap(find.byType(Button)); }). - Integration test kiểm tra toàn bộ app trên thiết bị/emulator thật.
- Unit test nhanh và đáng tin cậy.
- Widget test phát hiện bug UI.
- Integration test phát hiện vấn đề thực tế.
- Dùng cả ba theo mô hình kim tự tháp.
WidgetsFlutterBinding là gì và khi nào dùng nó?WidgetsFlutterBinding khởi tạo Flutter engine.
- Dùng
WidgetsFlutterBinding.ensureInitialized()trước khi chạy app để khởi tạo plugin/service trướcrunApp(). - Trường hợp dùng phổ biến: setup
GetIttrước khi app khởi động, khởi tạo code native, cấu hình logging. - Không có nó, một số khởi tạo bất đồng bộ sẽ thất bại vì binding chưa sẵn sàng.
- Luôn gọi trong
main()nếu có setup trước app.
base, interface, final) dùng để làm gì?base: chỉ cho phép extend, không cho implement—dùng khi muốn chia sẻ logic implementation nhưng kiểm soát interface. interface: chỉ cho phép implement, không cho extend—dùng khi muốn định nghĩa contract API. final: cấm cả extend lẫn implement—dùng để "đóng" class hoàn toàn. Các modifier này enforce ý định kiến trúc ngay ở compile-time, ngăn việc dùng sai inheritance.
Ví dụ: base class Animal {} — class khác chỉ có thể extend, không thể implement trực tiếp.
Sealed class giới hạn các subclass phải được định nghĩa trong cùng một library, tạo ra tập hợp đóng và biết trước. Khác với abstract class (ai cũng có thể extend từ bất kỳ đâu), sealed class đảm bảo exhaustiveness trong pattern matching—compiler báo lỗi nếu switch thiếu case.
Ví dụ: sealed class Shape {}; class Circle extends Shape {}; class Square extends Shape {};—switch trên Shape bắt buộc phải handle cả Circle lẫn Square. Rất phù hợp để mô hình hóa sum type như Result, State.
Provider giữ tất cả instance trong bộ nhớ đến khi bạn xóa thủ công.
- Riverpod (2.x trở lên) tự động dispose provider khi không còn listener nào — mức tiết kiệm bộ nhớ thực tế phụ thuộc vào số lượng và kích thước provider, không nên trích dẫn con số cụ thể mà không có benchmark.
- Về type safety: Riverpod dùng code generation (
@riverpod) tạo provider type-safe, phát hiện lỗi lúc compile thay vì runtime. - Provider hay gặp lỗi
ProviderNotFoundExceptionlúc chạy mà không có cảnh báo sớm. - Riverpod còn hỗ trợ
AsyncValueđể handle loading/error state một cách nhất quán.
BLoC phù hợp cho team lớn cần kiến trúc event-driven nghiêm ngặt và khả năng audit trail—mọi thay đổi state đều là kết quả của một event rõ ràng, dễ trace khi debug production.
- Ngành tài chính, y tế thường yêu cầu điều này.
- Riverpod đơn giản hơn, ít boilerplate hơn, lý tưởng cho hầu hết app consumer.
- Dùng BLoC khi: team lớn cần quy trình rõ ràng, cần log event để compliance, hoặc đã có codebase BLoC lớn.
- Riverpod cho phần còn lại.
Kết hợp AsyncNotifier với local storage (Hive, Isar, hoặc SharedPreferences). Pattern: khi build() chạy, đọc cache local trước rồi fetch API—nếu fetch thành công thì cập nhật cache. Dùng ref.invalidate() hoặc ref.refresh() để force refresh.
Ví dụ: @riverpod class PostsNotifier extends AsyncNotifier<List<Post>> { Future<List<Post>> build() async { final cached = await db.getPosts(); state = AsyncValue.data(cached); final fresh = await api.getPosts(); await db.savePosts(fresh); return fresh; } }. Kết quả: app show dữ liệu cũ ngay lập tức, cập nhật khi có mạng.
Shorebird là dịch vụ over-the-air (OTA) cho phép cập nhật code Dart của Flutter mà không cần qua App Store hay Play Store review.
- Deploy hotfix trong vài phút thay vì chờ 1-3 ngày review.
- Hỗ trợ A/B testing tính năng, rollback nhanh nếu có bug.
- Giới hạn: chỉ update được Dart code—không thể thay đổi native plugin, asset, hay Dart VM.
- Dùng Shorebird cho: bugfix khẩn cấp, copy change, logic update nhỏ.
- Vẫn dùng App Store cho: major feature, thay đổi UI lớn, cập nhật native code—để đảm bảo version control và rollback đầy đủ.