Bỏ qua để đến nội dung

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.

Xem xét pattern master-detail quen thuộc:

  • List view load danh sách courses
  • User tap vào course → mở CourseView
  • CourseView nhận full Course object từ list

Vấn đề:

  • Muốn mở CourseView từ 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.
  • CourseView như đứa trẻ cần parent giúp mọi thứ.

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.

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.

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.

  1. Self-loading: Chỉ cần nhận ID, feature tự biết cách fetch full data. Không depend vào parent.

  2. Self-handling errors: Error được handle và display inline ngay trong feature. Không propagate lên parent.

  3. 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.

  4. No navigation interference: Feature không push/present view lên parent’s navigation stack. Expose callback, để parent quyết định.

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.

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.