1. Giới thiệu
Trong lớp học lập trình này, bạn sẽ tìm hiểu những khái niệm nâng cao liên quan đến các API Trạng thái và API Hiệu ứng phụ trong Jetpack Compose. Bạn sẽ tìm hiểu cách tạo phần tử giữ trạng thái cho thành phần kết hợp có tính trạng thái với logic đáng kể, cách tạo coroutine và gọi các hàm có thể tạm ngưng (suspend function) bằng đoạn mã trong Compose cũng như cách kích hoạt hiệu ứng phụ để thực hiện các trường hợp sử dụng.
Để được hỗ trợ thêm khi tham gia lớp học lập trình này, hãy xem nội dung tập lập trình dưới đây:
Kiến thức bạn sẽ học được
- Cách quan sát các luồng dữ liệu của đoạn mã Compose để cập nhật giao diện người dùng.
- Cách tạo trình giữ trạng thái cho thành phần kết hợp có tính trạng thái.
- Các API hiệu ứng phụ như
LaunchedEffect
,rememberUpdatedState
,DisposableEffect
,produceState
vàderivedStateOf
. - Cách tạo coroutine và gọi các hàm có thể tạm ngưng trong thành phần kết hợp bằng API
rememberCoroutineScope
.
Bạn cần có
- Phiên bản Android Studio mới nhất
- Kinh nghiệm về cú pháp Kotlin, bao gồm cả lambda.
- Kinh nghiệm cơ bản về Compose Hãy cân nhắc việc tham gia lớp học lập trình cơ bản về Jetpack Compose trước khi tham gia lớp học lập trình này.
- Các khái niệm cơ bản về trạng thái trong Compose như Luồng dữ liệu đơn hướng (UDF), ViewModels, chuyển trạng thái lên trên (state hoisting), thành phần kết hợp phi trạng thái/có tính trạng thái, API ô trống cũng như các API trạng thái
remember
vàmutableStateOf
. Để nắm được kiến thức này, hãy cân nhắc đọc tài liệu về Trạng thái và Jetpack Compose hoặc hoàn thành lớp học lập trình Sử dụng Trạng thái trong Jetpack Compose. - Kiến thức cơ bản về coroutine trong Kotlin.
- Hiểu biết cơ bản về vòng đời của thành phần kết hợp.
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, bạn sẽ bắt đầu với một ứng dụng chưa hoàn thiện, đó là ứng dụng Crane – Nghiên cứu của Material và thêm các tính năng để cải thiện ứng dụng đó.
2. Thiết lập
Lấy mã
Bạn có thể tìm thấy đoạn mã dành cho lớp học lập trình này trong android-compose-codelabs trên kho lưu trữ GitHub. Để sao chép, hãy chạy:
$ git clone https://github.com/android/codelab-android-compose
Ngoài ra, bạn có thể tải kho lưu trữ ở dạng định dạng tệp zip:
Xem xét ứng dụng mẫu
Mã bạn vừa tải xuống có chứa mã dành cho tất cả lớp học lập trình Compose hiện có. Để hoàn tất lớp học lập trình này, hãy mở dự án AdvancedStateAndSideEffectsCodelab
trong Android Studio.
Bạn bên bắt đầu với các đoạn mã trong nhánh main và làm theo hướng dẫn từng bước của lớp học lập trình theo tốc độ của bản thân.
Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã bạn cần thêm vào dự án. Có lúc, bạn cũng sẽ cần phải xoá những dòng mã được đề cập rõ ràng trong các nhận xét trong đoạn mã.
Làm quen với các đoạn mã và chạy ứng dụng mẫu
Hãy dành chút thời gian tìm hiểu cấu trúc dự án và chạy ứng dụng.
Khi chạy ứng dụng từ nhánh main (chính), bạn sẽ thấy rằng một số chức năng như ngăn hoặc tải điểm đến của chuyến bay không hoạt động! Đó là những gì bạn sẽ thực hiện trong các bước tiếp theo của lớp học lập trình này.
Kiểm thử giao diện người dùng
Ứng dụng này sử dụng các bài kiểm thử giao diện người dùng rất cơ bản có trong thư mục androidTest
. Cả 2 nhánh main
và end
sẽ luôn vượt qua các bài kiểm tra này.
[Không bắt buộc] Hiển thị bản đồ trên màn hình chi tiết
Bạn không cần phải làm theo các bước để hiển thị bản đồ thành phố trên màn hình chi tiết. Tuy nhiên, nếu muốn làm như vậy, bạn cần phải có khoá API cá nhân như được nêu trong tài liệu Maps (Bản đồ). Thêm khoá đó vào tệp local.properties
như sau:
// local.properties file
google.maps.key={insert_your_api_key_here}
Giải pháp cho lớp học lập trình
Để nhận nhánh end
bằng git, hãy dùng lệnh sau:
$ git clone -b end https://github.com/android/codelab-android-compose
Hoặc bạn có thể tải đoạn mã chứa giải pháp từ đây:
Câu hỏi thường gặp
3. Quy trình tạo trạng thái giao diện người dùng
Như bạn thấy khi chạy ứng dụng từ nhánh main
, danh sách các điểm đến của chuyến bay bị trống!
Để khắc phục vấn đề này, bạn phải hoàn tất 2 bước sau đây:
- Thêm logic trong
ViewModel
để tạo trạng thái giao diện người dùng. Trong trường hợp của bạn, đây là danh sách điểm đến đề xuất. - Sử dụng trạng thái giao diện người dùng từ giao diện người dùng. Trạng thái này sẽ hiển thị giao diện người dùng trên màn hình.
Trong phần này, bạn sẽ hoàn tất bước đầu tiên.
Một cấu trúc tối ưu của ứng dụng được sắp xếp theo lớp để tuân theo các phương pháp cơ bản về thiết kế một hệ thống tối ưu, chẳng hạn như phân tách các mối quan ngại và khả năng kiểm thử.
Phần Tạo trạng thái giao diện người dùng đề cập đến quá trình ứng dụng truy cập vào lớp dữ liệu, áp dụng quy tắc kinh doanh nếu cần và hiển thị trạng thái giao diện người dùng được sử dụng từ giao diện người dùng.
Lớp dữ liệu trong ứng dụng này đã được triển khai. Bây giờ, bạn sẽ tạo trạng thái (danh sách điểm đến được đề xuất) để giao diện người dùng có thể sử dụng trạng thái đó.
Bạn có thể dùng một số API để tạo trạng thái giao diện người dùng. Các phương án thay thế được tóm tắt trong tài liệu về Các loại đầu ra trong quy trình tạo trạng thái. Nhìn chung, bạn nên sử dụng StateFlow
của Kotlin để tạo trạng thái giao diện người dùng.
Để tạo trạng thái giao diện người dùng, hãy làm theo các bước sau đây:
- Mở
home/MainViewModel.kt
. - Xác định biến
_suggestedDestinations
riêng tư thuộc loạiMutableStateFlow
để biểu thị danh sách điểm đến được đề xuất và đặt một danh sách trống làm giá trị bắt đầu.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
- Xác định biến không thể thay đổi thứ hai
suggestedDestinations
thuộc loạiStateFlow
. Đây là biến chỉ đọc công khai có thể được sử dụng từ giao diện người dùng. Việc hiển thị biến chỉ đọc trong khi sử dụng biến có thể thay đổi trong nội bộ là một phương pháp hay. Bằng cách này, bạn đảm bảo rằng chỉ có thể sửa đổi trạng thái giao diện người dùng thông quaViewModel
, giúp lớp này trở thành nguồn đáng tin cậy duy nhất. Hàm mở rộngasStateFlow
chuyển đổi luồng từ có thể thay đổi thành không thể thay đổi.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
- Trong khối init của
ViewModel
, hãy thêm một lệnh gọi từdestinationsRepository
để nhận các điểm đến từ lớp dữ liệu.
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>> = _suggestedDestinations.asStateFlow()
init {
_suggestedDestinations.value = destinationsRepository.destinations
}
- Cuối cùng, hãy huỷ nhận xét về việc sử dụng biến nội bộ
_suggestedDestinations
mà bạn thấy trong lớp này để có thể cập nhật biến này đúng cách với các sự kiện đến từ giao diện người dùng.
Vậy là xong! Bạn đã hoàn thành bước đầu tiên! ViewModel
hiện có thể tạo trạng thái giao diện người dùng. Trong bước tiếp theo, bạn sẽ sử dụng trạng thái này từ giao diện người dùng.
4. Sử dụng luồng dữ liệu từ ViewModel một cách an toàn
Danh sách điểm đến của chuyến bay vẫn trống. Ở bước trước, bạn đã tạo trạng thái giao diện người dùng trong MainViewModel
. Giờ đây, bạn sẽ sử dụng trạng thái giao diện người dùng có trong MainViewModel
để hiển thị trong giao diện người dùng.
Mở lại tệp home/CraneHome.kt
và xem thành phần kết hợp CraneHomeContent
.
Có một ghi chú trong mục TODO (Việc cần làm) phía trên định nghĩa về suggestedDestinations
được gán cho danh sách trống được ghi nhớ. Đây là nội dung sẽ xuất hiện trên màn hình: một danh sách trống! Trong bước này, bạn sẽ khắc phục vấn đề đó và trình bày các đích đến đề xuất mà MainViewModel
hiển thị.
Mở home/MainViewModel.kt
để xem StateFlow suggestedDestinations
, vốn được khởi tạo cho destinationsRepository.destinations
và sẽ cập nhật khi các hàm updatePeople
hoặc toDestinationChanged
được gọi.
Bạn muốn giao diện người dùng trong thành phần kết hợp CraneHomeContent
có thể cập nhật bất cứ khi nào có một mục mới được xuất vào luồng dữ liệu suggestedDestinations
. Bạn có thể sử dụng hàm collectAsStateWithLifecycle()
. collectAsStateWithLifecycle()
thu thập các giá trị từ StateFlow
và thể hiện giá trị mới nhất thông qua API State (Trạng thái) của Compose theo cách nhận biết vòng đời. Điều này sẽ khiến mã Compose đọc giá trị trạng thái đó kết hợp lại khi có một mục mới được xuất vào luồng dữ liệu.
Để bắt đầu sử dụng API collectAsStateWithLifecycle
, trước tiên hãy thêm phần phụ thuộc sau đây vào app/build.gradle
. Biến lifecycle_version
đã được xác định trong dự án bằng phiên bản phù hợp.
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}
Quay lại thành phần kết hợp CraneHomeContent
và thay thế dòng chỉ định suggestedDestinations
bằng lệnh gọi tới collectAsStateWithLifecycle
trên thuộc tính suggestedDestinations
của ViewModel
:
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle()
// ...
}
Nếu chạy ứng dụng, bạn sẽ thấy danh sách các điểm đến được điền sẵn và danh sách này sẽ thay đổi bất cứ khi nào bạn nhấn vào số hành khách.
5. LaunchedEffect và rememberUpdatedState
Trong dự án, hiện có một tệp home/LandingScreen.kt
chưa được dùng. Bạn phải thêm màn hình đích vào ứng dụng. Màn hình này có thể được dùng để tải ngầm tất cả dữ liệu cần thiết.
Màn hình đích sẽ chiếm toàn bộ màn hình và hiện biểu trưng của ứng dụng ở giữa màn hình. Tốt nhất là bạn nên hiện màn hình đích và sau khi toàn bộ dữ liệu đã được tải xong, bạn sẽ thông báo cho phương thức gọi rằng có thể bỏ qua màn hình đích bằng cách sử dụng lệnh gọi lại onTimeout
.
Bạn nên sử dụng coroutine Kotlin để thực hiện các thao tác không đồng bộ trên Android. Một ứng dụng thường sẽ sử dụng coroutine để tải ngầm mọi thứ khi ứng dụng khởi động. Jetpack Compose cung cấp các API giúp sử dụng coroutine an toàn trong lớp giao diện người dùng. Do ứng dụng này không giao tiếp với một phần phụ trợ (backend), bạn sẽ dùng hàm delay
của coroutine để mô phỏng việc tải ngầm các nội dung.
Hiệu ứng phụ trong Compose là sự thay đổi về trạng thái của ứng dụng bên ngoài phạm vi một hàm có khả năng kết hợp. Việc thay đổi trạng thái để hiện/ẩn màn hình đích sẽ xảy ra trong lệnh gọi lại onTimeout
và vì trước khi gọi onTimeout
, bạn cần tải mọi thứ bằng coroutine, nên việc thay đổi trạng thái cần diễn ra trong ngữ cảnh coroutine!
Để gọi các chức năng có thể tạm ngưng một cách an toàn từ bên trong thành phần kết hợp, hãy sử dụng API LaunchedEffect
để kích hoạt hiệu ứng phụ ở phạm vi coroutine trong Compose.
Khi LaunchedEffect
nhập một Thành phần, công cụ này sẽ khởi chạy một coroutine với khối mã được truyền dưới dạng tham số. Coroutine sẽ bị huỷ nếu LaunchedEffect
thoát khỏi thành phần đó.
Mặc dù mã tiếp theo không chính xác, nhưng hãy tìm hiểu cách sử dụng API này và thảo luận về lý do khiến mã sau không đúng. Bạn sẽ gọi thành phần kết hợp LandingScreen
sau trong bước này.
// home/LandingScreen.kt file
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// Start a side effect to load things in the background
// and call onTimeout() when finished.
// Passing onTimeout as a parameter to LaunchedEffect
// is wrong! Don't do this. We'll improve this code in a sec.
LaunchedEffect(onTimeout) {
delay(SplashWaitTime) // Simulates loading things
onTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
Một số API có hiệu ứng phụ như LaunchedEffect
sẽ lấy một số lượng biến khoá làm tham số dùng để khởi động lại hiệu ứng mỗi khi một trong các khoá đó thay đổi. Bạn đã phát hiện thấy lỗi? Chúng tôi không muốn khởi động lại LaunchedEffect
nếu phương thức gọi đến hàm có khả năng kết hợp này chuyển giá trị lambda onTimeout
khác. Điều đó sẽ khiến delay
bắt đầu lại và bạn sẽ không đáp ứng được các yêu cầu.
Hãy khắc phục vấn đề này. Để chỉ kích hoạt hiệu ứng phụ một lần trong vòng đời của thành phần kết hợp này, hãy dùng hằng số làm khoá, ví dụ như LaunchedEffect(Unit) { ... }
. Tuy nhiên, hiện có một vấn đề khác.
Nếu onTimeout
thay đổi trong khi hiệu ứng phụ đang diễn ra, không có gì đảm bảo rằng onTimeout
gần đây nhất sẽ được gọi khi hiệu ứng đó kết thúc. Để đảm bảo rằng onTimeout
gần đây nhất được gọi, hãy ghi nhớ onTimeout
bằng cách sử dụng API rememberUpdatedState
. API này sẽ thu thập và cập nhật giá trị mới nhất:
// home/LandingScreen.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.delay
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes or onTimeout changes,
// the delay shouldn't start again.
LaunchedEffect(Unit) {
delay(SplashWaitTime)
currentOnTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
Bạn nên sử dụng rememberUpdatedState
khi một biểu thức đối tượng hoặc biểu thức lambda dài hạn tham chiếu đến các tham số/giá trị được tính toán trong quá trình kết hợp. Trường hợp này thường xảy ra khi làm việc với LaunchedEffect
.
Hiện màn hình đích
Bây giờ, bạn cần hiện màn hình đích khi ứng dụng được mở. Mở tệp home/MainActivity.kt
và xem thành phần kết hợp MainScreen
được gọi đầu tiên.
Trong thành phần kết hợp MainScreen
, bạn chỉ cần thêm trạng thái nội bộ theo dõi liệu trang đích có xuất hiện hay không:
// home/MainActivity.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
Surface(color = MaterialTheme.colors.primary) {
var showLandingScreen by remember { mutableStateOf(true) }
if (showLandingScreen) {
LandingScreen(onTimeout = { showLandingScreen = false })
} else {
CraneHome(onExploreItemClicked = onExploreItemClicked)
}
}
}
Nếu chạy ứng dụng này ngay bây giờ, bạn sẽ thấy LandingScreen
xuất hiện và biến mất sau 2 giây.
6. rememberCoroutineScope
Ở bước này, bạn sẽ khiến ngăn điều hướng hoạt động. Hiện tại, sẽ không có gì xảy ra nếu bạn nhấn vào trình đơn ba đường kẻ.
Mở tệp home/CraneHome.kt
và xem thành phần kết hợp CraneHome
để biết bạn cần mở ngăn điều hướng ở đâu: trong lệnh gọi lại openDrawer
!
Trong CraneHome
, bạn có một scaffoldState
chứa DrawerState
. DrawerState
có các phương thức để mở và đóng ngăn điều hướng theo phương thức lập trình. Tuy nhiên, nếu bạn muốn ghi scaffoldState.drawerState.open()
trong lệnh gọi lại openDrawer
, bạn sẽ thấy một lỗi xuất hiện! Nguyên nhân vì hàm open
là hàm tạm ngưng. Chúng ta một lần nữa ở trong coroutine.
Ngoài API để giúp coroutine gọi an toàn từ lớp giao diện người dùng, một số API Compose là chức năng tạm ngưng. Một ví dụ cho trường hợp này là API để mở ngăn điều hướng. Các hàm tạm ngưng, ngoài việc có thể chạy mã không đồng bộ còn giúp trình bày các ý tưởng xảy ra theo thời gian. Do việc mở ngăn đòi hỏi thời gian, nên chuyển động và ảnh động tiềm năng cần được phản ánh hoàn hảo bằng chức năng tạm ngưng. Thao tác này sẽ tạm ngừng việc thực thi coroutine ở vị trí nó được gọi cho đến khi kết thúc và tiếp tục thực thi.
Phải gọi scaffoldState.drawerState.open()
trong coroutine. Bạn có thể làm gì? openDrawer
là một hàm callback đơn giản, do đó:
- Bạn không thể gọi các hàm có thể tạm ngưng trong đó bởi
openDrawer
không được thực thi trong bối cảnh một coroutine. - Bạn không thể sử dụng
LaunchedEffect
như trước vì chúng ta không thể gọi các thành phần kết hợp trongopenDrawer
. Chúng ta không ở trong Cấu trúc (Composition).
Nếu muốn chạy một coroutine, bạn nên dùng phạm vi nào? Lý tưởng nhất là bạn nên có CoroutineScope
tuân theo vòng đời của vị trí gọi. Việc sử dụng API rememberCoroutineScope
sẽ trả về một CoroutineScope
được liên kết với điểm trong Composition mà bạn gọi. Phạm vi sẽ tự động bị huỷ sau khi thoát khỏi Composition. Trong phạm vi đó, bạn có thể khởi động coroutine khi không ở trong Composition, chẳng hạn như trong lệnh gọi lại openDrawer
.
// home/CraneHome.kt file
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
@Composable
fun CraneHome(
onExploreItemClicked: OnExploreItemClicked,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.statusBarsPadding(),
drawerContent = {
CraneDrawer()
}
) {
val scope = rememberCoroutineScope()
CraneHomeContent(
modifier = modifier,
onExploreItemClicked = onExploreItemClicked,
openDrawer = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
}
Nếu chạy ứng dụng này, bạn sẽ thấy ngăn điều hướng mở ra khi nhấn vào biểu tượng trình đơn ba đường kẻ.
LaunchedEffect so với rememberCoroutineScope
Bạn không thể sử dụng LaunchedEffect
trong trường hợp này vì bạn cần kích hoạt lệnh gọi để tạo một coroutine trong lệnh gọi lại thông thường nằm ngoài Composition.
Xem lại bước trên màn hình đích đã dùng LaunchedEffect
, bạn có thể sử dụng rememberCoroutineScope
và gọi scope.launch { delay(); onTimeout(); }
thay vì sử dụng LaunchedEffect
không?
Bạn có thể làm vậy và dường như cách này có thể hiệu quả nhưng nó không đúng. Như đã giải thích trong tài liệu về Thinking in Compose (Tư duy trong Compose), bạn có thể gọi thành phần kết hợp (composable) bất cứ lúc nào. LaunchedEffect
đảm bảo rằng hiệu ứng phụ này sẽ được thực thi khi lệnh gọi thành phần kết hợp (composable) khiến nó nằm trong Composition. Nếu bạn sử dụng rememberCoroutineScope
và scope.launch
trong phần thân của LandingScreen
, thì coroutine sẽ được thực thi mỗi khi LandingScreen
được gọi bằng Compose bất kể lệnh gọi đó có khiến nó nằm trong Composition hay không. Do đó, bạn sẽ lãng phí tài nguyên và sẽ không thực thi hiệu ứng phụ này trong môi trường được kiểm soát.
7. Tạo phần tử giữ trạng thái
Bạn có thấy rằng nếu nhấn vào Choose Destination (Chọn điểm đến), bạn có thể chỉnh sửa trường này và lọc các thành phố dựa trên nội dung tìm kiếm đã nhập không? Bạn cũng có thể nhận thấy bất cứ khi nào bạn sửa đổi Choose Destination (Chọn điểm đến), kiểu văn bản sẽ thay đổi.
Mở tệp base/EditableUserInput.kt
. Thành phần kết hợp trạng thái CraneEditableUserInput
nhận được một vài tham số như hint
và caption
tương ứng với văn bản không bắt buộc bên cạnh biểu tượng. Ví dụ: caption
To (Tới) xuất hiện khi bạn tìm kiếm một điểm đến.
// base/EditableUserInput.kt file - code in the main branch
@Composable
fun CraneEditableUserInput(
hint: String,
caption: String? = null,
@DrawableRes vectorImageId: Int? = null,
onInputChanged: (String) -> Unit
) {
// TODO Codelab: Encapsulate this state in a state holder
var textState by remember { mutableStateOf(hint) }
val isHint = { textState == hint }
...
}
Tại sao?
Logic để cập nhật textState
và xác định xem nội dung đã hiển thị có tương ứng với gợi ý hay không đều nằm ở phần thân của thành phần kết hợp (composable) CraneEditableUserInput
. Điều này cũng gây ra một số nhược điểm:
- Giá trị của
TextField
không được nâng nên không thể bị kiểm soát từ bên ngoài, điều này khiến việc kiểm thử khó khăn hơn. - Logic của thành phần kết hợp (composable) này có thể phức tạp hơn và trạng thái nội bộ có thể thoát đồng bộ dễ dàng hơn.
Bằng cách tạo trình giữ trạng thái chịu trách nhiệm về trạng thái nội bộ của thành phần kết hợp (composable) này, bạn có thể tập trung tất cả các thay đổi về trạng thái ở cùng một nơi. Với cách này, trạng thái khó bị thoát đồng bộ hơn và logic liên quan sẽ được nhóm lại với nhau trong một lớp duy nhất. Hơn nữa, trạng thái này có thể dễ dàng được nâng lên và có thể được sử dụng từ người gọi của thành phần kết hợp (composable) này.
Trong trường hợp đó, bạn nên nâng trạng thái này do đây là thành phần giao diện người dùng cấp thấp có thể được sử dụng lại trong các phần khác của ứng dụng. Do đó, càng linh hoạt và có thể kiểm soát nhiều thì càng tốt.
Tạo phần tử giữ trạng thái
Do CraneEditableUserInput
là thành phần có thể sử dụng lại, hãy tạo một lớp thông thường làm phần tử giữ trạng thái có tên là EditableUserInputState
trong cùng tệp như sau:
// base/EditableUserInput.kt file
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class EditableUserInputState(private val hint: String, initialText: String) {
var text by mutableStateOf(initialText)
private set
fun updateText(newText: String) {
text = newText
}
val isHint: Boolean
get() = text == hint
}
Lớp phải có các đặc điểm sau:
text
là trạng thái khả biến thuộc loạiString
, tương tự như trạng thái bạn có trongCraneEditableUserInput
. Quan trọng là bạn phải sử dụngmutableStateOf
để Compose theo dõi các thay đổi đối với giá trị và kết hợp lại khi có thay đổi.text
là mộtvar
, có mộtset
riêng tư để không thể thay đổi trực tiếp từ bên ngoài lớp. Thay vì đặt biến này ở chế độ công khai, bạn có thể hiển thị một sự kiệnupdateText
để sửa đổi biến này, khiến lớp trở thành nguồn tin cậy duy nhất.- Lớp này lấy
initialText
làm phần phụ thuộc dùng để khởi độngtext
. - Logic để biết liệu
text
có phải là gợi ý hay không nằm trong thuộc tínhisHint
thực hiện việc kiểm tra theo yêu cầu.
Nếu logic ngày một phức tạp hơn, bạn chỉ cần thực hiện các thay đổi cho một lớp: EditableUserInputState
.
Ghi nhớ phần tử giữ trạng thái
Các trình giữ trạng thái luôn cần được ghi nhớ để có thể giữ chúng trong Composition và không phải lúc nào cũng tạo kênh mới. Nên tạo phương thức trong cùng một tệp để thực hiện việc này nhằm xoá bản mẫu và tránh mọi lỗi có thể xảy ra. Trong tệp base/EditableUserInput.kt
, hãy thêm mã sau:
// base/EditableUserInput.kt file
@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
remember(hint) {
EditableUserInputState(hint, hint)
}
Nếu bạn chỉ remember
trạng thái này, thì trạng thái sẽ không tồn tại khi tạo lại hoạt động. Để trạng thái này tồn tại, bạn có thể dùng API rememberSaveable
thay vì API hoạt động tương tự như remember
. Tuy nhiên, giá trị được lưu trữ cũng vẫn tồn tại trong quá trình tạo lại hoạt động và quy trình. Về phía nội bộ, trạng thái này sẽ sử dụng cơ chế trạng thái của thực thể đã lưu.
rememberSaveable
thực hiện tất cả thao tác này mà không cần phải thực hiện thêm thao tác nào cho các đối tượng có thể được lưu trữ trong Bundle
. Đó không phải là trường hợp của lớp EditableUserInputState
mà bạn đã tạo trong dự án. Do đó, bạn cần cho rememberSaveable
biết cách lưu và khôi phục một thực thể của lớp này bằng cách sử dụng Saver
.
Tạo một trình lưu tuỳ chỉnh
Saver
mô tả cách chuyển đổi một đối tượng thành Saveable
. Triển khai Saver
cần phải ghi đè hai hàm:
save
để chuyển đổi giá trị ban đầu thành một giá trị có thể lưu.restore
để chuyển đổi giá trị được khôi phục sang một thực thể của lớp ban đầu.
Đối với trường hợp này, thay vì tạo phương thức triển khai tuỳ chỉnh Saver
cho lớp EditableUserInputState
, bạn có thể dùng một vài API hiện có trong Compose, chẳng hạn như listSaver
hoặc mapSaver
(lưu trữ các giá trị để lưu vào List
hoặc Map
) nhằm giảm số lượng mã cần viết.
Nên đặt các định nghĩa Saver
gần với lớp mà chúng tương tác. Vì cần truy cập tĩnh, nên hãy thêm Saver
cho EditableUserInputState
trong companion object
. Trong tệp base/EditableUserInput.kt
, hãy thêm phương thức triển khai Saver
:
// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
class EditableUserInputState(private val hint: String, initialText: String) {
var text by mutableStateOf(initialText)
val isHint: Boolean
get() = text == hint
companion object {
val Saver: Saver<EditableUserInputState, *> = listSaver(
save = { listOf(it.hint, it.text) },
restore = {
EditableUserInputState(
hint = it[0],
initialText = it[1],
)
}
)
}
}
Trong trường hợp này, bạn sẽ dùng listSaver
làm chi tiết triển khai để lưu trữ và khôi phục một thực thể của EditableUserInputState
trong trình lưu.
Bây giờ, bạn có thể sử dụng trình lưu này trong rememberSaveable
(thay vì remember
) trong phương thức rememberEditableUserInputState
mà bạn tạo trước đây:
// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
rememberSaveable(hint, saver = EditableUserInputState.Saver) {
EditableUserInputState(hint, hint)
}
Bằng cách này, trạng thái đã ghi nhớ EditableUserInput
sẽ vẫn tồn tại khi tạo lại quy trình và hoạt động.
Sử dụng phần tử giữ trạng thái
Bạn sẽ sử dụng EditableUserInputState
thay vì text
và isHint
, nhưng bạn không muốn chỉ sử dụng nó làm trạng thái nội bộ trong CraneEditableUserInput
do thành phần kết hợp của phương thức gọi không thể kiểm soát trạng thái. Thay vào đó, bạn muốn chuyển EditableUserInputState
lên trên để các phương thức gọi có thể kiểm soát trạng thái của CraneEditableUserInput
. Nếu chuyển trạng thái lên trên thì thành phần kết hợp có thể được sử dụng ở chế độ xem trước và sẽ được kiểm thử dễ dàng hơn vì bạn có thể sửa đổi trạng thái từ phương thức gọi.
Để thực hiện việc này, bạn cần thay đổi các tham số của hàm có khả năng kết hợp và cung cấp giá trị mặc định nếu cần. Do có thể bạn sẽ muốn để cho CraneEditableUserInput
có gợi ý trống, nên hãy thêm một đối số mặc định:
@Composable
fun CraneEditableUserInput(
state: EditableUserInputState = rememberEditableUserInputState(""),
caption: String? = null,
@DrawableRes vectorImageId: Int? = null
) { /* ... */ }
Có thể bạn đã nhận thấy rằng tham số onInputChanged
không còn tồn tại! Do trạng thái có thể được chuyển lên, nên nếu phương thức gọi muốn biết liệu thông tin đầu vào có thay đổi hay không, chúng có thể kiểm soát trạng thái và chuyển trạng thái đó vào hàm này.
Tiếp theo, bạn cần tinh chỉnh phần nội dung hàm để sử dụng trạng thái đã nâng thay vì trạng thái nội bộ đã dùng trước đó. Sau khi được cải tiến cấu trúc, hàm sẽ có dạng như sau:
@Composable
fun CraneEditableUserInput(
state: EditableUserInputState = rememberEditableUserInputState(""),
caption: String? = null,
@DrawableRes vectorImageId: Int? = null
) {
CraneBaseUserInput(
caption = caption,
tintIcon = { !state.isHint },
showCaption = { !state.isHint },
vectorImageId = vectorImageId
) {
BasicTextField(
value = state.text,
onValueChange = { state.updateText(it) },
textStyle = if (state.isHint) {
captionTextStyle.copy(color = LocalContentColor.current)
} else {
MaterialTheme.typography.body1.copy(color = LocalContentColor.current)
},
cursorBrush = SolidColor(LocalContentColor.current)
)
}
}
Phương thức gọi của phần tử giữ trạng thái
Vì bạn đã thay đổi API của CraneEditableUserInput
, nên bạn cần kiểm tra tất cả các vị trí mà API đó được gọi để đảm bảo truyền đúng tham số.
Vị trí duy nhất trong dự án mà bạn gọi API này là trong tệp home/SearchUserInput.kt
. Mở tệp đó và chuyển đến hàm có khả năng kết hợp ToDestinationUserInput
, bạn sẽ thấy lỗi bản dựng ở đó. Do gợi ý hiện là một phần của phần tử giữ trạng thái và bạn muốn gợi ý tuỳ chỉnh cho thực thể này của CraneEditableUserInput
trong Composition, nên bạn sẽ cần ghi nhớ trạng thái ở cấp độ ToDestinationUserInput
và chuyển trạng thái đó vào CraneEditableUserInput
:
// home/SearchUserInput.kt file
import androidx.compose.samples.crane.base.rememberEditableUserInputState
@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
CraneEditableUserInput(
state = editableUserInputState,
caption = "To",
vectorImageId = R.drawable.ic_plane
)
}
snapshotFlow
Mã ở trên thiếu chức năng thông báo cho phương thức gọi của ToDestinationUserInput
khi thông tin đầu vào thay đổi. Do cấu trúc của ứng dụng nên bạn không muốn chuyển EditableUserInputState
lên cấp cao hơn trong hệ phân cấp. Bạn không muốn ghép các thành phần kết hợp khác như FlySearchContent
với trạng thái này. Bạn có thể gọi hàm lambda onToDestinationChanged
từ ToDestinationUserInput
như thế nào để vẫn có thể sử dụng lại thành phần kết hợp này?
Bạn có thể kích hoạt hiệu ứng phụ bằng LaunchedEffect
mỗi khi thay đổi dữ liệu đầu vào và gọi hàm lambda onToDestinationChanged
:
// home/SearchUserInput.kt file
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
CraneEditableUserInput(
state = editableUserInputState,
caption = "To",
vectorImageId = R.drawable.ic_plane
)
val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
LaunchedEffect(editableUserInputState) {
snapshotFlow { editableUserInputState.text }
.filter { !editableUserInputState.isHint }
.collect {
currentOnDestinationChanged(editableUserInputState.text)
}
}
}
Bạn đã sử dụng LaunchedEffect
và rememberUpdatedState
trước đó, nhưng mã ở trên cũng sử dụng API mới! API snapshotFlow
chuyển đổi các đối tượng State<T>
trong Compose thành Flow (Luồng). Khi thông số trạng thái bên trong snapshotFlow
thay đổi, Flow (Luồng) sẽ tạo ra giá trị mới cho trình thu thập. Trong trường hợp này, bạn chuyển đổi trạng thái thành một luồng để sử dụng sức mạnh của các toán tử luồng. Bằng cách đó, bạn filter
khi text
không phải là hint
và collect
các mục được tạo ra để thông báo cho mục chính rằng điểm đến hiện tại đã thay đổi.
Không có sự thay đổi nào về hình ảnh trong bước này của lớp học lập trình, nhưng bạn đã cải thiện chất lượng của phần mã này. Nếu chạy ứng dụng ngay bây giờ, bạn sẽ thấy mọi thứ hoạt động như trước đây.
8. DisposableEffect
Khi nhấn vào một điểm đến, màn hình chi tiết sẽ mở ra và bạn có thể thấy vị trí của thành phố trên bản đồ. Mã đó nằm trong tệp details/DetailsActivity.kt
. Trong thành phần kết hợp CityMapView
, bạn đang gọi hàm rememberMapViewWithLifecycle
. Nếu mở hàm này (vốn nằm trong tệp details/MapViewUtils.kt
), thì bạn sẽ thấy hàm không được kết nối với bất kỳ vòng đời nào! Chrome chỉ ghi nhớ MapView
và gọi onCreate
trên đó:
// details/MapViewUtils.kt file - code in the main branch
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current
// TODO Codelab: DisposableEffect step. Make MapView follow the lifecycle
return remember {
MapView(context).apply {
id = R.id.map
onCreate(Bundle())
}
}
}
Mặc dù ứng dụng chạy bình thường, nhưng đây là một vấn đề do MapView
không tuân theo đúng vòng đời. Do đó, bạn sẽ không biết khi nào ứng dụng chuyển sang chạy ngầm, khi nào nên tạm dừng View (Chế độ xem), v.v. Hãy cùng khắc phục lỗi này!
Vì MapView
là một Khung hiển thị chứ không phải thành phần kết hợp nên bạn muốn nó tuân theo vòng đời của Hoạt động mà theo đó nó được sử dụng thay vì vòng đời của Composition. Điều đó nghĩa là bạn cần tạo LifecycleEventObserver
để theo dõi các sự kiện trong vòng đời và gọi các phương thức phù hợp trên MapView
. Sau đó, bạn cần thêm trình quan sát này vào vòng đời của hoạt động hiện tại.
Bắt đầu bằng cách tạo một hàm trả về LifecycleEventObserver
để gọi các phương thức tương ứng trong một MapView
dựa trên một sự kiện nhất định:
// details/MapViewUtils.kt file
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
Lifecycle.Event.ON_START -> mapView.onStart()
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
Lifecycle.Event.ON_STOP -> mapView.onStop()
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> throw IllegalStateException()
}
}
Bây giờ, bạn cần thêm trình quan sát này vào vòng đời hiện tại để có thể sử dụng LifecycleOwner
hiện tại với cấu trúc (composition) LocalLifecycleOwner
cục bộ. Tuy nhiên, việc thêm trình quan sát là vẫn chưa đủ, bạn còn cần phải xoá nó! Bạn cần có một hiệu ứng phụ cho biết thời điểm hiệu ứng đó rời khỏi Composition này để bạn có thể thực thi mã dọn dẹp nào đó. API hiệu ứng phụ mà bạn đang tìm kiếm là DisposableEffect
.
DisposableEffect
dành cho các hiệu ứng phụ cần được dọn dẹp sau khi các khoá thay đổi hoặc thành phần kết hợp rời khỏi Composition. Mã rememberMapViewWithLifecycle
cuối cùng sẽ hoạt động chính xác như vậy. Triển khai các dòng sau trong dự án của bạn:
// details/MapViewUtils.kt file
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
@Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = LocalContext.current
val mapView = remember {
MapView(context).apply {
id = R.id.map
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(key1 = lifecycle, key2 = mapView) {
// Make MapView follow the current lifecycle
val lifecycleObserver = getMapLifecycleObserver(mapView)
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
return mapView
}
Trình quan sát được thêm vào lifecycle
hiện tại và sẽ bị xoá khi vòng đời hiện tại thay đổi hoặc thành phần kết hợp này rời khỏi Composition (Cấu trúc). Với key
trong DisposableEffect
, nếu lifecycle
hoặc mapView
thay đổi, trình quan sát sẽ bị xoá rồi thêm lại vào lifecycle
bên phải.
Với những thay đổi bạn vừa thực hiện, MapView
sẽ luôn tuân theo lifecycle
của LifecycleOwner
hiện tại và hành vi của nó sẽ giống như khi được sử dụng trong môi trường Khung hiển thị này.
Hãy thoải mái chạy ứng dụng này và mở màn hình chi tiết để đảm bảo MapView
vẫn hiển thị chính xác. Không có thay đổi nào về hình ảnh trong bước này.
9. produceState
Trong phần này, bạn sẽ cải thiện cách màn hình chi tiết khởi động. Thành phần kết hợp DetailsScreen
trong tệp details/DetailsActivity.kt
sẽ nhận cityDetails
một cách đồng bộ từ ViewModel và gọi DetailsContent
nếu kết quả thành công.
Tuy nhiên, cityDetails
có thể sẽ tốn nhiều phí tải hơn trên luồng giao diện người dùng và có thể sử dụng coroutine để chuyển dữ liệu sang một luồng khác. Bạn sẽ cải thiện mã này để thêm một màn hình tải và hiển thị DetailsContent
khi dữ liệu đã sẵn sàng.
Một cách để mô hình hoá trạng thái màn hình là sử dụng lớp sau đây, trong đó bao hàm mọi khả năng: dữ liệu cần hiển thị trên màn hình cũng như tín hiệu tải và tín hiệu lỗi. Thêm lớp DetailsUiState
vào tệp DetailsActivity.kt
:
// details/DetailsActivity.kt file
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
Bạn có thể liên kết những nội dung mà màn hình cần hiển thị và UiState
trong lớp ViewModel bằng cách sử dụng luồng dữ liệu, StateFlow
thuộc loại DetailsUiState
mà ViewModel cập nhật khi thông tin đã sẵn sàng và nội dung mà Compose thu thập bằng API collectAsStateWithLifecycle()
bạn đã biết.
Tuy nhiên, để thực hiện bài tập này, bạn sẽ triển khai một phương án thay thế. Nếu muốn di chuyển logic liên kết uiState
sang môi trường Compose, bạn có thể sử dụng API produceState
.
produceState
cho phép bạn chuyển đổi trạng thái không phải Compose thành Trạng thái Compose. Tệp này sẽ khởi chạy một coroutine trong phạm vi Composition, coroutine này có thể đẩy các giá trị vào State
được trả về bằng thuộc tính value
. Tương tự như LaunchedEffect
, produceState
cũng lấy các khoá để huỷ và khởi động lại việc tính toán.
Trong trường hợp sử dụng của bạn, bạn có thể sử dụng produceState
để phát hành các bản cập nhật uiState
có giá trị ban đầu là DetailsUiState(isLoading = true)
như sau:
// details/DetailsActivity.kt file
import androidx.compose.runtime.produceState
@Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
// In a coroutine, this can call suspend functions or move
// the computation to different Dispatchers
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
// TODO: ...
}
Tiếp theo, tuỳ thuộc vào uiState
, bạn sẽ cho hiện dữ liệu, màn hình tải hay báo cáo lỗi. Dưới đây là mã hoàn chỉnh cho thành phần kết hợp DetailsScreen
:
// details/DetailsActivity.kt file
import androidx.compose.foundation.layout.Box
import androidx.compose.material.CircularProgressIndicator
@Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
Nếu chạy ứng dụng thì bạn sẽ thấy cách vòng quay đang tải xuất hiện trước khi cho thấy thông tin chi tiết về thành phố.
10. derivedStateOf
Cải tiến cuối cùng bạn sẽ thực hiện với ứng dụng Crane là hiện nút Scroll to top (Di chuyển lên đầu) bất cứ khi nào bạn di chuyển trong danh sách điểm đến của chuyến bay sau khi truyền phần tử đầu tiên của màn hình. Khi nhấn vào nút đó, bạn sẽ được chuyển đến phần tử đầu tiên trong danh sách.
Mở tệp base/ExploreSection.kt
chứa mã này. Thành phần kết hợp ExploreSection
tương ứng với những gì bạn thấy trong nền của scaffold (giàn giáo).
Để tính toán xem liệu người dùng đã truyền mục đầu tiên hay chưa, hãy sử dụng LazyListState
của LazyColumn
và kiểm tra xem có phải là listState.firstVisibleItemIndex > 0
hay không.
Cách triển khai đơn thuần sẽ có dạng như sau:
// DO NOT DO THIS - It's executed on every recomposition
val showButton = listState.firstVisibleItemIndex > 0
Giải pháp này không hiệu quả như mọi khi vì hàm có khả năng kết hợp đọc showButton
sẽ kết hợp lại mỗi khi firstVisibleItemIndex
thay đổi – điều này xảy ra thường xuyên khi cuộn. Thay vào đó, bạn muốn hàm này chỉ kết hợp lại khi điều kiện thay đổi giữa true
và false
.
Có một API cho phép bạn làm việc này, đó là: API derivedStateOf
.
listState
là một State
Compose có thể quan sát. Theo tính toán của bạn, showButton
cũng cần phải là State
Compose vì bạn muốn giao diện người dùng kết hợp lại khi giá trị của giao diện đó thay đổi và hiện hoặc ẩn nút này.
Hãy sử dụng derivedStateOf
khi bạn muốn State
Compose được lấy từ State
khác. Khối tính toán derivedStateOf
sẽ được thực thi mỗi khi trạng thái nội bộ thay đổi, nhưng hàm có khả năng kết hợp chỉ kết hợp lại khi kết quả tính toán khác với kết quả sau cùng. Việc này giúp giảm thiểu số lần các hàm đọc showButton
kết hợp lại.
Trong trường hợp này, việc sử dụng API derivedStateOf
là giải pháp thay thế tốt hơn và hiệu quả hơn. Bạn cũng gói lệnh gọi bằng API remember
, vì vậy, giá trị đã tính vẫn tồn tại sau khi cấu trúc lại.
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary recompositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
Mã mới cho thành phần kết hợp ExploreSection
chắc hẳn quen thuộc với bạn. Bạn sẽ dùng Box
để đặt Button
được hiện một cách có điều kiện lên đầu ExploreList
. Và bạn dùng rememberCoroutineScope
để gọi hàm có thể tạm ngưng listState.scrollToItem
trong lệnh gọi onClick
của Button
.
// base/ExploreSection.kt file
import androidx.compose.material.FloatingActionButton
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.foundation.layout.navigationBarsPadding
import kotlinx.coroutines.launch
@Composable
fun ExploreSection(
modifier: Modifier = Modifier,
title: String,
exploreList: List<ExploreModel>,
onItemClicked: OnExploreItemClicked
) {
Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
Text(
text = title,
style = MaterialTheme.typography.caption.copy(color = crane_caption)
)
Spacer(Modifier.height(8.dp))
Box(Modifier.weight(1f)) {
val listState = rememberLazyListState()
ExploreList(exploreList, onItemClicked, listState = listState)
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
val coroutineScope = rememberCoroutineScope()
FloatingActionButton(
backgroundColor = MaterialTheme.colors.primary,
modifier = Modifier
.align(Alignment.BottomEnd)
.navigationBarsPadding()
.padding(bottom = 8.dp),
onClick = {
coroutineScope.launch {
listState.scrollToItem(0)
}
}
) {
Text("Up!")
}
}
}
}
}
}
Nếu chạy ứng dụng, bạn sẽ thấy nút xuất hiện ở dưới cùng sau khi bạn di chuyển và truyền phần tử đầu tiên của màn hình.
11. Xin chúc mừng!
Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu các khái niệm nâng cao về API trạng thái và hiệu ứng phụ trong ứng dụng Jetpack Compose!
Bạn đã tìm hiểu cách tạo trình giữ trạng thái, API hiệu ứng phụ như LaunchedEffect
, rememberUpdatedState
, DisposableEffect
, produceState
và derivedStateOf
cũng như cách sử dụng coroutine trong Jetpack Compose.
Tiếp theo là gì?
Hãy xem các lớp học lập trình khác trên Lộ trình Compose và các mã mẫu khác bao gồm Crane.
Tài liệu
Để biết thêm thông tin và hướng dẫn về những chủ đề này, vui lòng xem các tài liệu sau: