1. Trang chủ
  2. » Luận Văn - Báo Cáo

Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng

110 33 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Đồ án tìm hiểu Jetpack Compose và xây dựng ứng dụng
Tác giả Bùi Lê Hoài An
Người hướng dẫn ThS. Nguyễn Công Hoan
Trường học Trường Đại học Công nghệ Thông tin – ĐHQG TP.HCM
Chuyên ngành Công nghệ phần mềm
Thể loại Đồ án
Năm xuất bản 2023
Thành phố TP.HCM
Định dạng
Số trang 110
Dung lượng 4,6 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Cấu trúc

  • CHƯƠNG 1: GIỚI THIỆU TỔNG QUAN (8)
    • 1. Thông tin sinh viên (8)
    • 2. Tổng quan đề tài (8)
      • 2.1 Giới thiệu đề tài (8)
      • 2.2 Phạm vi nghiên cứu (8)
      • 2.3 Nội dung nghiên cứu (8)
      • 2.4 Kết quả hướng tới (9)
    • 3. Công cụ sử dụng (9)
  • CHƯƠNG 2: JETPACK COMPOSE (10)
    • 1. Giới thiệu ngôn ngữ lập trình Kotlin (10)
    • 2. Jetpack Compose (10)
    • 3. Tại sao lại là Jetpack Compose? (11)
    • 4. Mô hình tư duy trong Jetpack Compose (11)
      • 4.1. Mô hình lập trình khai báo (12)
      • 4.2. Hàm composable đơn giản (12)
      • 4.3. Thay đổi mô hình khai báo (14)
      • 4.4. Nội dung động (16)
      • 4.5. Recomposition (17)
        • 4.5.1. Các hàm composable có thể thực thi theo thứ tự bất kỳ (18)
        • 4.5.2. Các hàm composable có thể chạy song song (0)
        • 4.5.3. Bỏ qua recomposition nhiều nhất có thể (20)
        • 4.5.4. Recomposition là khả quan (0)
        • 4.5.5. Các hàm có khả năng recomposition có thể chạy khá thường xuyên (0)
    • 5. Bố cục Compose cơ bản (22)
      • 5.1. Mục tiêu (22)
      • 5.2. Các hàm Composable (22)
      • 5.3. Các bố cục cơ bản (23)
      • 5.4. Mô hình bố cục (26)
      • 5.5. Hiệu suất (28)
      • 5.6. Sử dụng modifiers (28)
    • 6. Quản lý trạng thái (29)
      • 6.1. Trạng thái và composition (30)
      • 6.2. Trạng thái trong composition (31)
      • 6.3. Chuyển trạng thái lên trên (32)
      • 6.4. Khôi phục trạng thái (35)
        • 6.4.1. Parcelize (35)
        • 6.4.2. MapSaver (36)
      • 6.5. Phần tử giữ trạng thái (36)
      • 6.6. Kích hoạt lại tính năng ghi nhớ các tính toán khi khoá thay đổi (36)
    • 7. Vòng đời của Composable (38)
      • 7.1. Tổng quan (38)
      • 7.2. Phân tích một Composable trong Composition (40)
        • 7.2.1. Thêm thông tin hỗ trợ quá trình recomposition (41)
      • 7.3. Bỏ qua nếu giá trị đầu vào không thay đổi (45)
    • 8. Các giai đoạn trong Jetpack Compose (46)
      • 8.2. Đọc trạng thái (48)
      • 8.3. Đọc trạng thái theo giai đoạn (49)
        • 8.3.1. Giai đoạn 1: Composition (49)
        • 8.3.2. Giai đoạn 2: Layout (49)
        • 8.3.3. Giai đoạn 3: Drawing (50)
      • 8.4. Tối ưu hóa việc đọc trạng thái (51)
      • 8.5. Vòng lặp tái kết hợp (phần phụ thuộc giai đoạn tuần hoàn) (53)
    • 9. Ktor (56)
      • 9.1. Tổng quan (56)
      • 9.2. Đặc điểm nổi bật (56)
      • 9.3. Vài thành phần chính của Ktor (57)
  • CHƯƠNG 3: XÂY DỰNG ỨNG DỤNG (59)
    • 1. Tổng quan (59)
      • 1.1. Tên ứng dụng: Yum - Ứng dụng hỗ trợ đầu bếp (59)
      • 1.2. Lý do chọn ứng dụng (59)
      • 1.3. Đối tượng hướng đến (59)
      • 1.4. Môi trường phát triển ứng dụng (59)
      • 1.5. Kết quả mong đợi (60)
      • 1.6. Quy trình thực hiện các công việc chính (60)
    • 2. Phân tích, thiết kế hệ thống (60)
      • 2.1. Xác định và mô hình hóa các yêu cầu phần mềm (60)
        • 2.1.1. Xác định yêu cầu (60)
          • 2.1.1.1. Một số yêu cầu phần mềm phải có (61)
          • 2.1.1.2. Ràng buộc logic ban đầu (61)
          • 2.1.1.3. Tính khả dụng (61)
          • 2.1.1.4. Tính ổn định (61)
          • 2.1.1.5. Hiệu suất (61)
          • 2.1.1.6. Bảo mật (62)
        • 2.1.2. Mô hình hóa yêu cầu (62)
          • 2.1.2.1. Danh sách chức năng (62)
          • 2.1.2.2. Lược đồ Use-case (62)
      • 2.2. Thiết kế hệ thống (79)
        • 2.2.1. Kiến trúc hệ thống (79)
      • 2.3. Thiết kế dữ liệu (81)
      • 2.4. Thiết kế giao diện (86)
        • 2.4.1. Sơ đồ liên kết màn hình (86)
        • 2.4.2. Danh sách các màn hình (86)
    • 3. Cài đặt và thử nghiệm (107)
  • CHƯƠNG 4: KẾT LUẬN (109)
    • 1. Kết quả đạt được (109)
    • 2. Hướng phát triển (109)

Nội dung

GIỚI THIỆU TỔNG QUAN

Thông tin sinh viên

20520985 Bùi Lê Hoài An 20520985@gm.uit.edu.vn

Tổng quan đề tài

Các framework cross-platform như Flutter và React-Native đã mang lại nhiều lợi ích, đặc biệt trong việc tiết kiệm thời gian phát triển Tuy nhiên, nhiều công ty lớn vẫn ưu tiên sử dụng ứng dụng native do vấn đề hiệu suất Hiệu suất không chỉ là yếu tố quan trọng mà còn ảnh hưởng trực tiếp đến trải nghiệm người dùng.

Vì thế phát triển ứng dụng

Phát triển ứng dụng native gặp nhiều khó khăn, đặc biệt là trên nền tảng Android, khi các nhà phát triển phải sử dụng ngôn ngữ XML bên cạnh Java hoặc Kotlin Việc phải học và làm việc với hai ngôn ngữ khác nhau không chỉ tốn thời gian mà còn gia tăng chi phí cho các công ty và lập trình viên.

Kể từ Google I/O 2019, Kotlin đã trở thành ngôn ngữ lập trình phổ biến cho phát triển ứng dụng Android native, dần thay thế Java Sự ra đời của Jetpack Compose, sử dụng Kotlin, đã mang đến một bước tiến mới với mục tiêu quan trọng: mã nguồn chỉ cần chứa duy nhất một ngôn ngữ lập trình.

2.2 Phạm vi nghiên cứu: Đồ án tập trung vào nghiên cứu tổng quan cách thức hoạt động và cách sử dụng các thư viện - package cũng như cách lưu trữ dữ liệu của Jetpack Compose Đồng thời, em áp dụng các nghiên cứu trên vào một ứng dụng thực tế để giúp em có thể hiểu sâu hơn các kiến thức đã tìm hiểu

Em sẽ tiến hành nghiên cứu chi tiết về cách thức hoạt động, ưu - khuyết điểm cũng như các thư viện - tính năng - package liên quan đến Compose

2.4 Kết quả hướng tới:

Với đề tài này, em đề ra hai mục tiêu chính:

Mở rộng kiến thức về Compose thông qua việc tìm hiểu và áp dụng vào ứng dụng thực tế giúp cá nhân phát triển thêm các ứng dụng khác bằng Compose Jetpack Quá trình nghiên cứu và sử dụng framework mới không chỉ nâng cao kỹ năng mà còn tạo nền tảng vững chắc để dễ dàng tiếp xúc với các công nghệ mới trong tương lai.

Đối với lập trình viên, tài liệu nghiên cứu này là nguồn tham khảo quý giá, giúp họ dễ dàng xác định các kiến thức cần thiết khi làm việc với Jetpack Compose Bài viết cung cấp các khái niệm cơ bản và giới thiệu những thư viện phổ biến được cộng đồng khuyến nghị, từ đó hỗ trợ lập trình viên trong việc tìm hiểu và ứng dụng Jetpack Compose một cách hiệu quả.

Công cụ sử dụng

Trong quá trình xây dựng phần mềm, em đã sử dụng các phần mềm sau:

• Android Studio: phát triển front end

• Intellij IDEA: phát triển back end

• GitHub: quản lý source code

JETPACK COMPOSE

Giới thiệu ngôn ngữ lập trình Kotlin

Kotlin là một ngôn ngữ lập trình đa năng, được tối ưu hóa để hoạt động trên nền tảng Java Virtual Machine (JVM) và có khả năng chạy trên Android.

JavaScript và Native Kotlin được phát triển bởi JetBrains, cùng với một số cộng đồng lập trình viên đóng góp

Một số đặc điểm của Kotlin bao gồm:

Kotlin là ngôn ngữ lập trình hỗ trợ cả lập trình hướng đối tượng (OOP) và lập trình hàm (FP), mang lại sự linh hoạt tối ưu cho quá trình phát triển ứng dụng.

• Kotlin có cú pháp đơn giản, dễ đọc và dễ viết hơn so với Java và nhiều ngôn ngữ lập trình khác

• Kotlin hỗ trợ null safety, giúp tránh được các lỗi runtime liên quan đến null pointer

• Kotlin hỗ trợ extension function, giúp mở rộng tính năng của một lớp đối tượng mà không cần thay đổi mã nguồn gốc

Kotlin tương thích hoàn hảo với mã nguồn Java, giúp lập trình viên dễ dàng chuyển đổi giữa hai ngôn ngữ hoặc kết hợp cả hai trong cùng một dự án.

Kotlin đã được Google công nhận là ngôn ngữ chính thức cho phát triển ứng dụng Android, trở thành lựa chọn phổ biến trong cộng đồng lập trình viên Android Bên cạnh đó, Kotlin còn được áp dụng trong nhiều lĩnh vực khác như phát triển máy chủ, ứng dụng web và ứng dụng cho thiết bị nhúng.

Jetpack Compose

Jetpack Compose là một thư viện UI mạnh mẽ do Google phát triển, cho phép lập trình viên xây dựng giao diện người dùng Android bằng ngôn ngữ lập trình Kotlin Thay vì sử dụng XML như trước đây, Jetpack Compose cung cấp cách tiếp cận linh hoạt và tùy chỉnh hơn, giúp tạo ra các giao diện người dùng một cách dễ dàng và hiệu quả.

Jetpack Compose cho phép lập trình viên dễ dàng tạo giao diện người dùng động và tương tác, với các tính năng nổi bật như tạo theme, animation, và layout linh hoạt Được xây dựng trên nền tảng công nghệ hiện đại như Kotlin, coroutines, và Android Jetpack, Jetpack Compose mang đến trải nghiệm phát triển ứng dụng hiệu quả và sáng tạo.

Tại sao lại là Jetpack Compose?

Khai báo giao diện người dùng trong Jetpack Compose cho phép bạn xác định những gì muốn hiển thị mà không cần chỉ rõ cách thực hiện, giúp mã dễ đọc, dễ hiểu và dễ bảo trì hơn so với phương pháp truyền thống sử dụng XML.

Jetpack Compose dễ dàng tích hợp với các thành phần và công nghệ hiện có của Android, cho phép người dùng khai thác những tính năng mạnh mẽ như ViewModel, LiveData và Room để quản lý trạng thái ứng dụng và tương tác hiệu quả với cơ sở dữ liệu.

Jetpack Compose mang lại hiệu suất cao hơn so với các phương pháp truyền thống nhờ vào cơ chế "Recompose", cho phép chỉ cập nhật các phần tử giao diện người dùng đã thay đổi Điều này không chỉ giảm tải cho CPU mà còn cải thiện hiệu suất tổng thể của ứng dụng.

Jetpack Compose được thiết kế để tương thích với cả các phiên bản Android cũ và mới, cho phép bạn dễ dàng tích hợp vào các dự án hiện có hoặc kết hợp với mã Java/Kotlin truyền thống Hơn nữa, Jetpack Compose khuyến khích việc tái sử dụng mã và các thành phần giao diện người dùng, giúp tiết kiệm thời gian và công sức trong quá trình phát triển ứng dụng.

Jetpack Compose đã thu hút sự hỗ trợ mạnh mẽ từ cộng đồng phát triển Android, với nhiều tài liệu, ví dụ mã và nguồn tài nguyên hữu ích được chia sẻ trên trang web của Google và từ các nhà phát triển khác Người dùng có thể tìm kiếm sự hỗ trợ qua các diễn đàn, nhóm Facebook và nhóm Telegram, tạo nên một mạng lưới hỗ trợ phong phú cho các lập trình viên.

Jetpack Compose là một framework mạnh mẽ cho phát triển giao diện người dùng Android, giúp tăng năng suất và cải thiện hiệu suất ứng dụng Nó cho phép tạo ra giao diện linh hoạt và tương tác, mang đến trải nghiệm tốt hơn cho người dùng.

Mô hình tư duy trong Jetpack Compose

Jetpack Compose là một công cụ hiện đại giúp phát triển giao diện người dùng cho Android Với API khai báo, Compose cho phép bạn dễ dàng tạo và duy trì giao diện người dùng cho ứng dụng của mình.

Trang 12 của ứng dụng mà không cần ra lệnh thay đổi các chế độ xem giao diện người dùng Thuật ngữ này cần được giải thích đôi chút nhưng có ý nghĩa rất quan trọng đối với cách thiết kế ứng dụng của bạn

4.1 Mô hình lập trình khai báo

Hệ phân cấp giao diện người dùng trên Android trước đây được biểu thị dưới dạng cây, và khi trạng thái ứng dụng thay đổi do tương tác của người dùng, cần cập nhật hệ thống này để hiển thị dữ liệu hiện tại Phương pháp phổ biến để thực hiện điều này là sử dụng các hàm như findViewById() để tìm kiếm các thành phần giao diện và thay đổi chúng bằng các phương thức như button.setText(String), container.addChild(View) hoặc img.setImageBitmap(Bitmap), nhằm thay đổi trạng thái nội bộ của tiện ích.

Việc điều chỉnh chế độ xem một cách thủ công có thể dẫn đến lỗi, đặc biệt khi một phần dữ liệu xuất hiện ở nhiều vị trí, khiến bạn có thể quên cập nhật một trong số đó Điều này dễ dàng tạo ra các trạng thái không hợp lệ khi các lượt cập nhật xung đột theo cách không mong đợi, chẳng hạn như khi một lượt cập nhật cố gắng thiết lập giá trị của một nút đã bị xóa Nhìn chung, độ phức tạp trong việc bảo trì phần mềm gia tăng cùng với số lượng chế độ xem cần được cập nhật.

Trong những năm gần đây, ngành công nghiệp đã chuyển sang mô hình giao diện người dùng khai báo, đơn giản hóa quy trình xây dựng và cập nhật giao diện Kỹ thuật này tạo lại toàn bộ màn hình từ đầu và chỉ áp dụng những thay đổi cần thiết, giúp cập nhật hệ phân cấp chế độ xem một cách thủ công dễ dàng hơn Compose là một ví dụ điển hình của khung giao diện người dùng khai báo.

Việc tạo lại toàn bộ màn hình có thể tốn thời gian, tài nguyên tính toán và pin, do đó, Compose sẽ lựa chọn thông minh các phần cần vẽ lại trên giao diện người dùng Điều này ảnh hưởng đến cách thiết kế các thành phần trên giao diện, như đã đề cập trong bài viết về tính năng kết hợp lại.

Sử dụng Compose, bạn có thể tạo giao diện người dùng bằng cách định nghĩa các hàm kết hợp để lấy dữ liệu và cung cấp các thành phần cho giao diện Chẳng hạn, hàm Greeting nhận vào một chuỗi và hiển thị tin nhắn chào mừng dưới dạng Text.

Hàm này có khả năng kết hợp đơn giản, cho phép chuyển dữ liệu và sử dụng dữ liệu đó để hiển thị một tiện ích văn bản trên màn hình.

Vài điều đáng chú ý về hàm này:

Hàm được đánh dấu bằng chú thích @Composable là yếu tố thiết yếu trong việc phát triển giao diện người dùng với Compose Tất cả các hàm có khả năng kết hợp đều cần có chú thích này, giúp trình biên dịch Compose nhận biết rằng hàm có nhiệm vụ chuyển đổi dữ liệu thành giao diện người dùng.

Hàm này lấy dữ liệu và cho phép kết hợp các thông số để mô tả logic của giao diện người dùng Tiện ích của chúng tôi chấp nhận chuỗi (String) để chào người dùng theo tên.

Hàm này hiển thị văn bản trên giao diện người dùng bằng cách gọi hàm Text(), tạo ra thành phần văn bản Nó cung cấp sự phân cấp cho giao diện người dùng thông qua việc kết hợp với các hàm khác.

Hàm này không trả về giá trị nào và các hàm Compose cung cấp giao diện người dùng mà không cần trả lại thông tin, vì chúng chỉ mô tả trạng thái màn hình mong muốn thay vì tạo ra các tiện ích giao diện người dùng.

• Hàm này nhanh chóng, không thay đổi giá trị và không có tác dụng phụ

