Tại sao lại không nên dùng LocalContext để lấy String trong Jetpack Compose ?
Nếu bạn vừa cập nhật Jetpack Compose BOM lên các phiên bản mới (đặc biệt từ các bản release 2026 trở đi), rất có thể bạn đã bắt đầu thấy Android Studio hiển thị một cảnh báo Lint mới:
LocalContextGetResourceValueCall
Thoạt nhìn, nhiều người sẽ nghĩ:
“Chỉ là warning thôi mà, code vẫn chạy bình thường.”
Nhưng thực tế, đây là một trong những cảnh báo quan trọng nhất liên quan đến cách Compose quản lý trạng thái giao diện (UI state) và phản ứng với thay đổi cấu hình hệ thống (configuration changes).
Điều đáng nói là:lỗi này không xuất hiện vì Google muốn “ép style code” mà vì LocalContext.current.getString() có thể dẫn tới những bug UI cực kỳ khó phát hiện trong thực tế
Đặc biệt trên:
- foldable devices
- multi-window
- split-screen
- dynamic locale switching
- và các hệ thống không recreate Activity như trước đây
Để hiểu tại sao, chúng ta cần nhìn sâu hơn vào cách Compose hoạt động phía bên trong.
Ba cách phổ biến để lấy String trong Compose
Trong Compose, hiện nay có ba cách mà lập trình viên thường dùng để lấy dữ liệu từ strings.xml.
1. stringResource()
Đây là API “chuẩn Compose”:
Text(
text = stringResource(R.string.app_name)
)
Ưu điểm:
- ngắn gọn
- đúng semantic
- reactive với configuration changes
- Compose-aware
Nhưng nó có một giới hạn lớn:
@Composable
fun stringResource(...)
Nghĩa là:
- chỉ gọi được bên trong composable scope
- không dùng được trong:
- function thường
- remember {}
- callback
- ViewModel
- business logic
Ví dụ sau sẽ lỗi:
val title = remember {
stringResource(R.string.title) // ❌
}
2. LocalContext.current.getString()
Đây là cách rất nhiều developer “tiện tay” sử dụng:
val context = LocalContext.current
val text = context.getString(R.string.title)
Thoạt nhìn nó có vẻ hợp lý:
đã có Context rồi =>gọi luôn getString()=>đỡ phải nghĩ nhiều
Đặc biệt: trong Android truyền thống (View system) cách này hoàn toàn bình thường
Nhưng trong Compose, đây lại là nơi vấn đề bắt đầu.
3. LocalResources.current.getString()
Đây là hướng mà Compose team thực sự muốn bạn dùng khi cần truy cập tài nguyên ngoài stringResource().
Ví dụ:
val resources = LocalResources.current
val title = resources.getString(R.string.title)
Thoạt nhìn: nó chỉ khác Context.getString() đúng… một dòng. Nhưng bên dưới runtime của Compose: hai cách này hoàn toàn khác nhau về mặt reactive state tracking.
Vấn đề thật sự nằm ở đâu?
Để hiểu bug, chúng ta cần hiểu một khái niệm cực kỳ quan trọng trong Compose:
Compose chỉ recompose khi nó biết một dữ liệu nào đó đã thay đổi.
Nói cách khác:
Compose không “đoán” nó phải biết dependency nào đang được observe LocalContext không phải reactive source
Khi bạn viết:
val context = LocalContext.current
val text = context.getString(R.string.title)
Compose chỉ thấy: bạn đang đọc một Context
Nhưng:
- Context không phải State
- Context không được coi là configuration-aware reactive dependency
Điều đó dẫn đến một hệ quả cực kỳ quan trọng:
Khi language / locale / fontScale thay đổi, Compose có thể không recompose composable này.
“Nhưng Activity sẽ recreate mà?”
Đúng… nh trong quá khứ.
Ở Android truyền thống:
- đổi locale
- đổi dark mode
- đổi orientation
→ Activity thường bị destroy + recreate.
Vì vậy:
- mọi thứ tự refresh
- bug bị che giấu
Nhưng Android hiện đại đã khác.
Kỷ nguyên mới của Android: configuration không còn đơn giản nữa
Ngày nay Android phải hỗ trợ:
- foldables
- freeform window
- desktop mode
- split screen
- live resize
- embedded activities
Trong nhiều tình huống Activity KHÔNG bị recreate nhưng configuration vẫn thay đổi
Ví dụ:
- user đổi ngôn ngữ hệ thống
- app đang split-screen
- Activity vẫn sống
Lúc này: Context object có thể vẫn giữ nguyên identity nhưng resource phía sau đã đổi
Compose nhìn vào:
LocalContext.current
và nghĩ:
“À, object này chưa đổi mà.”
=> composable không recompose.
Hậu quả thực tế: UI “nửa cũ nửa mới”
Đây là loại bug rất khó debug.
Ví dụ: Toolbar đã đổi sang tiếng Việt nhưng vài composable vẫn hiện tiếng Anh. Hoặc:một số text update một số text không
Người dùng sẽ thấy: UI không đồng nhất và trải nghiệm rất “rẻ tiền”. Và nguy hiểm nhất: Bug này thường chỉ xuất hiện trên một số device hoặc mode đặc biệt.
LocalResources khác gì?
Khi bạn dùng:
val resources = LocalResources.current
Compose runtime hiểu rằng: composable này phụ thuộc vào Resources configuration.
Điều này cực kỳ quan trọng. LocalResources được implement như một reactive CompositionLocal.
Nghĩa là:
- khi configuration đổi
- provider sẽ invalidate
- tất cả composable đang observe nó sẽ recompose
Đây mới chính là behavior Compose mong muốn.
Tại sao stringResource() luôn an toàn?
Bởi vì bên trong: stringResource() đã tự phụ thuộc vào LocalResources. Nghĩa là nó đã được Compose team xử lý reactive behavior sẵn rồi.
Đó là lý do nếu dùng được stringResource(), hãy luôn ưu tiên nó.
Trường hợp đặc biệt: remember
Đây là nơi rất nhiều người dính bug.
Ví dụ:
val text = remember {
context.getString(R.string.title)
}
Thoạt nhìn có vẻ tối ưu. Nhưng thực tế:
- string bị cache vĩnh viễn
- locale đổi → text không đổi
Đây là lý do Compose team đặc biệt ghét việc dùng Context.getString() trong các block nhớ trạng thái.
“Nhưng trong onClick thì có sao không?”
Đây là điểm thú vị.
Button(
onClick = {
Toast.makeText(
context,
context.getString(R.string.saved),
Toast.LENGTH_SHORT
).show()
}
)
Trên thực tế thì đa số trường hợp vẫn OK vì lambda chỉ chạy khi click lúc đó resources mới nhất thường đã được cập nhật. Nhưng: Lint vẫn warning. Tại sao?. Vì static analyzer không thể biết:
- lambda này chạy lúc nào
- capture state ra sao
- có bị remember không
Nên nó chọn hướng an toàn tuyệt đối.
Đây không chỉ là chuyện “style code”
Nhiều người nghĩ: “Google làm khó dev.” Không. Những warning như thế này thường xuất phát từ:
- bug production thực tế
- issue từ hàng triệu thiết bị
- edge case trên OEM
Compose team đang cố biến UI thành fully reactive syste giống React/SwiftUI
Và Context.getString() đi ngược triết lý đó.
Quy tắc thực chiến
✅ Dùng stringResource() khi có thể
✅ Dùng LocalResources.current. Khi:
- cần lấy string ngoài composable helper
- dùng trong remember
- dùng trong callback
val resources = LocalResources.current
val text = remember(resources) {
resources.getString(R.string.title)
}
❌ Hạn chế dùng LocalContext.getString()
Chỉ nên dùng khi:
- bạn thực sự đang làm Android system work
- không phải UI state Một góc nhìn quan trọng hơn
Compose không chỉ là:
“Android View viết kiểu mới”
Nó là:
một runtime reactive thực thụ.
Trong reactive UI:
dependency tracking là tất cả nếu framework không biết bạn phụ thuộc vào cái gì UI sẽ stale
LocalResources giúp Compose hiểu:
“UI này phụ thuộc vào configuration.”
LocalContext thì không.
Và đó là toàn bộ lý do của warning này.
Kết luận
LocalContext.current.getString() không sai về mặt kỹ thuật.
Nhưng trong Compose:nó không reactive đúng cách có thể gây stale UI đặc biệt trong configuration changes hiện đại
Ngược lại: stringResource() và LocalResources.current được thiết kế để hoạt động hài hòa với recomposition system của Compose.
Bằng cách tuân thủ những nguyên tắc này, bạn không chỉ làm cho code của mình trở nên chuyên nghiệp hơn mà còn xây dựng được những ứng dụng Android mạnh mẽ, hoạt động mượt mà trên mọi cấu hình thiết bị.
Cảm ơn vì đã đọc hết bài!!!!
All rights reserved