Giống như hầu hết bộ công cụ giao diện người dùng khác, Compose sẽ hiển thị một khung hình qua nhiều giai đoạn riêng biệt. Ví dụ: hệ thống Android View có 3 giai đoạn chính: đo lường, bố cục và bản vẽ. Compose thì rất giống nhưng có thêm một giai đoạn quan trọng gọi là thành phần (composition) khi bắt đầu.
Tài liệu Compose mô tả thành phần trong Tư duy trong Compose và Trạng thái và Jetpack Compose.
Ba giai đoạn của một khung
Compose có ba giai đoạn chính:
- Thành phần (Composition): Nội dung mà giao diện người dùng sẽ hiển thị. Compose chạy các hàm có khả năng kết hợp và tạo nội dung mô tả giao diện người dùng.
- Bố cục (Layout): Vị trí để đặt giao diện người dùng. Giai đoạn này bao gồm hai bước: đo lường và đặt vị trí. Các thành phần bố cục đo lường và đặt vị trí cho chính nó và cho mọi thành phần con trong các toạ độ 2D vào mỗi nút trong cây bố cục.
- Bản vẽ (Drawing): Cách hiển thị. Các thành phần trên giao diện người dùng vẽ vào Canvas, thường là màn hình thiết bị.

Thứ tự của các giai đoạn này thường giống nhau, cho phép dữ liệu truyền theo một hướng từ thành phần đến bố cục đến bản vẽ để tạo một khung (còn gọi là luồng dữ liệu một chiều). BoxWithConstraints
, LazyColumn
và LazyRow
là các trường hợp ngoại lệ đáng chú ý, trong đó thành phần của các thành phần con phụ thuộc vào giai đoạn bố cục của thành phần mẹ.
Về mặt khái niệm, mỗi giai đoạn này xảy ra đối với mọi khung hình; tuy nhiên, để tối ưu hoá hiệu suất, Compose sẽ tránh lặp lại các công việc cho ra cùng một kết quả với dữ liệu đầu vào giống nhau trong tất cả các giai đoạn này. Compose bỏ qua việc chạy một hàm có khả năng kết hợp nếu có thể sử dụng lại kết quả cũ, đồng thời giao diện người dùng Compose sẽ không bố trí lại hoặc vẽ lại toàn bộ cây nếu không cần thiết. Compose chỉ thực hiện lượng công việc tối thiểu cần thiết để cập nhật giao diện người dùng. Quá trình tối ưu hoá này có thể diễn ra vì Compose theo dõi việc đọc trạng thái trong các giai đoạn khác nhau.
Tìm hiểu các giai đoạn
Phần này mô tả chi tiết hơn về cách thực thi 3 giai đoạn Compose cho các thành phần kết hợp.
Bản sáng tác
Trong giai đoạn thành phần, thời gian chạy Compose sẽ thực thi các hàm có khả năng kết hợp và xuất ra một cấu trúc dạng cây đại diện cho giao diện người dùng của bạn. Cây giao diện người dùng này bao gồm các nút bố cục chứa tất cả thông tin cần thiết cho các giai đoạn tiếp theo, như minh hoạ trong video sau:
Hình 2. Cây đại diện cho giao diện người dùng được tạo trong giai đoạn thành phần Compose.
Một phần của mã và cây giao diện người dùng sẽ có dạng như sau:

