Khắc phục lỗi kotlin.UninitializedPropertyAccessException: Cạm bẫy 'Lateinit'

trung bình📱 Android2026-05-24| Android SDK, Kotlin (mọi phiên bản), Android Studio

Error Message

kotlin.UninitializedPropertyAccessException: lateinit property ... has not been initialized
#kotlin#lateinit#android#crash-fixing

Cuộc gọi đánh thức lúc 2 giờ sáng

Điện thoại của bạn báo tin nhắn. Một cảnh báo từ Crashlytics cho thấy tỷ lệ crash tăng 15% trong bản phát hành mới nhất. Thủ phạm chính là lỗi UninitializedPropertyAccessException tai tiếng. Bạn đã "hứa" với trình biên dịch Kotlin rằng sẽ khởi tạo một biến trước khi sử dụng nó, nhưng vòng đời (lifecycle) của Android lại có kế hoạch khác. Giờ đây, người dùng của bạn đang phải nhìn chằm chằm vào một ứng dụng đã bị đóng.

FATAL EXCEPTION: main
Process: com.example.app, PID: 12345
kotlin.UninitializedPropertyAccessException: lateinit property viewModel has not been initialized
    at com.example.app.ui.MainActivity.onCreate(MainActivity.kt:15)

Bản vá nhanh

Cách nhanh nhất để ngăn chặn crash là kiểm tra trạng thái khởi tạo trước khi truy cập thuộc tính. Sử dụng cú pháp ::prop.isInitialized. Nó đóng vai trò như một cánh cổng an toàn cho mã nguồn của bạn.

if (::viewModel.isInitialized) {
    viewModel.doSomething()
}

Đừng lạm dụng điều này ở mọi nơi. Nếu bạn thấy mình phải kiểm tra isInitialized ở mười chỗ khác nhau, kiến trúc của bạn có khả năng đang gặp lỗi về cấu trúc.

Tại sao Android gây ra lỗi này

Framework của Android nổi tiếng với việc tách biệt quá trình tạo đối tượng và thiết lập (setup). Chúng ta không gọi hàm khởi tạo (constructor) cho Activity hay Fragment; hệ điều hành sẽ làm việc đó. Điều này buộc chúng ta phải đợi các hàm gọi lại (callback) cụ thể trong vòng đời để thiết lập các phụ thuộc (dependencies). Dưới đây là những trường hợp thường gây ra lỗi:

- **Delayed Injection:** Bạn đang sử dụng Dagger hoặc Hilt, nhưng vô tình truy cập vào một trường `@Inject` trước khi gọi `super.onCreate()`.
- **Bẫy Fragment View:** Bạn truy cập vào đối tượng View Binding trong Fragment sau khi `onDestroyView()` đã được gọi. Đây là nguồn cơn phổ biến gây rò rỉ bộ nhớ (memory leak) và crash.
- **Race Conditions:** Một yêu cầu mạng trả về kết quả trong 200ms, cố gắng cập nhật một thành phần UI chưa hoàn tất trình tự `onCreate`.

Các chiến lược triển khai tốt hơn

1. Luồng vòng đời chuẩn

Đối với Activity, hãy chuyển tất cả các phép gán vào onCreate(). Đối với Fragment, onViewCreated() là "bến đỗ" an toàn nhất cho logic UI. Nếu bạn sử dụng các luồng chạy ngầm (background thread), hãy đảm bảo chúng không thể kích hoạt callback cho đến khi UI sẵn sàng tiếp nhận.

// Rủi ro: Callback này có thể hoàn tất trước khi adapter tồn tại
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    api.fetchData { data ->
        adapter.submitList(data) // Crash nếu api trả về kết quả nhanh hơn dòng tiếp theo
    }
    adapter = MainAdapter()
}

2. Mô hình Nullable (An toàn hơn)

Nếu một thuộc tính không được đảm bảo tồn tại trong suốt vòng đời của lớp, đừng sử dụng lateinit. Hãy sử dụng kiểu nullable. Nó buộc bạn phải xử lý trạng thái "null", điều này tốt hơn nhiều so với một ngoại lệ gây chết ứng dụng.

private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null // Giải phóng để tránh crash và rò rỉ bộ nhớ
}

3. Tận dụng 'by lazy'

Sử dụng by lazy cho các thuộc tính chỉ đọc không cần đầu vào bên ngoài khi khởi tạo. Thuộc tính sẽ không được tạo cho đến giây phút bạn thực sự gọi đến nó. Cách này vừa an toàn với đa luồng (thread-safe) vừa hiệu quả.

private val analytics by lazy {
    AnalyticsProvider.get(this)
}

Cách xác minh bản vá

Đừng bao giờ mặc định lỗi đã hết chỉ vì ứng dụng mở được. Hãy làm theo các bước sau:

- **Stress Test:** Bật tùy chọn "Don't keep activities" (Không giữ các activity) trong Developer Options và xoay màn hình liên tục. Điều này buộc vòng đời ứng dụng phải hủy bỏ và xây dựng lại.
- **Kiểm tra Logcat:** Tìm dòng `Caused by` trong stack trace. Đảm bảo không có lỗi `lateinit` mới xuất hiện khi điều hướng nhanh giữa các màn hình.
- **Unit Tests:** Sử dụng `mockk` để xác minh rằng các phương thức thiết lập thực sự được gọi. Nếu bạn dùng Robolectric, hãy kiểm tra xem thuộc tính đã sẵn sàng sau khi gọi `activityController.create()`.

Kết luận cuối cùng

lateinit là một công cụ mang tính tiện lợi, không phải là cách để phớt lờ an toàn null (null safety). Nếu thuộc tính của bạn thực sự là tùy chọn hoặc có tính động cao, hãy sử dụng kiểu nullable. Hãy dành lateinit cho các phụ thuộc chắc chắn sẽ tồn tại, như các ViewModel được inject hoặc các công cụ chuyên dụng được thiết lập trong onCreate.

Related Error Notes