Hàm này hoạt động tương tự như khi được gọi nhiều lần với cùng một đối số, mà không sử dụng các giá trị khác như biến toàn cục hay lệnh gọi đến random().

Hàm này cung cấp một giao diện người dùng rõ ràng mà không gây ra bất kỳ tác dụng phụ nào, bao gồm việc không sửa đổi các thuộc tính hoặc biến toàn cục.

Tổng quan, bạn cần viết tất cả các hàm có khả năng kết hợp dựa trên các thuộc tính này, do những lý do đã được đề cập trong phần kết hợp.

4.3 Thay đổi mô hình khai báo

Bố cục Compose cơ bản

Việc triển khai hệ thống bố cục trong Jetpack Compose có hai mục tiêu chính:

• Khả năng viết các bố cục tuỳ chỉnh một cách dễ dàng

Khi sử dụng hệ thống Android View, bạn có thể gặp phải vấn đề về hiệu suất khi lồng ghép các hệ thống View như RelativeLayout Tuy nhiên, với Compose, bạn có thể dễ dàng tạo các lớp lồng nhau với nhiều nội dung mà không lo ảnh hưởng đến hiệu suất, nhờ vào việc tránh nhiều chế độ đo lường.

Hàm có khả năng kết hợp là khối xây dựng cơ bản trong Compose, cho phép chèn các thành phần giao diện người dùng bằng cách sử dụng dữ liệu đầu vào để tạo nội dung hiển thị Để tìm hiểu thêm về các thành phần kết hợp, hãy tham khảo tài liệu Mô hình tư duy của Compose.

Hàm có khả năng kết hợp trong giao diện người dùng có thể chuyển phát nhiều thành phần, nhưng nếu không chỉ định cách sắp xếp, Compose sẽ tự động sắp xếp các thành phần theo cách không mong muốn Ví dụ, đoạn mã này sẽ tạo ra hai thành phần văn bản.

Nếu không có hướng dẫn về cách sắp xếp các thành phần, công cụ Compose sẽ xếp chồng các thành phần văn bản, dẫn đến tình trạng khó đọc.

Compose cung cấp các bố cục sẵn có, giúp bạn dễ dàng sắp xếp các thành phần trên giao diện người dùng Điều này cho phép bạn xác định và tùy chỉnh các bố cục của riêng mình với độ chuyên biệt cao hơn.

5.3 Các bố cục cơ bản

Trong nhiều trường hợp, bạn chỉ cần sử dụng Các thành phần của bố cục tiêu chuẩn trong Compose

Hãy dùng Column để đặt các mục theo chiều dọc trên màn hình

Sử dụng Row để sắp xếp các mục theo chiều ngang trên màn hình, trong khi cả mã Column và Row đều cho phép cấu hình căn chỉnh các thành phần bên trong.

Sử dụng Box để xếp các thành phần chồng lên nhau Box cũng hỗ trợ định cấu hình căn chỉnh cụ thể các thành phần có trong mã

Các khối xây dựng này thường là đủ cho nhu cầu của bạn Bạn có thể tạo hàm kết hợp riêng để tích hợp các bố cục này thành một bố cục chi tiết hơn, phù hợp với ứng dụng của mình.

Compose efficiently handles nested layout structures, making it an excellent choice for designing complex user interfaces This represents an improvement over Android Views, where nested layouts are often avoided due to performance concerns To position child layouts within a Row, use the horizontalArrangement and verticalAlignment parameters For a Column, set the verticalArrangement and horizontalAlignment parameters accordingly.

Trong mô hình bố cục, cây giao diện người dùng được sắp xếp theo một luồng nhất định Mỗi nút tự đo lường trước khi đo lường định kỳ các thành phần con, chuyển các giới hạn kích thước xuống cho chúng Sau đó, cần định kích thước và đặt các nút lá, và cuối cùng, các kích cỡ cùng với các câu lệnh hướng dẫn vị trí sẽ được chuyển trở lại cây.

Để đạt được sự chính xác trong việc đo lường, cần ưu tiên xác định các thành phần mẹ trước khi tiến hành đo lường các thành phần con cháu Tuy nhiên, quá trình định kích thước và sắp xếp các thành phần mẹ thường diễn ra sau khi các thành phần con cháu đã được xác định.

Hãy xem xét hàm SearchResult sau

Hàm này tạo cây giao diện người dùng sau đây

Trong ví dụ SearchResult, bố cục cây giao diện người dùng tuân theo thứ tự sau:

1 Câu lệnh yêu cầu đo lường nút gốc Row

2 Nút gốc Row yêu cầu nút con cháu đầu tiên của nó, Image, đo lường

3 Image là một nút lá (nút không có nút con cháu), vì vậy nút này báo cáo kích thước và trả về các câu lệnh hướng dẫn vị trí

4 Nút gốc Row yêu cầu nút con cháu thứ hai của nó, Column, đo lường

5 Nút Column yêu cầu nút con cháu đầu tiên của nó, Text, đo lường

6 Nút Text đầu tiên là một nút lá Nút lá chỉ báo cáo kích thước và trả về câu lệnh hướng dẫn vị trí

7 Nút Column yêu cầu nút con cháu Text thứ hai của nó đo lường

8 Nút Text thứ hai là một nút lá, vì vậy nút này báo cáo kích thước và trả về câu lệnh hướng dẫn vị trí

9 Hiện tại, nút Column đã đo lường xong, đã định kích thước và đặt các nút con cháu của nó, nút này có thể xác định kích thước và vị trí của riêng mình

10 Hiện tại, nút gốc Row đã đo lường xong, đã định kích thước và đặt các nút con cháu của nó, nút này có thể xác định kích thước và vị trí của riêng mình

Compose đạt hiệu suất cao bằng cách chỉ đo lường nút con cháu một lần duy nhất Chế độ đo lường một luồng giúp cải thiện hiệu suất, cho phép Compose xử lý các cây giao diện người dùng một cách hiệu quả Nếu một thành phần đo lường con cháu của nó nhiều lần, và mỗi con cháu cũng làm tương tự, thì việc tạo ra toàn bộ giao diện người dùng sẽ trở nên phức tạp, gây khó khăn cho việc duy trì ứng dụng.

Nếu bố cục của bạn cần nhiều lần đo lường vì một lý do nào đó, Compose có một hệ thống đặc biệt, phép đo lường hàm nội tại

Việc đo lường và xác định vị trí là hai giai đoạn độc lập trong quy trình bố cục, cho phép thực hiện các thay đổi chỉ ảnh hưởng đến vị trí của các mục mà không làm ảnh hưởng đến quá trình đo lường.

Các đối tượng sửa đổi trong Compose cho phép bạn trang trí và bổ sung các thành phần kết hợp, đóng vai trò quan trọng trong việc tùy chỉnh bố cục Ví dụ, bạn có thể kết hợp nhiều đối tượng sửa đổi để tùy chỉnh ArtistCard một cách linh hoạt và sáng tạo.

Trong mã trên, hãy chú ý đến các đối tượng sửa đổi có chức năng khác nhau được sử dụng cùng nhau

• clickable tạo một phản ứng kết hợp với thông tin do người dùng nhập và hiển thị một hiệu ứng gợn sóng

• padding đặt khoảng trống quanh một thành phần

• fillMaxWidth làm cho thành phần kết hợp lấp đầy chiều rộng tối đa mà thành phần mẹ đã cấp cho nó

• size() xác định chiều rộng và chiều cao ưu tiên của một thành phần

Quản lý trạng thái

Trạng thái trong ứng dụng đại diện cho giá trị có thể thay đổi theo thời gian, bao gồm nhiều khía cạnh từ cơ sở dữ liệu Room đến các biến trong lớp (class).

Tất cả ứng dụng Android đều cho người dùng thấy trạng thái Sau đây là một số ví dụ về trạng thái trong ứng dụng Android:

• Một thanh thông báo nhanh cho biết thời điểm không thể thiết lập kết nối mạng

• Một bài đăng trên blog và các bình luận liên quan

• Ảnh động gợn sóng trên các nút phát khi người dùng nhấp vào

• Hình dán mà người dùng có thể vẽ lên hình ảnh

Jetpack Compose giúp bạn nắm bắt cách lưu trữ và sử dụng trạng thái trong ứng dụng Android Hướng dẫn này tập trung vào việc kết nối giữa các trạng thái và thành phần kết hợp (composable), đồng thời giới thiệu các API của Jetpack Compose để quản lý trạng thái một cách hiệu quả hơn.

6.1 Trạng thái và composition

Compose mang tính khai báo, vì vậy để cập nhật, bạn cần gọi lại cùng một thành phần kết hợp với đối số mới, đại diện cho trạng thái giao diện người dùng Mỗi khi trạng thái được cập nhật, sẽ xảy ra một lượt tái cấu trúc Do đó, các thành phần như TextField không tự động cập nhật như trong khung hiển thị XML Một thành phần kết hợp cần được thông báo rõ ràng về trạng thái mới để thực hiện cập nhật tương ứng.

Khi thực hiện thao tác này, bạn sẽ nhận thấy không có sự thay đổi nào, vì TextField chỉ cập nhật khi tham số value của nó thay đổi Điều này liên quan đến cách hoạt động của tính năng cấu trúc và tái cấu trúc trong Compose.

6.2 Trạng thái trong composition

Các hàm kết hợp có thể sử dụng API remember để lưu trữ đối tượng trong bộ nhớ, với giá trị được tính toán và lưu trữ trong Cấu trúc (Composition) trong quá trình cấu trúc ban đầu Giá trị này sẽ được trả về trong quá trình tái cấu trúc, cho phép lưu trữ cả đối tượng có thể thay đổi và không thể thay đổi Hàm mutableStateOf tạo ra MutableState có thể quan sát, là một loại đối tượng tích hợp với thời gian chạy Compose.

Mọi thay đổi đối với giá trị sẽ dẫn đến việc lên lịch tái cấu trúc tất cả các hàm có khả năng đọc giá trị đó Đối với ExpandingCard, bất kỳ sự thay đổi nào về trạng thái mở rộng sẽ kích hoạt hệ thống tái cấu trúc ExpandingCard.

Có 3 cách để khai báo đối tượng MutableState trong một thành phần kết hợp:

• val mutableState = remember { mutableStateOf(default) }

• var value by remember { mutableStateOf(default) }

• val (value, setValue) = remember { mutableStateOf(default) }

Các thông tin khai báo này tương đương và được trình bày theo cú pháp dễ hiểu nhằm phục vụ cho mục đích sử dụng của trạng thái Bạn nên lựa chọn định dạng tạo ra mã dễ đọc nhất trong thành phần kết hợp mà bạn đang viết.

Cú pháp uỷ quyền (delegate syntax) by yêu cầu các import sau:

Bạn có thể sử dụng giá trị đã ghi nhớ làm tham số cho các thành phần kết hợp khác hoặc logic trong các câu lệnh để điều chỉnh thành phần hiển thị Ví dụ, để ẩn lời chào khi phần tên trống, hãy áp dụng trạng thái trong câu lệnh if.

Mặc dù remember giúp duy trì trạng thái qua các lần tái cấu trúc, trạng thái này sẽ không được giữ lại khi cấu hình thay đổi Để giữ lại trạng thái, bạn cần sử dụng rememberSaveable, vì nó tự động lưu trữ mọi giá trị có thể lưu trong Bundle Đối với các giá trị khác, bạn có thể sử dụng một đối tượng lưu tùy chỉnh.

6.3 Chuyển trạng thái lên trên

Tính năng chuyển trạng thái lên trên (state hoisting) trong Compose là một dạng chuyển đổi trạng thái cho phương thức gọi của một thành phần kết hợp khiến

Trang 33 nó trở thành không trạng thái Mô hình chung để di chuyển trạng thái lên trên trong Jetpack Compose là thay thế biến trạng thái bằng 2 tham số:

• value: T: giá trị hiện tại để hiển thị