Trong những ví dụ này, mỗi hàm có khả năng kết hợp trong mã sẽ ánh xạ đến một nút bố cục duy nhất trong cây giao diện người dùng. Trong các ví dụ phức tạp hơn, các thành phần kết hợp có thể chứa logic và luồng điều khiển, đồng thời tạo ra một cây khác nhau cho các trạng thái khác nhau.
Bố cục
Trong giai đoạn bố cục, Compose sử dụng cây giao diện người dùng được tạo trong giai đoạn kết hợp làm dữ liệu đầu vào. Tập hợp các nút bố cục chứa tất cả thông tin cần thiết để quyết định kích thước và vị trí của từng nút trong không gian 2D.
Hình 4. Việc đo lường và đặt vị trí của từng nút bố cục trong cây giao diện người dùng trong giai đoạn bố cục.
Trong giai đoạn bố cục, cây được duyệt qua bằng cách sử dụng thuật toán 3 bước sau:
- Đo lường các phần tử con: Một nút sẽ đo lường các phần tử con (nếu có).
- Quyết định kích thước riêng: Dựa trên các phép đo này, một nút sẽ quyết định kích thước riêng của nó.
- Đặt các nút con: Mỗi nút con được đặt tương ứng với vị trí của nút đó.
Khi kết thúc giai đoạn này, mỗi nút bố cục sẽ có:
- Chiều rộng và chiều cao được chỉ định
- Toạ độ x, y nơi cần vẽ
Nhớ lại cây giao diện người dùng trong phần trước:
Đối với cây này, thuật toán hoạt động như sau:
Row
đo lường các thành phần con của nó,Image
vàColumn
.Image
được đo lường. Nút này không có nút con cháu nên sẽ quyết định kích thước của riêng mình và báo cáo kích thước đó choRow
.Column
sẽ được đo lường tiếp theo. Trước tiên, nó đo lường các thành phần kết hợp con của chính nó (2 thành phần kết hợpText
).Text
đầu tiên được đo. Nút này không có nút con cháu nên sẽ quyết định kích thước của riêng mình và báo cáo kích thước đó choColumn
.Text
thứ hai được đo. Nút này không có nút con cháu nên sẽ quyết định kích thước của riêng mình và báo cáo lại choColumn
.
Column
sử dụng các phép đo của nút con để quyết định kích thước của chính nó. Thành phần này sử dụng chiều rộng tối đa của thành phần con và tổng chiều cao của các thành phần con.Column
đặt các phần tử con tương ứng với chính nó, đặt các phần tử con bên dưới nhau theo chiều dọc.Row
sử dụng các phép đo của nút con để quyết định kích thước của chính nó. Thành phần này sử dụng chiều cao tối đa của thành phần con và tổng chiều rộng của các thành phần con. Sau đó, nó sẽ đặt các phần tử con của mình.
Xin lưu ý rằng mỗi nút chỉ được truy cập một lần. Thời gian chạy Compose chỉ yêu cầu một lần truyền qua cây giao diện người dùng để đo lường và đặt tất cả các nút, giúp cải thiện hiệu suất. Khi số lượng nút trong cây tăng lên, thời gian duyệt qua cây cũng tăng lên theo cách tuyến tính. Ngược lại, nếu mỗi nút được truy cập nhiều lần, thời gian duyệt qua sẽ tăng theo cấp số nhân.
Vẽ
Trong giai đoạn vẽ, cây sẽ được duyệt lại từ trên xuống dưới và mỗi nút sẽ lần lượt vẽ chính nó trên màn hình.
Hình 5. Giai đoạn vẽ sẽ vẽ các pixel trên màn hình.
Theo ví dụ trước, nội dung cây được vẽ theo cách sau:
Row
vẽ mọi nội dung mà nó có thể có, chẳng hạn như màu nền.Image
tự vẽ.Column
tự vẽ.Text
thứ nhất và thứ hai tự vẽ lần lượt.
Hình 6. Một cây giao diện người dùng và bản trình bày được vẽ của cây đó.
Đọc trạng thái
Khi bạn đọc value
của một snapshot state
trong một trong các giai đoạn được liệt kê trước đó, Compose sẽ tự động theo dõi trạng thái của hoạt động khi đọc value
. Tính năng theo dõi này cho phép Compose thực thi lại trình đọc khi value
của trạng thái thay đổi và là cơ sở để quan sát trạng thái trong Compose.
Bạn thường tạo trạng thái bằng cách sử dụng mutableStateOf()
, sau đó truy cập vào trạng thái đó theo một trong hai cách: truy cập trực tiếp vào thuộc tính value
hoặc sử dụng một đối tượng uỷ quyền thuộc tính Kotlin. Bạn có thể đọc thêm điều này trong phần Trạng thái trong các hàm có thể kết hợp. Theo mục đích của hướng dẫn này, lệnh "đọc trạng thái" ("state read") tham chiếu cho một trong các phương thức truy cập tương đương đó.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Trong chế độ đại diện thuộc tính, các hàm "getter" và "setter" được dùng để truy cập và cập nhật value
của Trạng thái. Các hàm getter và setter này chỉ được gọi khi bạn tham chiếu thuộc tính dưới dạng một giá trị, chứ không phải khi thuộc tính này được tạo. Đó là lý do tại sao 2 cách được mô tả trước đó lại tương đương nhau.
Mỗi khối mã có thể được thực thi lại khi trạng thái đọc thay đổi được gọi là phạm vi khởi động lại. Compose theo dõi các thay đổi về trạng thái value
và khởi động lại các phạm vi ở các giai đoạn khác nhau.
Đọc trạng thái theo giai đoạn
Như đã đề cập trước đó, có 3 giai đoạn chính trong Compose và Compose theo dõi trạng thái nào được đọc trong mỗi giai đoạn. Điều này cho phép Compose chỉ thông báo cho các giai đoạn cụ thể cần thực hiện công việc cho từng thành phần bị ảnh hưởng trong giao diện người dùng.
Các phần sau đây mô tả từng giai đoạn và mô tả những gì xảy ra khi một giá trị trạng thái được đọc trong giai đoạn đó.
Giai đoạn 1: Thành phần
Các trạng thái đọc trong hàm @Composable
hoặc khối lambda ảnh hưởng đến thành phần và có thể là các giai đoạn tiếp theo. Khi value
của trạng thái thay đổi, trình kết hợp lại sẽ lên lịch chạy lại tất cả hàm có khả năng kết hợp đã đọc value
của trạng thái đó. Lưu ý rằng thời gian chạy có thể bỏ qua một vài hoặc tất cả hàm có thể kết hợp nếu dữ liệu đầu vào không thay đổi. Hãy xem phần Bỏ qua nếu dữ liệu đầu vào không thay đổi để biết thêm thông tin.
Tuỳ thuộc vào kết quả của thành phần, giao diện người dùng Compose sẽ thực hiện giai đoạn bố cục và vẽ. Tính năng này có thể bỏ qua các giai đoạn trên nếu nội dung được giữ nguyên và kích thước cũng như bố cục không thay đổi.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Giai đoạn 2: Bố cục
Giai đoạn bố cục bao gồm hai bước: đo lường và đặt vị trí. Bước đo lường sẽ chạy lambda đo lường được chuyển đến thành phần kết hợp Layout
, phương thức MeasureScope.measure
của giao diện LayoutModifier
, v.v.
Bước đặt vị trí sẽ chạy khối vị trí của hàm layout
, khối lambda của Modifier.offset { … }
và các hàm tương tự.
Trạng thái đọc trong mỗi bước này sẽ ảnh hưởng đến bố cục và có thể cả giai đoạn vẽ. Khi value
của trạng thái thay đổi, giao diện người dùng Compose sẽ lên lịch cho giai đoạn bố cục. Nó cũng chạy giai đoạn vẽ nếu kích thước hoặc vị trí thay đổi.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Giai đoạn 3: Bản vẽ
Việc đọc trạng thái trong khi vẽ mã sẽ ảnh hưởng đến giai đoạn vẽ. Các ví dụ phổ biến
bao gồm Canvas()
, Modifier.drawBehind
và Modifier.drawWithContent
. Khi value
của trạng thái thay đổi, giao diện người dùng Compose chỉ chạy giai đoạn vẽ.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Tối ưu hoá việc đọc trạng thái
Vì Compose theo dõi việc đọc trạng thái được bản địa hoá, nên bạn có thể giảm thiểu công việc bằng cách đọc từng trạng thái trong từng giai đoạn phù hợp.
Hãy xem ví dụ sau đây. Ví dụ này có một Image()
sử dụng bộ sửa đổi chênh lệch để bù cho vị trí bố cục cuối cùng, tạo ra hiệu ứng thị sai là các đường cuộn của người dùng.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Mã này hoạt động nhưng mang lại hiệu suất không tối ưu. Như đã viết, mã sẽ đọc value
của trạng thái firstVisibleItemScrollOffset
và chuyển trạng thái đó đến hàm Modifier.offset(offset: Dp)
. Khi người dùng cuộn, value
của firstVisibleItemScrollOffset
sẽ thay đổi. Như bạn đã biết, Compose theo dõi mọi trạng thái đọc để có thể khởi động lại (gọi lại) mã đọc, như trong ví dụ này là nội dung của Box
.
Đây là ví dụ về việc đọc một trạng thái trong giai đoạn thành phần. Điều này không nhất thiết là một điều xấu và trên thực tế, nó lại là cơ sở của việc tái cấu trúc, cho phép các thay đổi dữ liệu tạo ra giao diện người dùng mới.
Lưu ý quan trọng: Ví dụ này không tối ưu vì mọi sự kiện cuộn đều dẫn đến việc toàn bộ nội dung của thành phần kết hợp bị đánh giá lại, đo lường lại, bố cục lại và cuối cùng là vẽ lại. Bạn kích hoạt giai đoạn Compose cho mỗi lần cuộn mặc dù nội dung hiển thị không thay đổi, mà chỉ thay đổi vị trí. Bạn có thể tối ưu hoá việc đọc trạng thái để chỉ kích hoạt lại giai đoạn bố cục.
Bù trừ bằng lambda
Hiện có một phiên bản khác của công cụ sửa đổi mức chênh lệch: Modifier.offset(offset: Density.() -> IntOffset)
.
Phiên bản này lấy thông số lambda, trong đó độ lệch kết quả được khối lambda trả về. Cập nhật mã để sử dụng thông số đó:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Vậy tại sao việc này có hiệu suất cao hơn? Khối lambda mà bạn cung cấp cho đối tượng sửa đổi được gọi trong giai đoạn bố cục (cụ thể là trong bước đặt vị trí của giai đoạn bố cục), có nghĩa là trạng thái firstVisibleItemScrollOffset
không còn được đọc trong thành phần. Vì Compose theo dõi thời điểm đọc trạng thái nên sự thay đổi này có nghĩa là nếu value
của firstVisibleItemScrollOffset
thay đổi, Compose chỉ cần khởi động lại giai đoạn bố cục và giai đoạn vẽ.
Tất nhiên, bạn thường phải đọc các trạng thái trong giai đoạn thành phần. Mặc dù vậy, có những trường hợp mà bạn có thể giảm thiểu số lần tái cấu trúc bằng cách lọc các thay đổi về trạng thái. Để biết thêm thông tin về vấn đề này, hãy xem phần derivedStateOf
: chuyển đổi một hoặc nhiều đối tượng trạng thái thành trạng thái khác.
Vòng lặp tái kết hợp (phần phụ thuộc giai đoạn tuần hoàn)
Hướng dẫn này trước đây đã đề cập rằng các giai đoạn của Compose luôn được gọi theo cùng một thứ tự và không có cách nào để quay ngược lại khi ở trong cùng một khung. Tuy nhiên, điều đó không ngăn các ứng dụng tham gia vào vòng lặp thành phần trên các khung khác nhau. Hãy xem xét ví dụ sau:
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Ví dụ này triển khai một cột dọc, với hình ảnh ở trên cùng và văn bản bên dưới. Thao tác này sử dụng Modifier.onSizeChanged()
để lấy kích thước đã xử lý của hình ảnh, sau đó sử dụng Modifier.padding()
trên văn bản để giảm kích thước đó.
Việc chuyển đổi không tự nhiên từ Px
trở về Dp
đã chỉ ra rằng mã đó có một vài vấn đề.
Vấn đề với ví dụ này là mã không đạt được bố cục "cuối cùng" trong một khung duy nhất. Mã này dựa trên nhiều khung, thực hiện các công việc không cần thiết và kết quả là giao diện người dùng nhảy xung quanh màn hình cho người dùng.
Thành phần khung hình đầu tiên
Trong giai đoạn kết hợp của khung hình đầu tiên, ban đầu imageHeightPx
là 0
. Do đó, mã này cung cấp văn bản bằng Modifier.padding(top = 0)
.
Giai đoạn bố cục tiếp theo sẽ gọi lệnh gọi lại của đối tượng sửa đổi onSizeChanged
, lệnh này sẽ cập nhật imageHeightPx
thành chiều cao thực của hình ảnh. Sau đó, Compose sẽ lên lịch kết hợp lại cho khung hình tiếp theo. Tuy nhiên, trong giai đoạn vẽ hiện tại, văn bản hiển thị với khoảng đệm là 0
, vì giá trị imageHeightPx
đã cập nhật chưa được phản ánh.
Thành phần khung hình thứ hai
Compose khởi tạo khung thứ hai, được kích hoạt bởi sự thay đổi về giá trị của imageHeightPx
. Trong giai đoạn thành phần của khung hình này, trạng thái được đọc trong khối nội dung Box
. Giờ đây, văn bản được cung cấp khoảng đệm phù hợp chính xác với chiều cao của hình ảnh. Trong giai đoạn bố cục, imageHeightPx
được đặt lại; tuy nhiên, không có hoạt động tái cấu trúc nào khác được lên lịch vì giá trị vẫn nhất quán.
Ví dụ này có vẻ hơi dàn dựng, nhưng bạn nên cẩn thận với mẫu hình chung này:
Modifier.onSizeChanged()
,onGloballyPositioned()
hoặc một số thao tác bố cục khác- Cập nhật một số trạng thái
- Sử dụng trạng thái đó làm đầu vào cho công cụ sửa đổi bố cục (
padding()
,height()
hoặc tương tự) - Có thể lặp lại
Cách khắc phục cho mẫu trước đó là sử dụng các nguyên hàm bố cục thích hợp. Ví dụ trước đó có thể được triển khai bằng một Column()
, nhưng bạn có thể có một ví dụ phức tạp hơn cần một số tuỳ chỉnh, do đó sẽ yêu cầu viết một bố cục tuỳ chỉnh. Xem hướng dẫn Bố cục tuỳ chỉnh để biết thêm thông tin
Nguyên tắc chung ở đây là có một nguồn chân lý duy nhất cho nhiều thành phần của giao diện người dùng mà những thành phần đó cần được đo lường và đặt vị trí liên quan đến nhau. Việc sử dụng một nguyên hàm bố cục hợp lý hoặc tạo một bố cục tuỳ chỉnh có nghĩa là thuộc tính mẹ tối thiểu mà bạn dùng để làm nguồn chân lý có thể điều phối mối quan hệ giữa nhiều thành phần. Việc áp dụng trạng thái động sẽ phá vỡ nguyên tắc này.
Đề 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
- Danh sách và lưới
- Kotlin cho Jetpack Compose