Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Danh mục

Flutter iconFlutter

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.

dart
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 tra if (name != null), trong khối đó Dart coi nameString non-nullable mà không cần !.
  • Điều này xảy ra tự động với kiểm tra null, is checks, 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. StatefulWidget duy trì state có thể thay đổi qua setState(), kích hoạt rebuild.
  • Dùng StatelessWidget cho UI tĩnh (nhãn văn bản, icon), StatefulWidget cho component tương tác (form, toggle).
  • Luôn ưu tiên StatelessWidget vì 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, NavigatorScaffoldState.

  • Mọi widget đều có BuildContext được truyền vào hàm build().
  • 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 ValueKey cho giá trị đơn giản, ObjectKey cho đố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 StatefulWidget sẽ 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 const bỏ 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 const giống nhau thành một instance, giảm bộ nhớ.
  • Luôn dùng const cho widget với tham số cố định: const Text("Hello").
  • Tránh tạo UniqueKey trong const vì sẽ mất lợi ích.

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. Expanded buộc con lấp đầy không gian còn lại đều nhau. Flexible cho phép con chiếm thêm không gian nhưng có thể nhỏ hơn nếu cần.
  • Dùng MainAxisAlignmentCrossAxisAlignment để 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ùng AnimatedBuilder hoặ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()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 RepaintBoundary thay đổ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 Consumer hoặc context.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 Event vào BLoC, BLoC xử lý và emit ra State mớ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 ChangeNotifier và rebuild khi notifyListeners() đượ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), ChangeNotifier thườ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 BuildContext trong 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()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 qua GoRouterState.
  • 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 ShellRoute build 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 GetIt cho 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ằng var (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}'); }.

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 http khi 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:

  1. không lắng nghe authStateChanges() mà kiểm tra currentUser ngay lập tức—có thể null khi app mới mở;
  2. quên bật Email/Password provider trong Firebase Console;
  3. quên thêm entitlement trên iOS;
  4. 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.