• onValueChange: (T) -> Unit: một sự kiện yêu cầu thay đổi giá trị này, trong đó T là giá trị mới được đề xuất

Bạn không chỉ giới hạn ở onValueChange; nếu có các sự kiện cụ thể hơn phù hợp với thành phần kết hợp, hãy sử dụng lambda để xác định sự kiện, chẳng hạn như sử dụng onExpand và onCollapse cho ExpandingCard.

Trạng thái được di chuyển lên trên theo cách này có một số thuộc tính quan trọng:

Bằng cách chuyển trạng thái thay vì sao chép, chúng tôi tạo ra một nguồn thông tin duy nhất, giúp giảm thiểu lỗi và đảm bảo tính chính xác.

• Được đóng gói (encapsulated): Chỉ các thành phần kết hợp có trạng thái mới có thể sửa đổi trạng thái của chúng Nó có tính nội bộ hoàn toàn

Việc chia sẻ trạng thái lên trên với nhiều thành phần kết hợp cho phép bạn dễ dàng đọc tên trong các thành phần khác Điều này giúp tối ưu hóa khả năng tương tác và quản lý trạng thái trong ứng dụng của bạn.

Phương thức gọi đến các thành phần kết hợp không trạng thái có thể được xem là có khả năng chắn (interceptable), cho phép quyết định bỏ qua hoặc sửa đổi các sự kiện trước khi thực hiện thay đổi trạng thái.

ExpandingCard được tách riêng, nghĩa là trạng thái của nó không thể được lưu trữ ở bất kỳ đâu Điều này cho phép bạn di chuyển thuộc tính name sang ViewModel một cách linh hoạt hơn.

Trong ví dụ này, bạn cần trích xuất các thành phần name và onValueChange từ HelloContent và sau đó di chuyển chúng lên một thành phần kết hợp là HelloScreen bằng cách gọi HelloContent.

Vòng đời của Composable

Thành phần Compose trong quản lý trạng thái mô tả giao diện người dùng của ứng dụng, được tạo ra thông qua việc thực thi các thành phần kết hợp.

Trang 39 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 thực hiện quá trình kết hợp lần đầu, nó sẽ theo dõi các thành phần bạn sử dụng để mô tả giao diện người dùng Khi trạng thái ứng dụng thay đổi, Jetpack Compose sẽ lên lịch cho việc kết hợp lại Quá trình này xảy ra khi Jetpack Compose tái thực thi các thành phần có thể thay đổi, từ đó cập nhật giao diện để phản ánh các thay đổi trong trạng thái.

Thành phần Compose chỉ có thể được tạo ra và cập nhật thông qua quá trình kết hợp ban đầu và kết hợp lại Phương pháp duy nhất để chỉnh sửa thành phần Compose là thực hiện việc 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 trong Compose sẽ theo dõi các thay đổi và tự động cập nhật tất cả các thành phần kết hợp có khả năng đọc State, cũng như bất kỳ thành phần kết hợp nào được gọi trong quá trình này.

Khi một thành phần kết hợp được gọi nhiều lần, sẽ có nhiều thực thể được tạo ra trong thành phần Compose Mỗi lần gọi sẽ có vòng đời riêng biệt trong thành phần này.

Giá trị đại diện của MyComposable trong thành phần Compose cho thấy rằng khi một thành phần kết hợp được gọi nhiều lần, sẽ có nhiều thực thể được tạo ra trong thành phần này Mỗi phần tử với màu sắc riêng biệt biểu thị một thực thể độc lập.

7.2 Phân tích một Composable trong Composition

Trong Compose, thực thể của một thành phần kết hợp được xác định qua vị trí gọi, với trình biên dịch xem mỗi vị trí gọi là độc lập Khi gọi các thành phần kết hợp từ nhiều vị trí khác nhau, sẽ tạo ra nhiều thực thể khác nhau của thành phần đó trong Compose.

Trong quá trình kết hợp các thành phần, Compose xác định và nhận diện những thành phần đã được gọi trong các lần kết hợp trước Đặc biệt, đối với những thành phần được gọi trong cả hai lần, Compose sẽ tránh việc kết hợp lại nếu giá trị đầu vào không thay đổi.

Bảo tồn mã nhận dạng là yếu tố then chốt giúp liên kết các hiệu ứng phụ với thành phần kết hợp, từ đó cho phép các hiệu ứng này hoàn tất thành công mà không cần tái khởi động cho mỗi quá trình kết hợp.

Hãy xem ví dụ sau đây:

Trong đoạn mã này, thành phần LoginScreen sẽ kích hoạt thành phần kết hợp LoginError dựa trên điều kiện nhất định, đồng thời luôn gọi thành phần LoginInput Mỗi lần gọi đều có vị trí và nguồn duy nhất mà trình biên dịch sẽ sử dụng để xác định.

Giá trị đại diện của LoginScreen trong thành phần Compose phản ánh sự thay đổi trạng thái và quá trình kết hợp lại Màu sắc tương đồng cho thấy mã này vẫn chưa được kết hợp lại.

Dù mức độ ưu tiên của LoginInput đã chuyển từ thứ nhất sang thứ hai, nhưng LoginInput vẫn được duy trì trong các thành phần kết hợp.

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

7.2.1 Thêm thông tin hỗ trợ quá trình recomposition

Khi gọi một thành phần kết hợp nhiều lần từ cùng một vị trí, thành phần đó sẽ được thêm vào thành phần kết hợp Compose nhiều lần mà không có thông tin để phân biệt các lệnh gọi Điều này dẫn đến việc sử dụng thứ tự thực thi tại cùng một vị trí gọi để tách biệt các thực thể Mặc dù hành vi này có thể cần thiết trong một số trường hợp, nhưng nó cũng có thể gây ra những hành vi không mong muố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 Khi một bộ phim mới được thêm vào cuối danh sách, Compose có khả năng tái sử dụng các thực thể đã có trong thành phần, vì vị trí của chúng trong danh sách không thay đổi, do đó, giá trị đầu vào của các thực thể đó vẫn được giữ nguyên.

