0

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

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí