0
0
Lập trình
NM

Hướng dẫn sử dụng Jetpack Compose Navigation để quay lại với kết quả

Đăng vào 3 tuần trước

• 5 phút đọc

Hướng dẫn sử dụng Jetpack Compose Navigation để quay lại với kết quả

Khi bạn cần mở một màn hình, cho phép người dùng chọn một giá trị, và sau đó truyền lại giá trị đó về màn hình trước, có thể bạn đã từng sử dụng các phương pháp như ViewModel chia sẻ, singleton, hoặc các tham số đường dẫn không ổn định. Tuy nhiên, với Jetpack Compose Navigation, có một cách đơn giản hơn: điều hướng tới màn hình mới, chờ nhận kết quả và quay lại với kết quả đó.

Bài viết này sẽ giới thiệu một tiện ích nhỏ mà bạn có thể thêm vào dự án của mình, giải thích cách thức hoạt động của nó, kèm theo ví dụ, các cạm bẫy và mẹo kiểm thử.

Tiện ích

Đặt đoạn mã này vào một tệp trong dự án của bạn:

kotlin Copy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.json.Json

private const val RESULT_KEY = "result"

fun <T> NavController.navigateBackWithResult(value: T) {
    previousBackStackEntry?.savedStateHandle?.set(RESULT_KEY, value)
    navigateUp()
}

inline fun <reified T> NavController.navigateBackWithSerializableResult(value: T) {
    navigateBackWithResult(Json.encodeToString(value))
}

suspend fun <T> NavController.navigateForResult(route: Any): T? =
    suspendCancellableCoroutine { continuation ->
        val currentNavEntry = currentBackStackEntry
            ?: throw IllegalStateException("No current back stack entry found")

        navigate(route)

        val lifecycleObserver = object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                if (event == Lifecycle.Event.ON_START) {
                    continuation.resume(currentNavEntry.savedStateHandle[RESULT_KEY])
                    currentNavEntry.savedStateHandle.remove<T>(RESULT_KEY)
                    currentNavEntry.lifecycle.removeObserver(this)
                }
            }
        }

        currentNavEntry.lifecycle.addObserver(lifecycleObserver)

        continuation.invokeOnCancellation {
            currentNavEntry.savedStateHandle.remove<T>(RESULT_KEY)
            currentNavEntry.lifecycle.removeObserver(lifecycleObserver)
        }
    }

suspend inline fun <reified T> NavController.navigateForSerializableResult(route: Any): T? {
    val result: String = navigateForResult(route) ?: return null
    return Json.decodeFromString(result)
}

Cách hoạt động

  1. Người gọi khởi động một đường dẫn bằng navigateForResult(route). Màn hình hiện tại sẽ được ghi nhớ, điều hướng xảy ra và một observer vòng đời được thiết lập.
  2. Người gọi hoàn thành bằng cách gọi navigateBackWithResult(value). Kết quả được lưu trữ trong SavedStateHandle của người gọi dưới một khóa cố định.
  3. Người gọi tiếp tục trên ON_START. Coroutine sẽ mở khóa, lấy kết quả và xóa khóa để tránh các giá trị cũ.

Phương pháp này nhận thức vòng đời, an toàn về kiểu dữ liệu và tối thiểu - không cần ActivityResultContract, không cần ViewModel chia sẻ, không cần bus sự kiện.

Cách sử dụng

Dưới đây là một ví dụ đơn giản để thấy được cách hoạt động. Giả sử có một màn hình nơi người dùng chọn màu sắc, và khi họ quay lại, giá trị được chọn sẽ được truyền về màn hình trước. Đây là trường hợp đơn giản nhất: một kiểu dữ liệu cơ bản (String) được gửi lại.

kotlin Copy
// Các đường dẫn:
object Routes {
    const val HOME = "home"
    const val COLOR_PICKER = "color-picker"
}

// Nav host:
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(navController, startDestination = Routes.HOME) {
        composable(Routes.HOME) { HomeScreen(navController) }
        composable(Routes.COLOR_PICKER) { ColorPickerScreen(navController) }
    }
}

// Người gọi (HomeScreen):
@Composable
fun HomeScreen(navController: NavController) {
    val scope = rememberCoroutineScope()
    var selectedColor by remember { mutableStateOf<String?>(null) }

    Column(Modifier.padding(16.dp)) {
        Text("Đã chọn: ${'$'}{selectedColor ?: "không có"}")
        Button(onClick = {
            scope.launch {
                val result: String? = navController.navigateForResult(Routes.COLOR_PICKER)
                selectedColor = result
            }
        }) {
            Text("Chọn màu")
        }
    }
}

// Người gọi (ColorPickerScreen):
@Composable
fun ColorPickerScreen(navController: NavController) {
    val colors = listOf("Đỏ", "Xanh lá", "Xanh dương")
    Column(Modifier.padding(16.dp)) {
        colors.forEach { color ->
            Button(onClick = { navController.navigateBackWithResult(color) }) {
                Text(color)
            }
        }
        OutlinedButton(onClick = { navController.navigateUp() }) {
            Text("Hủy")
        }
    }
}

Các trường hợp và lưu ý

Có một số trường hợp và chi tiết cần lưu ý. Kết quả luôn có thể là null (T?), vì vậy bạn nên chuẩn bị xử lý trường hợp người dùng hủy bỏ hoặc quay lại mà không đặt giá trị nào. Nếu bạn sử dụng biến thể không tuần tự và người gọi cùng người nhận không thống nhất về kiểu dữ liệu, bạn sẽ gặp ClassCastException, vì vậy việc đảm bảo kiểu dữ liệu là rất quan trọng. Kết quả cũng sẽ không tồn tại sau khi quá trình chết, vì cơ chế này thực chất là một callback chứ không phải là trạng thái bền vững. Cuối cùng, tránh chờ đợi nhiều kết quả đồng thời từ cùng một điểm đến; nếu bạn cần điều đó, hãy bọc truy cập trong một guard hoặc hàng đợi của riêng bạn để ngăn chặn xung đột.

Kết luận

Tiện ích này cung cấp cho bạn một mô hình nhẹ, nhận thức vòng đời để truyền dữ liệu trở lại qua ngăn xếp điều hướng Compose: bạn chỉ cần thêm nó vào dự án của mình, gọi navigateForResult() khi điều hướng tới màn hình mới, và sau đó gọi navigateBackWithResult() khi quay lại. Đó là tất cả những gì bạn cần - không cần hợp đồng bổ sung và không cần các phương pháp không ổn định.

Chúc bạn lập trình vui vẻ!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào