Self-sufficient Features (P1)
Trong nhiều app, feature thường depend vào parent view để hoạt động: parent load data, parent handle error, parent control navigation.
Điều này tạo ra tight coupling — khó move feature, khó mở rộng, khó hỗ trợ deep linking.
Chương này giới thiệu self-sufficient features — feature có thể tự load data, tự handle error, và hoạt động independently khỏi parent view.
1. Vấn đề của dependent features
Phần tiêu đề “1. Vấn đề của dependent features”Xem xét pattern master-detail quen thuộc:
- List view load danh sách courses
- User tap vào course → mở
CourseView CourseViewnhận full Course object từ list
Vấn đề:
- Muốn mở
CourseViewtừ deep link? Không được — không có parent cung cấp data. - Muốn dùng
CourseViewở flow khác? Phải lo chuyện load data ở nơi mới. CourseViewnhư đứa trẻ cần parent giúp mọi thứ.
2. Handle error tại chỗ
Phần tiêu đề “2. Handle error tại chỗ”Một dạng dependency khác: khi lỗi xảy ra, feature đẩy error lên parent:
Connection drop? → Throw lên parent.Load failed? → Throw lên parent.User input sai? → Throw lên parent.Và parent làm gì? Show một blocking alert — chặn toàn bộ UI.
Cách làm tốt hơn
Phần tiêu đề “Cách làm tốt hơn”Thay vì blocking alert, hãy:
- Inline error: Hiển thị error message ngay trong View thay vì popup.
- Auto-retry queue: Mất mạng giữa chừng upload → lưu lại và retry khi có mạng.
- Retry button: Giống messaging app đánh dấu failed message bằng ❗ để user tap retry.
- Local notification: Nếu app đang background, gửi notification thay vì để error mất.
Nhiều error cùng lúc
Phần tiêu đề “Nhiều error cùng lúc”Khi nhiều sub-features cùng gặp error và đều show alert, user sẽ thấy hàng loạt popup liên tiếp — UX rất kém.
Giải pháp: Dùng inline errors. Mỗi feature show error của riêng nó trong View, không tranh giành alert.
3. Bốn nguyên tắc Self-sufficient Features
Phần tiêu đề “3. Bốn nguyên tắc Self-sufficient Features”-
Self-loading: Chỉ cần nhận ID, feature tự biết cách fetch full data. Không depend vào parent.
-
Self-handling errors: Error được handle và display inline ngay trong feature. Không propagate lên parent.
-
Independent operation: Không cần communication qua lại phức tạp với parent. Nhận ID vào, tự lo phần còn lại.
-
No navigation interference: Feature không push/present view lên parent’s navigation stack. Expose callback, để parent quyết định.
4. Kết nối data loading
Phần tiêu đề “4. Kết nối data loading”Thay vì nhận full Course object, CourseView chỉ nhận ID và tự load:
struct CourseView: View { let courseId: String @StateObject private var loader = CourseLoader()
var body: some View { switch loader.state { case .loading: ProgressView() case .loaded(let course): CourseContentView(course: course) case .error(let message): // Inline error — không throw lên parent ErrorView(message: message, onRetry: { loader.load(courseId: courseId) }) } }}Lợi ích:
- Deep linking hoạt động ngay: Chỉ cần truyền ID, feature tự lo.
- Move tự do: Đặt
CourseViewở bất kỳ flow nào — nó đều hoạt động. - Giảm coupling: Parent không cần biết cách load data cho child.
5. Loader pattern
Phần tiêu đề “5. Loader pattern”Mỗi feature có Loader riêng, quản lý state riêng:
class CourseLoader: ObservableObject { @Published var state: LoadState = .loading
func load(courseId: String) async { state = .loading do { let course = try await courseAPI.loadCourse(id: courseId) state = .loaded(course) } catch { state = .error("Không thể tải khóa học. Hãy thử lại.") } }}Kết luận: Self-sufficient features giống người trưởng thành — tự lo liệu. Chỉ cần ID, tự load data, tự handle error, không interfere navigation. Unlock deep linking, cho phép move feature tự do.