Trong thành phần Compose, giá trị đại diện của MoviesScreen được thể hiện rõ khi thêm một phần tử mới vào cuối danh sách Điều này cho thấy khả năng tái sử dụng các thành phần kết hợp một cách hiệu quả.

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

Các giai đoạn trong Jetpack Compose

Ứng dụng Compose, giống như nhiều bộ công cụ giao diện người dùng khác, hiển thị qua nhiều giai đoạn riêng biệt Hệ thống Android View có ba giai đoạn chính là đo lường, bố cục và bản vẽ Tuy nhiên, Compose bổ sung thêm một giai đoạn quan trọng gọi là sáng tác, diễn ra ở giai đoạn đầu tiên.

8.1 3 giai đoạn của một frame

Compose có ba giai đoạn chính:

1 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

2 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

3 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ác giai đoạn trong quá trình truyền dữ liệu thường giống nhau, cho phép dữ liệu di chuyển theo một hướng từ thành phần đến bố cục và sau đó đến bản vẽ, tạo thành một khung dữ liệu một chiều Tuy nhiên, BoxWithConstraints, LazyColumn và LazyRow là những trường hợp ngoại lệ, trong đó thành phần của tệp con phụ thuộc vào giai đoạn bố cục của tệp mẹ.

Bạn có thể yên tâm rằng ba giai đoạn này xảy ra hầu hết trong mọi khung Tuy nhiên, để tối ưu hiệu suất, Compose sẽ tránh lặp lại các công việc để đạt được kết quả giống nhau với cùng một dữ liệu đầu vào Compose không thực hiện một hàm có thể kết hợp nếu nó có thể tái sử dụng kết quả cũ, và giao diện người dùng Compose sẽ không tái tạo bố cục hoặc vẽ lại toàn bộ cây.

Trang 48 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

Khi đọc giá trị của trạng thái tổng quan nhanh (snapshot state) trong các giai đoạn đã liệt kê, Compose sẽ tự động theo dõi trạng thái của hoạt động tương ứng Tính năng này cho phép Compose thực thi lại trình đọc khi giá trị trạng thái thay đổi, tạo nền tảng cho việc quan sát trạng thái trong Compose.

Trạng thái trong Kotlin thường được tạo ra bằng cách sử dụng mutableStateOf() và có thể được truy cập theo hai cách: thông qua thuộc tính value trực tiếp hoặc thông qua đại diện thuộc tính Kotlin Để tìm hiểu thêm, bạn có thể tham khảo phần Trạng thái trong các hàm có thể kết hợp Trong hướng dẫn này, thuật ngữ "đọc trạng thái" đề cập đến một trong các phương thức truy cập tương đương đó.

Trong chế độ đại diện thuộc tính (properties delegates), các hàm "getter" và

"Setter" được sử dụng để truy cập và thiết lập giá trị của trạng thái Các hàm getter và setter chỉ được gọi khi bạn tham chiếu thuộc tính như một giá trị, không phải khi thuộc tính được tạo ra Do đó, hai cách này là tương đương nhau.

Mỗi khối mã có khả năng thực hiện lại khi có sự thay đổi trong trạng thái đọc, được gọi là phạm vi khởi động lại Compose theo dõi các thay đổi về giá trị của trạng thái và khởi động lại các phạm vi tại những giai đoạn khác nhau.

8.3 Đọc trạng thái theo giai đoạn

Compose có ba giai đoạn chính và theo dõi trạng thái được đọc trong từng giai đoạn Điều này giúp Compose chỉ thông báo cho các giai đoạn cần thực hiện công việc cho các thành phần bị ảnh hưởng trong giao diện người dùng.

Lưu ý rằng vị trí tạo và lưu trữ một phiên bản trạng thái không ảnh hưởng nhiều đến các giai đoạn Điều quan trọng là thời điểm và địa điểm mà trạng thái giá trị được đọc.

Hãy xem qua từng giai đoạn và mô tả các sự việc xảy ra khi giá trị của Trạng thái được đọc trong mỗi giai đoạn đó

Các trạng thái đọc trong hàm @Composable hoặc khối lambda ảnh hưởng đến các thành phần và có thể dẫn đến các giai đoạn tiếp theo Khi giá trị trạng thái thay đổi, trình biên dịch sẽ lên lịch chạy lại tất cả các hàm có thể kết hợp đã đọc giá trị trạng thái đó Tuy nhiên, thời gian chạy có thể bỏ qua một số hoặc tất cả các hàm có thể kết hợp nếu dữ liệu đầu vào không thay đổi Để biết thêm thông tin, hãy tham khảo phần Bỏ qua nếu dữ liệu đầu vào không thay đổi.

Giao diện người dùng Compose sẽ thực hiện giai đoạn bố cục và vẽ dựa vào kết quả của composition, nhưng có thể bỏ qua các giai đoạn này nếu nội dung, kích thước và bố cục không thay đổi.

Giai đoạn bố cục trong thiết kế giao diện bao gồm hai bước chính: đo lường và đặt vị trí Đầu tiên, bước đo lường thực hiện thông qua lambda đo lường được chuyển đến thành phần Layout, sử dụng phương thức MeasureScope.measure của giao diện LayoutModifier Tiếp theo, bước đặt vị trí được thực hiện thông qua khối vị trí của hàm layout và khối lambda của Modifier.offset.

Ktor

Ktor là một framework phát triển ứng dụng web viết bằng Kotlin, ngôn ngữ lập trình chạy trên nền tảng Java Virtual Machine (JVM) Được phát triển bởi JetBrains và ra mắt lần đầu vào năm 2017, Ktor được thiết kế để đơn giản, nhẹ nhàng và linh hoạt, đồng thời cung cấp đầy đủ các tính năng cần thiết cho việc xây dựng ứng dụng web mạnh mẽ.

Kotlin là một ngôn ngữ lập trình hiện đại, an toàn và linh hoạt, được sử dụng để phát triển Ktor Nó kế thừa những ưu điểm của Java như tính bảo mật và độ tin cậy, đồng thời cung cấp cú pháp ngắn gọn hơn cùng với các tính năng tiên tiến như null safety và extension functions Việc sử dụng Kotlin giúp tối ưu hóa quy trình phát triển ứng dụng với Ktor, mang lại hiệu quả cao hơn cho lập trình viên.

