Xin chào tất cả mọi người, tôi ở đây để nói về Jetpack Compose, bộ công cụ giao diện
người dùng mới của chúng tôi, hoạt động tốt như thế nào với các thư viện Jetpack hiện có
mà bạn có thể đã sử dụng trong ứng dụng của mình.
Một trong những phần quan trọng nhất khi thiết kế Compose là bạn có thể dần dần áp dụng
nó với các ứng dụng hiện có của mình mà không cần viết lại mọi thứ từ đầu. Điều đó có nghĩa
là doanh nghiệp của bạn và các lớp dữ liệu bên dưới lớp Giao diện người dùng có thể được
sử dụng với Giao diện người dùng Compose mới sáng bóng của chúng tôi. Ví dụ, hãy lấy
ứng dụng này, Bloom.
Bloom là một ứng dụng mua sắm trong nhà và vườn cung cấp cho người dùng khả năng
tìm kiếm thông qua một bộ thực vật khổng lồ và xem qua các bộ sưu tập cây có liên quan
được tạo sẵn. Hãy đi sâu vào cách chúng ta có thể viết màn hình này trong Compose thay vì
sử dụng Views và XML. Tất cả điều đó với sức mạnh của Jetpack.
Trong trường hợp của Bloom, chúng tôi đã tận dụng rất nhiều thư viện Jetpack khác. Nếu
chúng ta xem xét kiến trúc của ứng dụng, Bloom sử dụng Room để lưu trữ các loại cây trong
cơ sở dữ liệu. Phân trang để tải chúng trong các phần hiệu quả của bộ nhớ, ViewModels để
xử lý logic giao diện người dùng, Kotlin coroutines Luồng để hiển thị trạng thái và dữ liệu
giữa mỗi lớp và Hilt là giải pháp tiêm phụ thuộc.
Bloom’s HomeViewModel phục vụ hai mục đích chính.
Đầu tiên, nó cung cấp sự tách biệt giữa trạng thái được hiển thị với giao diện người dùng
và cách trạng thái đó được tạo ra. Chính ViewModel chịu trách nhiệm xử lý logic nghiệp vụ
của Home UI.
Thứ hai, vì nó mở rộng lớp Jetpack ViewModel, nó vẫn tồn tại các thay đổi cấu hình, đảm
bảo dữ liệu mới nhất có sẵn ngay lập tức. Danh sách các loài thực vật được phân trang
được hiển thị từ lớp repo và phần còn lại của trạng thái giao diện người dùng cho màn hình
này bao gồm bộ sưu tập các loài thực vật và tín hiệu liên quan trong trường hợp trang đang
tải hoặc cần hiển thị lỗi.
Nhìn vào HomeScreen, chúng ta có thể chia nó thành một số phần tổng hợp riêng biệt cho
từng phần của giao diện người dùng của chúng tôi, mỗi phần chỉ lấy dữ liệu mà nó cần.
Bắt đầu triển khai HomeScreen, chúng ta có thể lấy một phiên bản của HomeViewModel ở
dạng có thể kết hợp bằng cách sử dụng phương thức ViewModel(). Phương pháp này
đang làm hai điều cho bạn.
Đầu tiên, nó tự động xác định phạm vi ViewModel đến ViewModelStoreOwner gần nhất.
Nếu chúng tôi đặt HomeScreen của mình trực tiếp vào một hoạt động, bạn có thể sử dụng
điều đó. Nếu nó nằm trong Fragment, nó sẽ sử dụng phạm vi Fragment để thay thế.
Thứ hai, nó đang sử dụng Factory mặc định. Đối với trường hợp Hoạt động có chú
thích AndroidEntryPoint hoặc Phân đoạn, Hilt đã được cài đặt chính nó làm nhà máy mặc
định, vì vậy ViewModel do Hilt hỗ trợ của chúng tôi hoạt động ngay lập tức.
Nếu bạn không sử dụng Hilt hoặc bất kỳ giải pháp chèn phụ thuộc nào khác, bạn sẽ cần
tạo ViewModel Factory bằng tay và chuyển nó vào hàm viewModel.
Trong Compose, bạn nên ưu tiên các bản tổng hợp không giữ trạng thái để có thể tái sử
dụng và kiểm tra tốt hơn. Chúng tôi gọi chúng là vật thể tổng hợp không trạng thái thay vì
những vật thể trạng thái giữ trạng thái bên trong.
Trong có thể kết hợp này, HomeScreen đang truy xuất một thể hiện của HomeViewModel
bên trong chính hàm, điều này làm cho nó trở thành một thể tổng hợp trạng thái. Để làm cho
nó không trạng thái, thay vào đó chúng ta nên cung cấp ViewModel dưới dạng một tham
số. Bằng cách này, chúng tôi ủy thác trách nhiệm lấy ViewModel cho cha mẹ để có thể tổng
hợp này tập trung vào việc tạo ra giao diện người dùng.
Khi chúng ta có một phiên bản của ViewModel, chúng ta có thể thu thập luồng uiState
bằng cách sử dụng hàm collectAsState. Thao tác này sẽ thực thi lại tệp tổng hợp nơi trạng
thái được đọc và sẽ cập nhật giao diện người dùng tương ứng với các giá trị mới.
Nếu bạn sử dụng LiveData hoặc RxJava, đừng lo lắng. Chúng tôi cũng hỗ trợ bạn sử dụng
các chức năng ObserverAsState và subscribeAsState.
Chúng tôi đã nói rằng chúng tôi có rất nhiều loại cây trong Bloom nên việc sử dụng Paging
để tải chúng theo từng phần rất có ý nghĩa. Paging Compose giúp bạn có thể chuyển đổi
Flow of PagingData của chúng ta thành một trạng thái mà chúng ta có thể sử dụng
trong Compose bằng phương thức collectAsLazyPagingItems(). Với điều đó, chúng ta có thể
dễ dàng hiển thị chúng bằng LazyColumn.
Ngoài ra, bằng cách nhìn vào loadState, chúng ta có thể dễ dàng thêm khả năng kết hợp tải
của riêng mình để cho biết khi nào dữ liệu được làm mới và ngay cả khi chúng ta xuống
cuối màn hình trong khi nhiều dữ liệu hơn đang được thêm vào danh sách của chúng ta.
Và bây giờ, trên HomeScreen, chúng ta có thể gọi PlantList với dữ liệu đến từ ViewModel.
Trước khi tiếp tục, hãy tạm dừng để xác nhận rằng chúng tôi đã sử dụng rất nhiều thư
viện Jetpack trong Compose UI của chúng tôi mà không sửa đổi ViewModel hoặc thay đổi
bất kỳ lớp nào khác trong ứng dụng của chúng tôi. Thật tuyệt, phải không?
Nhìn vào thiết kế ứng dụng của chúng tôi, có một phần giao diện người dùng mà chúng tôi
có thể muốn sử dụng lại trong các màn hình khác và đó là Browse CollectionsCarousel có
thể được sử dụng lại cho các đề xuất trên PlantDetailScreen của chúng tôi, như bạn có thể
thấy ở đây. Điều thú vị về băng chuyền này là khi bạn chạm vào một mục, nó sẽ mở rộng
để hiển thị các loại cây có liên quan đến bộ sưu tập đó. Trong quá trình triển khai, băng
chuyền cần logic và trạng thái bên trong để tự mở rộng hoặc thu gọn, cũng như xử lý logic
để chỉ cho phép một bộ sưu tập được mở rộng và chuyển đến PlantDetailScreen khi
người dùng chạm vào cây. Chúng ta đặt logic đó ở đâu? Trong ViewModel của chúng tôi?
Điều đó là không thể đối với các thành phần giao diện người dùng có thể tái sử dụng. Hãy
đi sâu vào vấn đề này.
Để làm cho CollectionsCarousel có thể tái sử dụng, chúng tôi làm theo các phương pháp
hay nhất về Compose bằng cách chuyển trạng thái vào và hiển thị các sự kiện. Đóng gói
logic và kiểm soát trạng thái của tệp có thể tổng hợp trong một lớp là một cách tốt để bảo vệ
tệp có thể kết hợp hiển thị giao diện người dùng không nhất quán và tránh lặp lại cùng một
mã ở những nơi khác nhau.
Đối với trạng thái của CollectionsCarousel, chúng ta có thể tạo một lớp thông thường được
gọi là CollectionsCarouselState lấy các bộ sưu tập làm tham số.
Nó hiển thị thông tin về việc liệu băng chuyền có được mở rộng bộ sưu tập đã chọn hay
không và các loại cây sẽ hiển thị.
Và nó đóng gói logic để cập nhật trạng thái khi một tập hợp được nhấp vào trong một
phương thức điểm nhập duy nhất, onCollectionClick. Trạng thái này hiện được cung cấp cho
tệp tổng hợp dưới dạng tham số mà người gọi CollectionsCarousel cần tạo và quản lý. Đối
với các sự kiện, CollectionsCarousel nhận thêm một tham số, một lambda được gọi bất cứ
khi nào người dùng chạm vào cây.
Nhưng bạn có thể tự hỏi tại sao chúng tôi có CollectionsCarouselState như một lớp
thông thường và các sự kiện để chỉ ra các tương tác của người dùng thay vì có tất cả
những điều đó trong một ViewModel?
Theo quy tắc chung, bạn sẽ tạo một ViewModel để lưu trữ trạng thái, xử lý logic và xử lý các
sự kiện khi có thể tổng hợp gần với cấp gốc của màn hình, ví dụ: có thể tổng hợp
HomeScreen hoặc Màn hình đăng nhập. Trong trường hợp đó, chẳng hạn như đối với các
vật liệu tổng hợp có thể tái sử dụng như Carousel, đừng sử dụng ViewModel. Thay vào đó,
hãy tạo một lớp chủ sở hữu trạng thái thông thường để quản lý trạng thái và hiển thị các sự
kiện cho hàm gọi. Điều này là do ViewModels được xác định phạm vi đến Hoạt động,
Phân đoạn hoặc đích của biểu đồ điều hướng. Do đó, bạn chỉ có thể nhận được một phiên
bản của kiểu ViewModel trong phạm vi đó.
Các lợi ích đối với ViewModels ở cấp độ sàng lọc các vật thể tổng hợp là chúng có thể tồn
tại trước các thay đổi cấu hình, dữ liệu có thể tồn tại trong quá trình chết thông
qua SavedStateHandle, đó là một nơi tốt hơn cho trạng thái có vòng đời khớp với màn hình
và chúng có phạm vi quy trình điều tra tích hợp sẵn mà bạn có thể sử dụng để điều chỉnh
logic kinh doanh của bạn.
Quay lại mã, vì trạng thái của HomeScreen được HomeViewModel quản lý, nên
carouselState phải là một phần của trạng thái giao diện người dùng mà ViewModel hiển thị.
Sau đó, nó được chuyển cho CollectionsCarousel. Nhưng những gì về sự kiện điều
hướng? Chúng ta nên làm gì với điều đó? Và bản thân HomeScreen thì sao? Chúng ta sẽ
có thể áp dụng cùng một khả năng kiểm tra và khả năng xem trước cho toàn màn hình,
phải không?
Đó là hoàn toàn đúng. Chúng tôi muốn có thể viết các bài kiểm tra trên HomeScreen của
mình và chuyển dữ liệu giả mạo sang bản xem trước mà không cần lo lắng về trạng thái
thu thập và mô hình View của chúng tôi.
Thật dễ dàng để thực hiện những thay đổi đó đối với HomeScreen của chúng tôi, chỉ cần
thêm trạng thái giao diện người dùng, luồng của các cây được phân trang và
lambda onPlantClick làm thông số.
Mặc dù điều này có vẻ không phải là một vấn đề lớn, nhưng chúng tôi vừa đẩy lệnh gọi
đến ViewModel() và collectAsState() lên một cấp độ, nó tạo ra sự khác biệt lớn khi nghĩ về vị
trí của màn hình chính này so với mọi thứ khác trong ứng dụng của bạn.
Nếu Bloom là một ứng dụng chỉ có một màn hình này, chúng tôi có thể sử dụng chức
năng setContent để đặt Composable của chúng tôi trực tiếp trong hoạt động. Nhưng các
bản mô phỏng của chúng tôi cho thấy một ứng dụng phức tạp hơn nhiều, vì vậy việc thêm
trực tiếp có thể tổng hợp của chúng tôi vào hoạt động có thể không phải là cách tiếp cận
phù hợp.
Một con đường di chuyển rất tự nhiên cho một ứng dụng hiện có khi bắt đầu áp dụng tính
năng Compose sẽ là di chuyển từng màn hình một. Ví dụ: nếu bạn đang sử dụng Fragment,
thì HomeScreen này thực sự có thể là nội dung duy nhất trong HomeFragment của bạn,
tạo ComposeView trực tiếp trong onCreateView (). Trong kiểu tiếp cận này, bạn sẽ sử dụng
các API như FragmentScenario để kiểm tra toàn bộ phân đoạn của mình.
Sau khi mọi màn hình trong ứng dụng của bạn chỉ là một lớp bao bọc xung quanh
một Composable có thể sử dụng lại, có thể thử nghiệm, bước tiếp theo sẽ là liên kết tất cả
các màn hình đó lại với nhau bằng tính năng Navigation Compose. Navigation
Compose Jetpack được xây dựng ngay từ đầu như một thời gian chạy chung không biết gì
về bất kỳ loại điểm đến nào. Sau đó, phần trừu tượng này được sử dụng để xây dựng cấu
phần Mảnh ghép điều hướng và gần đây hơn là phần tạo tác phần Navigation Compose.
Thời gian chạy được chia sẻ này có nghĩa là mỗi triển khai này đều được hỗ trợ sẵn sàng
để xây dựng biểu đồ điều hướng của màn hình hoặc điểm đến trong ứng dụng của bạn
thông qua Kotlin DSL, tự động xác định phạm vi vòng đời, ViewModels và Saved State
từng điểm đến, liên kết sâu, trả về kết quả và tích hợp với nút quay lại của hệ thống.
Trong Navigation Compose, có hai phần chính bạn cần: một NavController và
NavHost. NavController tuân theo cùng một mẫu mà chúng tôi đã sử dụng cho trạng
thái CollectionsCarousel, bằng cách cho phép bạn tạo NavController trạng thái như một
đối tượng riêng biệt từ NavHost lấy NavController làm đầu vào.
Điều này cho phép các thành phần khác bên ngoài NavHost phản ứng với việc thay đổi
đích hiện tại bằng cách sử dụng NavController làm nguồn chân lý duy nhất cho trạng thái.
Ví dụ: Điều này hữu ích khi đặt mục đã chọn trong thanh điều hướng dưới cùng.
NavHost Composable chịu trách nhiệm thêm điểm đến Composable của bạn vào hệ thống
phân cấp Composable. Đây là nơi bạn xác định biểu đồ điều hướng của mình thông qua
Kotlin DSL. Đây là bản đồ của tất cả các điểm đến có thể có trong ứng dụng của bạn.
Vì vậy, đối với HomeScreen của chúng tôi, chúng tôi có thể tạo một điểm đến có thể tổng
hợp tại nhà. Bạn sẽ lưu ý rằng chúng tôi khai báo một tuyến đường trên HomeScreen
của chúng tôi. Đây là con đường duy nhất mà bạn sử dụng để điều hướng đến điểm đến đó.
Vì đây là màn hình chính của ứng dụng, chúng tôi đặt nó làm điểm đến bắt đầu của biểu đồ.
Tuy nhiên, chúng tôi cần thực hiện một thay đổi đối với mã của mình. Thay vì sử
dụng ViewModel(), chúng tôi đang sử dụng phương thức hiltNavGraphViewModel() của
Hilt Navigation Compose. Giống như tôi đã đề cập, Điều hướng tự động tìm các
ViewModels của bạn đến đích riêng lẻ, nhưng vì đích đến không phải là Hoạt động hoặc
Phân đoạn @AndroidEntryPoint, nó không có nhà máy của Hilt làm nhà máy mặc định.
Phương thức hiltNavGraphViewModel() là một trình bao bọc thuận tiện xung quanh
viewModel() đảm bảo rằng nhà máy phù hợp luôn được đặt cho bạn. Lưu ý rằng chúng tôi
hoàn toàn không cần thay đổi HomeScreen của mình. Nó hoàn toàn không biết rằng nó
đang được sử dụng trong Navigation Compose và vẫn có thể được kiểm tra một cách độc
lập. Tất nhiên, lợi ích lớn của tính năng Navigation Compose đến khi bạn có nhiều màn hình.
Đó là khi khả năng phân chia trạng thái đã lưu và ViewModels tới đích riêng lẻ đảm bảo
trạng thái chỉ được tạo khi cần thiết và chỉ bị xóa khi bạn bật đích ra khỏi ngăn xếp phía sau.
Trong trường hợp của Bloom, những hình ảnh nhỏ trên PlantList hoặc băng chuyền mở rộng
chỉ nên là bản xem trước cho màn hình chi tiết thực sự cung cấp cho người dùng ý tưởng
về những gì họ có thể nhận được cho khu vườn của mình.
Bước đầu tiên là xây dựng PlantDetailsScreen. Cũng giống như HomeScreen của chúng
ta, chúng ta nên xây dựng PlantDetailsScreen của mình dưới dạng một thành phần không
trạng thái để chúng ta có thể sử dụng các bản xem trước và kiểm tra nó một cách riêng
biệt ngay từ đầu. Bạn sẽ lưu ý rằng CollectionsCarousel của chúng tôi xuất hiện một cách
khác. Việc xây dựng các thành phần có thể tái sử dụng không chỉ có nghĩa là mang lại
trải nghiệm nhất quán hơn cho người dùng trên ứng dụng của bạn mà còn giúp ích khi tạo
ra các màn hình mới.
Tiếp theo, chúng ta cần thêm PlantDetailsScreen vào biểu đồ điều hướng của mình. Đây chỉ
là một điểm đến có thể kết hợp khác, nhưng có một điểm khác biệt ở đây so với
HomeScreen của chúng tôi, chúng tôi cần biết người dùng đã chọn loại cây nào. Các
tuyến đường trong Navigation có rất nhiều điểm chung với các URL web ở chỗ chúng được
thiết kế để phục vụ như địa chỉ của nội dung chúng tôi hiển thị. Điều này có nghĩa là
tuyến đường của chúng tôi không chỉ là ‘plant/’, mà là ‘plant/’ với ID của thực vật mà chúng
tôi muốn hiển thị bằng cách sử dụng cú pháp giữ chỗ này. Đối với lớp Plant của chúng tôi, ID
là một số nguyên, vì vậy chúng tôi khai báo nó với IntType.
Bây giờ, chúng tôi có thể trích xuất ID trực tiếp từ mục nhập ngăn xếp phía sau mà
NavHost chuyển đến đích chi tiết thực vật của chúng tôi, nhưng chúng tôi thực sự muốn ID
đó trong ViewModel thực vật của chúng tôi, không phải ở đây.
Để thực hiện điều này, chúng tôi có thể sử dụng một API khác là một phần của
Jetpack ViewModel, SavedStateHandle. SavedStateHandle thực sự chỉ là một bản đồ giá
trị khóa lạ mắt, nhưng điều quan trọng là nó được tự động điền bởi các đối số mà chúng tôi
đặt trong lộ trình của mình.
Điều này cho phép ViewModel của chúng tôi chuyển trực tiếp ID đó đến kho lưu trữ của
chúng tôi, để truy xuất thông tin chi tiết về nhà máy và cung cấp Luồng có thể quan sát đó
cho giao diện người dùng. Điều này có nghĩa là nếu tải nền cập nhật giá của nhà máy hoặc
thay đổi nguồn gốc duy nhất của sự thật, PlantDetailScreen của chúng tôi sẽ tự động biên
soạn lại với dữ liệu mới.
Nhờ công việc của Manuel trong việc làm cho HomeScreen không ở trạng thái, chúng tôi
đã phát hiện ra một lambda được kích hoạt khi người dùng chạm vào một cái cây. Sau đó,
đích đến HomeScreen của chúng tôi thực hiện lambda đó, gọi navigate() với tuyến đường
của PlantDetailScreen của chúng tôi, điền vào ID. NavController sẽ xử lý phần còn lại của
nó, lưu trạng thái của HomeScreen và hoán đổi trong PlantDetailScreen. Nhấn vào nút quay
lại hệ thống sẽ tự động đưa bạn trở lại HomeScreen ở trạng thái chính xác mà bạn đã
để nguyên.
Chúng tôi chỉ đang sơ lược về những gì bạn có thể làm với Jetpack Compose và tất cả
các điểm tích hợp với các thư viện Jetpack khác.
Nếu bạn muốn tìm hiểu thêm, tôi thực sự khuyên bạn nên đọc qua hướng dẫn của chúng tôi
về khả năng tương tác đã soạn và hướng dẫn cụ thể về Điều hướng bằng Compose trong
tài liệu của chúng tôi. Để biết các ví dụ thực tế, hãy xem tất cả các mẫu Compose của chúng
tôi và ví dụ thực tế về việc chuyển đổi một ứng dụng hiện có thành Compose trong ứng
dụng mẫu, Tivi. Tất cả các liên kết đều có trong phần mô tả.
Cho dù bạn đang thực hiện một dự án hoàn toàn mới trong phần Compose hay điều chỉnh
một dự án hiện có, Jetpack luôn sẵn sàng trợ giúp. Cảm ơn.
Nhận xét
Đăng nhận xét