Trong trang này, bạn sẽ tìm hiểu về vòng đời (lifecycle) của thành phần kết hợp cũng như cách Compose quyết định liệu thành phần kết hợp có cần kết hợp lại hay không.
Tổng quan về vòng đời
Như đã đề cập trong Tài liệu về quản lý trạng thái, thành phần Compose mô tả Giao diện người dùng của ứng dụng và được tạo ra bằng cách chạy các thành phần kết hợp. Một thành phần Compose là cấu trúc dạng cây của các thành phần kết hợp mô tả Giao diện người dùng.
Khi Jetpack Compose chạy các thành phần kết hợp lần đầu tiên, trong quá trình kết hợp ban đầu, bộ công cụ này sẽ theo dõi các thành phần kết hợp bạn gọi để mô tả Giao diện người dùng trong một thành phần Compose. Sau đó, khi trạng thái ứng dụng thay đổi, Jetpack Compose sẽ lên lịch kết hợp lại. Quá trình kết hợp lại xảy ra khi Jetpack Compose tái thực thi các thành phần kết hợp có thể thay đổi theo thay đổi về trạng thái, tiếp đến cập nhật thành phần Compose để phản ánh mọi thay đổi.
Thành phần Compose chỉ có thể được quá trình kết hợp ban đầu tạo ra và cập nhật bằng quá trình kết hợp lại. Phương pháp duy nhất để chỉnh sửa thành phần Compose là kết hợp lại.
Hình 1. Vòng đời của một thành phần kết hợp trong thành phần Compose. Thành phần kết hợp này sẽ được nhập vào thành phần Compose, kết hợp lại từ 0 lần trở lên, cuối cùng ra khỏi thành phần Compose.
Quá trình kết hợp lại thường được kích hoạt khi có thay đổi đối với
đối tượng State<T>
. Compose sẽ theo dõi quá trình thay đổi này, đồng thời chạy tất cả thành phần kết hợp trong thành phần Compose có khả năng đọc State<T>
và bất kỳ thành phần kết hợp quá trình này gọi mà không thể
bỏ qua.
Nếu một thành phần kết hợp được gọi nhiều lần thì nhiều thực thể sẽ được đặt trong thành phần Compose. Mỗi lệnh gọi có vòng đời riêng trong thành phần Compose.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Hình 2. Giá trị đại diện của MyComposable
trong thành phần Compose. Nếu một thành phần kết hợp được gọi nhiều lần, thì nhiều thực thể sẽ được đặt trong
thành phần Compose. Phần tử có màu riêng biểu thị một thực thể riêng biệt.
Phân tích một thành phần kết hợp trong thành phần Compose
Thực thể của một thành phần kết hợp trong thành phần Compose được xác định qua vị trí gọi. Trình biên dịch Compose xem mỗi vị trí gọi hoàn toàn riêng biệt với nhau. Thao tác gọi các thành phần kết hợp từ nhiều vị trí gọi sẽ tạo ra nhiều thực thể của thành phần kết hợp trong thành phần Compose.
Nếu trong quá trình kết hợp lại một thành phần kết hợp gọi các thành phần kết hợp khác những thành phần đã gọi trong quá trình kết hợp trước, Compose sẽ xác định các thành phần kết hợp nào đã được gọi. Đồng thời, đối với các thành phần được gọi trong cả hai quá trình kết hợp, Compose sẽ tránh kết hợp lại nếu giá trị đầu vào không đổi.
Việc bảo tồn mã nhận dạng là yếu tố quan trọng nhằm liên kết các hiệu ứng phụ với thành phần kết hợp. Nhờ đó, các hiệu ứng phụ này có thể hoàn tất thành công thay vì phải tái khởi động cho mỗi quá trình kết hợp lại.
Hãy xem ví dụ sau đây:
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
Trong đoạn mã trên, LoginScreen
sẽ gọi thành phần kết hợp
LoginError
theo điều kiện và sẽ luôn gọi thành phần kết hợp LoginInput
. Mỗi lệnh gọi có một vị trí gọi và địa điểm nguồn duy nhất mà trình biên dịch sẽ sử dụng để xác định.
Hình 3. Giá trị đại diện của LoginScreen
trong thành phần Compose khi trạng thái
thay đổi và quá trình kết hợp lại diễn ra. Cùng màu có nghĩa mã này vẫn chưa được kết hợp lại.
Mặc dù mức độ ưu tiên của việc gọi LoginInput
chuyển từ thứ nhất sang thứ hai, nhưng
LoginInput
vẫn sẽ được giữ nguyên trong các thành phần kết hợp lại. Ngoài ra, do LoginInput
không có bất kỳ tham số nào thay đổi
trong quá trình kết hợp
lại, Compose sẽ bỏ qua lệnh gọi LoginInput
.
Thêm thông tin bổ sung nhằm hỗ trợ quá trình kết hợp lại thông minh
Việc gọi một thành phần kết hợp nhiều lần cũng sẽ thêm thành phần đó vào thành phần kết hợp Compose nhiều lần. Khi gọi thành phần kết hợp nhiều lần từ cùng một vị trí gọi, Compose không có bất kỳ thông tin nào để nhận dạng từng lệnh gọi đến thành phần trên. Do đó, thứ tự thực thi được sử dụng cùng vị trí gọi để tách biệt các thực thể. Hành vi này đôi khi cần thiết, nhưng trong một số trường hợp có thể gây ra hành vi không mong muốn.
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
Trong ví dụ trên, Compose sử dụng thứ tự thực thi ngoài vị trí gọi để tách biệt thực thể trong thành phần Compose. Nếu một movie
mới được thêm vào cuối của danh sách thì Compose có thể tái sử dụng các thực thể có sẵn trong thành phần Compose vì vị trí của chúng trong danh sách chưa thay đổi, do đó, giá trị đầu vào của những thực thể trên
movie
vẫn còn nguyên vẹn.
Hình 4. Giá trị đại diện của MoviesScreen
trong thành phần Compose khi thêm một phần tử mới vào cuối danh sách. Có thể tái sử dụng thành phần kết hợp MovieOverview
trong
thành phần Compose MovieOverview
cùng màu có nghĩa thành phần kết hợp
chưa được kết hợp lại.
Tuy nhiên, nếu danh sách movies
thay đổi bằng cách thêm vào phần đầu hoặc
phần giữa thì việc xoá hoặc sắp xếp lại các mục sẽ tạo ra quá trình kết hợp lại trong tất cả lệnh gọi MovieOverview
có tham số đầu vào đã thay đổi vị trí trong danh sách. Thao tác này có thể gây ảnh hưởng vô cùng nghiêm trọng. Chẳng hạn như MovieOverview
tìm nạp hình ảnh phim bằng hiệu ứng phụ. Nếu quá trình kết hợp lại diễn ra khi hiệu ứng đang tiến hành thì hiệu ứng đó sẽ bị huỷ và phải bắt đầu lại.
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
Hình 5. Giá trị đại diện của MoviesScreen
trong thành phần Compose khi thêm một phần tử mới vào danh sách. Các thành phần kết hợp MovieOverview
không thể được tái sử dụng, đồng thời
tất cả hiệu ứng phụ sẽ khởi động lại. MovieOverview
mang màu khác có nghĩa thành phần kết hợp đã được kết hợp lại.
Lý tưởng là chúng ta có thể xem mã nhận dạng của thực thể MovieOverview
liên kết với mã nhận dạng của movie
được truyền vào. Nếu sắp xếp lại danh sách phim, tốt nhất chúng ta nên sắp xếp lại các thực thể trong cây thành phần Compose thay vì kết hợp lại từng thành phần kết hợp MovieOverview
với
một thực thể movie (phim) khác. Bạn có thể sử dụng Compose để báo thời gian chạy giá trị muốn sử dụng nhằm xác định một phần của cây: thành phần kết hợp key
.
Bằng cách gói một khối mã cùng lệnh gọi thành phần kết hợp khoá có một hoặc nhiều
giá trị được truyền, những giá trị đó sẽ được kết hợp để xác định thực thể trong thành phần Compose. Giá trị của một key
không nhất thiết phải là giá trị toàn cục mà chỉ cần là giá trị duy nhất trong các lệnh gọi
của thành phần kết hợp tại vị trí gọi. Vì vậy, trong ví dụ này, mỗi movie
cần có một
key
duy nhất trong số movies
. Tuy vậy, nếu chia sẻ key
này với
các thành phần kết hợp tại vị trí khác của ứng dụng thì vẫn ổn.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
Với các tuỳ chọn nêu trên, ngay cả khi các phần tử trong danh sách thay đổi, Compose vẫn nhận ra
các lệnh gọi riêng lẻ đến MovieOverview
và có thể tái sử dụng các lệnh gọi đó.
Hình 6. Giá trị đại diện của MoviesScreen
trong thành phần Compose khi thêm một phần tử mới vào danh sách. Vì thành phần kết hợp MovieOverview
có các khoá
riêng biệt nên Compose nhận ra thực thể MovieOverview
nào không thay đổi và
có thể sử dụng lại các thực thể đó; các hiệu ứng phụ của những thực thể này vẫn tiếp tục được thực thi.
Một vài thành phần kết hợp đã có hỗ trợ sẵn cho thành phần kết hợp key
. Ví dụ:
LazyColumn
chấp nhận việc chỉ định một key
tuỳ chỉnh trong DSL items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Bỏ qua nếu giá trị đầu vào không đổi
Trong quá trình kết hợp lại, một số hàm có khả năng kết hợp đủ điều kiện có thể bị bỏ qua hoàn toàn quá trình thực thi nếu dữ liệu đầu vào của chúng không thay đổi so với thành phần trước đó.
Hàm có khả năng kết hợp đủ điều kiện để bỏ qua trừ phi:
- Hàm có kiểu dữ liệu trả về không phải là
Unit
- Hàm này được chú giải bằng
@NonRestartableComposable
hoặc@NonSkippableComposable
- Tham số bắt buộc thuộc loại không ổn định
Có một chế độ trình biên dịch thử nghiệm, Strong Skipping (Bỏ qua mạnh), giúp nới lỏng yêu cầu cuối cùng.
Để được coi là ổn định, một kiểu phải tuân thủ hợp đồng sau:
- Kết quả
equals
cho hai thực thể sẽ vĩnh viễn giống nhau với cùng hai thực thể trên. - Nếu thuộc tính công khai của kiểu ổn định thay đổi, thành phần Compose sẽ được thông báo.
- Mọi kiểu thuộc tính công cộng đều ổn định.
Có một số kiểu phổ biến quan trọng thuộc hợp đồng này mà trình biên dịch Compose sẽ coi là ổn định, mặc dù chúng không được đánh dấu rõ ràng là ổn định bằng cách sử dụng chú thích @Stable
:
- Tất cả kiểu giá trị gốc:
Boolean
,Int
,Long
,Float
,Char
, v.v. - Chuỗi
- Tất cả kiểu hàm (lambda)
Vì không thể thay đổi, tất cả kiểu trên đều có thể tuân theo hợp đồng ổn định. Vì các kiểu không thể thay đổi luôn giữ nguyên nên không cần phải thông báo thay đổi cho thành phần Compose, do đó, việc tuân thủ hợp đồng sẽ dễ dàng hơn rất nhiều.
Một kiểu ổn định nhưng có thể thay đổi đáng lưu ý là kiểu MutableState
của Compose. Nếu một giá trị được nắm giữ trong MutableState
thì tổng thể đối tượng trạng thái
sẽ được coi là ổn định vì Compose sẽ được thông báo về mọi thay đổi đối với
thuộc tính .value
của State
.
Khi tất cả các kiểu được truyền dưới dạng tham số đến thành phần kết hợp đều ổn định, các giá trị tham số sẽ được so sánh bằng nhau dựa trên vị trí có thể kết hợp trong cây Giao diện người dùng. Quá trình kết hợp lại được bỏ qua nếu tất cả các giá trị không thay đổi kể từ lệnh gọi trước.
Compose chỉ xem một kiểu ổn định nếu có thể chứng minh kiểu này. Ví dụ: giao diện thường được coi như không ổn định, các kiểu thuộc tính công khai có thể thay đổi mà thao tác triển khai không đổi cũng được coi như không ổn định.
Nếu Compose không thể dự đoán nhưng bạn muốn buộc phải xem kiểu này là một kiểu ổn định, hãy đánh dấu bằng chú giải @Stable
.
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
Trong đoạn mã trên, vì UiState
là một giao diện nên Compose thường có thể coi kiểu này là không ổn định. Bằng cách thêm chú giải @Stable
, bạn cho Compose biết kiểu trên ổn định và cho phép bộ công cụ này ưu tiên
quá trình kết hợp lại thông minh. Điều này cũng có nghĩa Compose sẽ coi tất cả thao tác triển khai là ổn định nếu giao diện được sử dụng làm kiểu tham số.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Trạng thái và Jetpack Compose
- Những hiệu ứng phụ trong ứng dụng Compose
- Lưu trạng thái giao diện người dùng trong Compose