Ktor đơn giản hóa quá trình phát triển ứng dụng web với cấu trúc dự án tối giản, cho phép lập trình viên tập trung vào logic ứng dụng mà không phải lo lắng về cấu hình phức tạp Nó tích hợp nhiều module hỗ trợ các tính năng quan trọng như routing, gửi và nhận yêu cầu HTTP, và xử lý lỗi.

Ktor hỗ trợ phát triển trên nền tảng JVM và triển khai trên Android và iOS nhờ vào Kotlin Multiplatform, giúp bạn dễ dàng xây dựng ứng dụng web chạy trên nhiều nền tảng khác nhau.

Ktor hỗ trợ tích hợp hiệu quả với các công nghệ hiện đại như JSON, HTML, gRPC và WebSocket, giúp các nhà phát triển dễ dàng xây dựng ứng dụng web.

Ktor được tối ưu hóa cho hiệu suất cao và khả năng mở rộng, sử dụng cơ chế xử lý không đồng bộ cùng với coroutine để xử lý nhiều yêu cầu đồng thời mà không làm chậm luồng chính Nhờ đó, Ktor có khả năng xử lý nhanh chóng và đáp ứng hiệu quả trong điều kiện tải cao.

9.3 Vài thành phần chính của Ktor

Ktor cung cấp một hệ thống routing mạnh mẽ, cho phép định nghĩa các tuyến đường trong ứng dụng web một cách dễ dàng thông qua cú pháp DSL Hệ thống này hỗ trợ nhiều loại yêu cầu HTTP như GET, POST, PUT, DELETE và nhiều hơn nữa, giúp xử lý các yêu cầu một cách hiệu quả.

Ktor cho phép định nghĩa các handlers để xử lý yêu cầu HTTP từ khách hàng, với mỗi tuyến đường có thể chứa một hoặc nhiều handlers Bạn có thể thực hiện các thao tác như xử lý yêu cầu, truy cập cơ sở dữ liệu, tương tác với các API bên ngoài, và thực hiện các logic khác trong các handlers này.

Ktor hỗ trợ middleware, cho phép thực hiện các xử lý chung trước và sau khi yêu cầu đi qua các handlers Middleware có thể được sử dụng cho xác thực, ghi log, kiểm soát truy cập và nhiều tác vụ khác Ngoài các middleware thông dụng mà Ktor cung cấp, bạn cũng có thể tự định nghĩa middleware riêng của mình.

Ktor hỗ trợ Content Negotiation, cho phép xử lý nhiều định dạng dữ liệu như JSON, HTML, XML và hơn thế nữa Tính năng này giúp bạn nhận và gửi dữ liệu theo các định dạng khác nhau dựa trên yêu cầu của khách hàng.

9.4 Sử dụng Ktor Để sử dụng Ktor trong phát triển ứng dụng web, bạn cần thêm dependency của Ktor vào dự án Kotlin của mình Bạn có thể sử dụng công cụ quản lý phụ thuộc như Maven hoặc Gradle để thêm Ktor vào dự án Sau khi thêm dependency, bạn có thể bắt đầu viết mã để định nghĩa tuyến đường, handlers và middleware

Dưới đây là một ví dụ đơn giản về việc sử dụng Ktor để tạo một ứng dụng web Hello World:

Chúng ta sử dụng hàm embeddedServer từ module Netty để khởi tạo một máy chủ Ktor, định nghĩa tuyến đường GET "/" để trả về chuỗi "Hello, World!" khi được truy cập, và cuối cùng sử dụng start(wait) để khởi động máy chủ.

= true) để bắt đầu chạy máy chủ và lắng nghe các yêu cầu

Ktor là một framework phát triển ứng dụng web mạnh mẽ và linh hoạt dành cho Kotlin, nổi bật với cú pháp đơn giản và khả năng tích hợp công nghệ hiện đại Với hiệu suất cao, Ktor là lựa chọn lý tưởng cho việc xây dựng ứng dụng web đa nền tảng Sự hỗ trợ từ cộng đồng phát triển Kotlin càng làm tăng sự phổ biến của Ktor trong lĩnh vực phát triển ứng dụng.

XÂY DỰNG ỨNG DỤNG

Ngày đăng: 04/09/2023, 20:28

HÌNH ẢNH LIÊN QUAN

Hình 1. Một hàm có khả năng kết hợp đơn giản được chuyển dữ liệu và sử dụng dữ - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 1. Một hàm có khả năng kết hợp đơn giản được chuyển dữ liệu và sử dụng dữ (Trang 13)
Hình 2:  Logic của ứng dụng cung cấp dữ liệu cho hàm có khả năng kết hợp cấp  cao nhất - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 2 Logic của ứng dụng cung cấp dữ liệu cho hàm có khả năng kết hợp cấp cao nhất (Trang 15)
Hình 3. Người dùng tương tác với một thành phần trên giao diện người dùng, - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 3. Người dùng tương tác với một thành phần trên giao diện người dùng, (Trang 16)
Hình ảnh khác trong bộ chọn làm hình nền. - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
nh ảnh khác trong bộ chọn làm hình nền (Trang 38)
Hình 2. Giá trị đại diện của MyComposable trong thành phần Compose. Nếu - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 2. Giá trị đại diện của MyComposable trong thành phần Compose. Nếu (Trang 40)
Hình  4.  Giá  trị  đại  diện  của  MoviesScreen  trong  thành  phần  Compose  khi - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
nh 4. Giá trị đại diện của MoviesScreen trong thành phần Compose khi (Trang 42)
Hình  5.  Giá  trị  đại  diện  của  MoviesScreen  trong  thành  phần  Compose  khi - Đồ án tìm hiểu jetpack compose và xây dựng ứng dụng
nh 5. Giá trị đại diện của MoviesScreen trong thành phần Compose khi (Trang 43)

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN

w