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

Tìm hiểu jetpack compose và xây dựng ứng dụng

104 15 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

Định dạng
Số trang 104
Dung lượng 6,42 MB

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 (8)
    • 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? (10)
    • 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 (15)
      • 4.5. Recomposition (16)
        • 4.5.1. Các hàm composable có thể thực thi theo thứ tự bất kỳ (17)
        • 4.5.2. Các hàm composable có thể chạy song song (17)
        • 4.5.3. Bỏ qua recomposition nhiều nhất có thể (19)
        • 4.5.4. Recomposition là khả quan (20)
        • 4.5.5. Các hàm có khả năng recomposition có thể chạy khá thường xuyên (21)
    • 5. Bố cục Compose cơ bản (21)
      • 5.1. Mục tiêu (21)
      • 5.2. Các hàm Composable (21)
      • 5.3. Các bố cục cơ bản (22)
      • 5.4. Mô hình bố cục (24)
      • 5.5. Hiệu suất (26)
      • 5.6. Sử dụng modifiers (26)
    • 6. Quản lý trạng thái (28)
      • 6.1. Trạng thái và composition (28)
      • 6.2. Trạng thái trong composition (29)
      • 6.3. Chuyển trạng thái lên trên (30)
      • 6.4. Khôi phục trạng thái (33)
        • 6.4.1. Parcelize (33)
        • 6.4.2. MapSaver (34)
      • 6.5. Phần tử giữ trạng thái (34)
      • 6.6. Kích hoạt lại tính năng ghi nhớ các tính toán khi khoá thay đổi (34)
    • 7. Vòng đời của Composable (36)
      • 7.1. Tổng quan (36)
      • 7.2. Phân tích một Composable trong Composition (38)
        • 7.2.1. Thêm thông tin hỗ trợ quá trình recomposition (39)
      • 7.3. Bỏ qua nếu giá trị đầu vào không thay đổi (42)
    • 8. Các giai đoạn trong Jetpack Compose (44)
      • 8.2. Đọc trạng thái (45)
      • 8.3. Đọc trạng thái theo giai đoạn (46)
        • 8.3.1. Giai đoạn 1: Composition (46)
        • 8.3.2. Giai đoạn 2: Layout (47)
        • 8.3.3. Giai đoạn 3: Drawing (48)
      • 8.4. Tối ưu hóa việc đọc trạng thái (48)
      • 8.5. Vòng lặp tái kết hợp (phần phụ thuộc giai đoạn tuần hoàn) (50)
    • 9. Ktor (53)
      • 9.1. Tổng quan (53)
      • 9.2. Đặc điểm nổi bật (53)
      • 9.3. Vài thành phần chính của Ktor (53)
  • CHƯƠNG 3: XÂY DỰNG ỨNG DỤNG (56)
    • 1. Tổng quan (56)
      • 1.1. Tên ứng dụng: Yum - Ứng dụng hỗ trợ đầu bếp (56)
      • 1.2. Lý do chọn ứng dụng (56)
      • 1.3. Đối tượng hướng đến (56)
      • 1.4. Môi trường phát triển ứng dụng (56)
      • 1.5. Kết quả mong đợi (57)
      • 1.6. Quy trình thực hiện các công việc chính (57)
    • 2. Phân tích, thiết kế hệ thống (57)
      • 2.1. Xác định và mô hình hóa các yêu cầu phần mềm (57)
        • 2.1.1. Xác định yêu cầu (57)
          • 2.1.1.1. Một số yêu cầu phần mềm phải có (57)
          • 2.1.1.2. Ràng buộc logic ban đầu (58)
          • 2.1.1.3. Tính khả dụng (58)
          • 2.1.1.4. Tính ổn định (58)
          • 2.1.1.5. Hiệu suất (58)
          • 2.1.1.6. Bảo mật (58)
        • 2.1.2. Mô hình hóa yêu cầu (59)
          • 2.1.2.1. Danh sách chức năng (59)
          • 2.1.2.2. Lược đồ Use-case (59)
      • 2.2. Thiết kế hệ thống (74)
        • 2.2.1. Kiến trúc hệ thống (74)
      • 2.3. Thiết kế dữ liệu (77)
      • 2.4. Thiết kế giao diện (81)
        • 2.4.1. Sơ đồ liên kết màn hình (81)
        • 2.4.2. Danh sách các màn hình (81)
    • 3. Cài đặt và thử nghiệm (101)

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

Chúng ta không thể phủ nhận sự hữu ích của các framework coss-platform (Flutter, React-Native, …) đã giúp chúng ta phần nào giải quyết được vấn đề về mặt thời gian Tuy vậy khi nhìn vào thực tế, các công ty lớn trên thế giới tiếp tục sử dụng các ứng dụng native, đó là vấn đề lớn cũng cần phải được kể đến: hiệu suất Hiệu suất ả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 từ lâu đã tồn tại những yếu điểm dễ dàng nhận thấy Đơn cử như đối với Android, đó là việc nhà phát triển phải sử dụng một loại ngôn ngữ khác hoàn toàn với ngôn ngữ chính để lập trình – XML Đối với các nhà phát triển cũng như các công ty phải học song song 2 ngôn ngữ (Java/Kotlin và XML) là rất mất thời gian, dẫn đến đó là tiêu tốn nhiều chi phí.

Kể từ sau Google I/O 2019, Kotlin dần phát triển để thay thế Java, trở thành một ngôn ngữ lập trình được ưa chuộng nhất cho phát triển ứng dụng Android native Và điều chúng ta mong muốn nhất rồi cũng đến: Jetpack Compose, sử dụng Kotlin làm ngôn ngữ lập trình đã ra đời với nhiệm vụ tiên quyết: source code chỉ chứa duy nhất 1 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:

 Đối với các nhân: mở rộng kiến thức của mình về Compose thông qua quá trình tìm hiểu và áp dụng vào ứng dụng thực tế Các kiến thức tìm hiểu được thông qua đồ án thúc đẩy em phát triển thêm các ứng dụng khác bằng Compose Jetpack Đồng thời, em học được cách nghiên cứu và sử dụng một framework mới cần trải qua quá trình gì nhằm giúp em dễ dàng tiếp xúc với các công nghệ mới hơn trong tương lai.

 Đối với các lập trình viên dùng đồ án nghiên cứu này làm tài liệu tham khảo: thông qua tài liệu nghiên cứu và ứng dụng em đã xây dựng, các lập trình viên khác có thể dễ dàng định hướng cần phải tìm hiểu gì khi sử dụng Jetpack Compose Đồng thời em có nêu ra một số khái niệm, kiến thức cơ bản và thư viện phổ biến được đề xuất bởi cộng đồng khi sử dụng Compose Jetpack, các lập trình viên khác có thể tham khảo và tìm hiểu sâu hơn.

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 thiết kế để chạy trên nền tảng Java Virtual Machine (JVM), nhưng cũng có thể chạy trên nền tảng 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à một ngôn ngữ lập trình hướng đối tượng (OOP) và hỗ trợ các khái niệm lập trình hàm (FP), giúp cho việc phát triển ứng dụng linh hoạt hơn.

 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 có thể tương thích với mã nguồn Java, cho phép lập trình viên dễ dàng chuyển đổi từ Java sang Kotlin hoặc sử dụng cả hai ngôn ngữ trong một dự án.

Kotlin đã được Google chính thức công nhận là ngôn ngữ lập trình cho phát triển ứng dụng Android, và được sử dụng rộng rãi trong cộng đồng lập trình viên Android Ngoài ra, Kotlin cũng được sử dụng trong nhiều dự án khác, bao gồm phát triển trên máy chủ, ứng dụng web và phát triển ứng dụng trên các thiết bị nhúng.

Jetpack Compose

Jetpack Compose là một thư viện UI cho phép bạn xây dựng giao diện người dùng Android bằng cách sử dụng Kotlin Nó cho phép tạo các giao diện người dùng tùy chỉnh và linh hoạt hơn Bạn có thể tạo các giao diJetpack Compose là một framework để phát triển giao diện người dùng (UI) trên nền tảng Android, được phát triển bởi Google Nó cho phép lập trình viên tạo giao diện người dùng bằng cách sử dụng các hàm và phương thức trong ngôn ngữ lập trình Kotlin, thay vì sử dụng XML như trước đây.

Với Jetpack Compose, người lập trình có thể tạo các giao diện người dùng động và tương tác với người dùng một cách dễ dàng Framework này cũng cung cấp cho người dùng các tính năng như tạo theme, animation, và layout cực kỳ linh hoạt.

Jetpack Compose được xây dựng trên cơ sở của các công nghệ hiện đại như

Kotlin, coroutines, và Android Jetpack.

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

 Khai báo giao diện người dùng: Jetpack Compose sử dụng phong cách lập trình khai báo, cho phép bạn xác định giao diện bằng cách chỉ định những gì bạn muốn nó hiển thị, thay vì xác định cách để đạt được nó Điều này làm cho mã dễ đọc, dễ hiểu hơn và dễ bảo trì hơn so với phương pháp truyền thống sử dụng XML.

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

 Hiệu suất cao hơn: Jetpack Compose được tối ưu để cung cấp hiệu suất cao hơn so với phương pháp truyền thống Compose sử dụng một cơ chế gọi là "Recompose" để chỉ cập nhật lại các phần tử giao diện người dùng đã thay đổi thay vì cập nhật toàn bộ giao diện Điều này giúp giảm tải cho CPU và cải thiện hiệu suất của ứng dụng.

 Tương thích và tái sử dụng tốt hơn: Jetpack Compose được thiết kế để tương thích với các phiên bản Android cũ hơn và mới hơn Bạn có thể sử dụng Compose trong các dự án hiện có hoặc tích hợp nó với mã Java/Kotlin truyền thống Ngoài ra, Jetpack Compose cũng khuyến khích 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.

 Cộng đồng mạnh mẽ: Jetpack Compose đã nhận được sự hỗ trợ rất lớn từ cộng đồng phát triển Android Có 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 trên Internet Bạn cũng có thể nhận được sự hỗ trợ từ cộng đồng qua các diễn đàn, nhóm Facebook, và các nhóm Telegram.

Tổng thể, Jetpack Compose là một framework mạnh mẽ và tiện ích cho việc phát triển giao diện người dùng Android Nó giúp bạn tăng năng suất, cải thiện hiệu suất ứng dụng và tạo ra giao diện linh hoạt, tương tác.

Mô hình tư duy trong Jetpack Compose

Jetpack Compose là bộ công cụ khai báo giao diện người dùng hiện đại choAndroid Compose sẽ giúp bạn dễ dàng ghi và duy trì giao diện người dùng ứng dụng bằng cách cung cấp API khai báo cho phép bạn hiển thị giao diện người dùng 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

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

Việc điều chỉnh chế độ xem theo cách thủ công sẽ làm tăng khả năng xảy ra lỗi. Nếu một phần dữ liệu hiển thị ở nhiều vị trí thì bạn sẽ có thể quên cập nhật một trong các chế độ xem hiển thị dữ liệu đó Việc tạo các trạng thái không hợp lệ cũng rất dễ xảy ra khi hai lượt cập nhật xung đột theo cách không mong đợi Ví dụ: một lượt cập nhật có thể cố gắng đặt một giá trị nút vừa mới bị xoá khỏi giao diện người dùng Nói chung, độ phức tạp của việc bảo trì phần mềm tăng lên cùng với số chế độ xem cần cập nhật.

Trong vài năm qua, toàn bộ ngành đã bắt đầu chuyển sang mô hình giao diện người dùng khai báo, giúp đơn giản hoá rất nhiều kỹ thuật liên quan đến việc xây dựng và cập nhật giao diện người dùng Kỹ thuật này hoạt động bằng cách tạo lại từ đầu toàn bộ màn hình, sau đó chỉ áp dụng những thay đổi cần thiết Phương pháp này giúp bạn dễ dàng cập nhật hệ phân cấp chế độ xem có trạng thái theo cách thủ công. Compose là một khung giao diện người dùng khai báo.

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

Bằng cách sử dụng Compose, bạn có thể xây dựng giao diện người dùng bằng cách xác định một tập hợp các hàm có khả năng kết hợp lấy dữ liệu và cung cấp các thành phần trên giao diện người dùng Một ví dụ đơn giản là hàm Greeting lấy String và cung cấp Text hiển thị tin nhắn chào mừ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ữ liệu này để 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 này được chú thích bằng chú thích @Composable Tất cả hàm có khả năng kết hợp đều phải có chú thích này; chú thích này sẽ thông báo cho trình biên dịch Compose rằng hàm này có mục đích chuyển đổi dữ liệu thành giao diện người dùng.

 Hàm này lấy dữ liệu Các hàm có khả năng kết hợp có thể chấp nhận các thông số cho phép logic của ứng dụng mô tả giao diện người dùng Trong trường hợp này, tiện ích của chúng tôi chấp nhận String để có thể chào người dùng theo tên.

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

 Hàm này không trả về bất kỳ giá trị nào Các hàm Compose cung cấp giao diện người dùng không cần trả về bất cứ điều gì vì các hàm này mô tả trạng thái màn hình mong muốn thay vì tạo 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 theo cách tương tự như khi được gọi nhiều lần với cùng một đối số và không sử dụng các giá trị khác như biến toàn cục hoặc lệnh gọi đến random().

 Hàm này mô tả giao diện người dùng mà không để lại bất kỳ tác dụng phụ nào, chẳng hạn như sửa đổi các thuộc tính hoặc biến toàn cục.

Nhìn chung, bạn phải viết tất cả các hàm có khả năng kết hợp bằng các thuộc tính này vì những lý do được thảo luận trong phần kết hợp lại.

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

Với nhiều bộ công cụ giao diện người dùng hướng đối tượng bắt buộc, bạn khởi chạy giao diện người dùng bằng cách tạo một cây tiện ích Bạn thường thực hiện việc này bằng cách tăng cường một tệp bố cục XML Mỗi tiện ích duy trì một trạng thái nội bộ riêng và hiển thị các phương thức getter và setter cho phép logic ứng dụng tương tác với tiện ích đó.

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

Lưu ý: Khi sử dụng hệ thống Android View, bạn có thể gặp phải một số vấn đề về hiệu suất khi lồng một số hệ thống View nhất định, chẳng hạn như RelativeLayout.

Vì Compose tránh nhiều chế độ đo lường, bạn có thể tạo các lớp lồng nhau nhiều nội dung theo ý muốn mà không ảnh hưởng đến hiệu suất

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

Một hàm có khả năng kết hợp có thể chuyển phát nhiều thành phần trên giao diện người dùng Tuy nhiên, nếu bạn không đưa ra cách sắp xếp, thì Compose có thể sắp xếp các thành phần theo cách mà bạn không muốn Ví dụ: mã này tạo ra hai thành phần văn bản:

Nếu bạn không hướng dẫn cách sắp xếp các thành phần này, thì công cụ Compose sẽ xếp chồng các thành phần văn bản lên nhau, khiến cho chúng không thể đọc được:

Compose cung cấp một tập hợp các bố cục sẵn sàng sử dụng để giúp bạn sắp xếp các thành phần trên giao diện người dùng và giúp bạn dễ dàng xác định các bố cục của riêng mình, ở mức độ 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.

Tương tự, hãy dùng Row để đặt các mục theo chiều ngang trên màn hình Cả hai mã Column và Row đều hỗ trợ định cấu hình căn chỉnh các thành phần có trong mã.

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ã.

Thông thường, các khối xây dựng này là tất cả những gì bạn cần Bạn có thể viết hàm có khả năng kết hợp của riêng mình để kết hợp các bố cục này vào một bố cục chi tiết hơn phù hợp với ứng dụng của bạn.

Lưu ý: Compose xử lý hiệu quả các bố cục lớp lồng nhau, khiến chúng trở thành một cách tuyệt vời để thiết kế giao diện người dùng phức tạp Đây là một sự cải tiến so với Android Views, trong đó bạn cần tránh các bố cục lớp lồng ghép vì lý do hiệu suất. Để đặt vị trí bố cục con trong Row, hãy đặt các đối số horizontalArrangement và verticalAlignment Đối với Column, hãy đặt các đối số verticalArrangement và horizontalAlignment:

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

Tóm lại, cần đo lường các thành phần mẹ trước các thành phần con cháu, nhưng việc định kích thước và đặt các thành phần mẹ lại diễn ra sau các thành phần con cháu.

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 được hiệu suất cao bằng cách đo lường nút con cháu một lần duy nhất Chế độ đo lường một luồng tốt cho hiệu suất, cho phép công cụ Compose xử lý sâu 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 thành phần con cháu của nó hai lần và thành phần con cháu đó đo lường từng thành phần con cháu của nó hai lần và cứ tiếp tục như vậy, thì một lần thử duy nhất để tạo ra toàn bộ Giao diện người dùng sẽ phải thực hiện rất nhiều việc, khiến ứng dụng của bạn khó duy trì hoạt độ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.

Vì việc đo lường và vị trí là các giai đoạn phụ riêng biệt của luồng bố cục, nên các thay đổi chỉ ảnh hưởng đến vị trí của các mục mà không ảnh hưởng đến hoạt động đo lường, đều có thể được thực hiện riêng biệt.

Như đã đề cập ở phần các đối tượng sửa đổi trong Compose, bạn có thể sử dụng đối tượng sửa đổi để trang trí hoặc bổ sung các thành phần kết hợp của bạn Các đối tượng sửa đổi là thành phần cần thiết để tuỳ chỉnh bố cục của bạn Ví dụ: ở đây chúng tôi liên kết một số đối tượng sửa đổi để tuỳ chỉnh ArtistCard:

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 là giá trị bất kỳ có thể thay đổi theo thời gian Đây là định nghĩa rất rộng và bao gồm mọi thứ từ cơ sở dữ liệu Room cho đến một biến trên một 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 hiểu rõ vị trí và cách thức lưu trữ cũng như dùng trạng thái trong một ứng dụng Android Tài liệu hướng dẫn này tập trung vào hoạt động 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 tập trung vào những API mà Jetpack Compose cung cấp để xử lý trạng thái dễ dàng hơn.

6.1 Trạng thái và composition

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

Nếu thực hiện việc này, bạn sẽ thấy rằng không có điều gì xảy ra Nguyên nhân là do TextField không tự cập nhật, mà sẽ cập nhật khi tham số value của nó thay đổi.

Lý do nằm ở cách hoạt động của tính năng cấu trúc (composition) và tái cấu trúc (recomposition) trong Compose.

6.2 Trạng thái trong composition

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

Mọi thay đổi đối với value sẽ lên lịch tái cấu trúc mọi hàm có khả năng kết hợp có thể đọc value Trong trường hợp ExpandingCard, bất cứ khi nào expanded thay đổi, hệ thống sẽ 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 là tương đương và được cung cấp dưới dạng cú pháp dễ hiểu theo mục đích sử dụng của trạng thái Bạn nên 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 thậm chí là logic trong các câu lệnh để thay đổi thành phần kết hợp được hiển thị Ví dụ: nếu bạn không muốn hiện lời chào nếu phần tên trống, hãy sử dụng trạng thái trong câu lệnh if:

Mặc dù remember giúp bạn giữ lại trạng thái trên các lần tái cấu trúc, trạng thái này sẽ không được giữ lại khi bạn thay đổi cấu hình Để làm được điều này, bạn phải sử dụng rememberSaveable rememberSaveable tự động lưu mọi giá trị có thể lưu trong Bundle Đối với các giá trị khác, bạn có thể chuyển vào một đối tượng lưu tuỳ 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 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

Tuy nhiên, bạn không bị giới hạn ở onValueChange Nếu các sự kiện cụ thể hơn phù hợp với thành phần kết hợp, bạn nên xác định sự kiện bằng cách sử dụng lambda như ExpandingCard với onExpand và onCollapse.

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:

 Một nguồn đáng tin cậy (single source of truth): Bằng cách di chuyển trạng thái thay vì sao chép, chúng tôi đảm bảo rằng chỉ có một nguồn thông tin duy nhất Điều này giúp tránh các lỗi.

 Đượ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.

 Có thể chia sẻ (shareable): Bạn có thể chia sẻ trạng thái được di chuyển lên trên với nhiều thành phần kết hợp Nếu bạn muốn đọc name trong một thành phần kết hợp khác, việc di chuyển trạng thái lên trên sẽ cho phép bạn làm việc đó.

 Có thể chắn (interceptable): 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ể quyết định bỏ qua hoặc sửa đổi các sự kiện trước khi thay đổi trạng thái.

 Được tách riêng (decoupled): trạng thái của ExpandingCard không có trạng thái có thể được lưu trữ ở bất cứ đâu Ví dụ: bạn hiện có thể di chuyển name sang ViewModel.

Trong trường hợp ví dụ, bạn trích xuất name và onValueChange ra HelloContent rồi di chuyển chúng lên trên đến một thành phần kết hợp HelloScreen bằng lệnh gọi

Vòng đời của Composable

Như đã đề cập trong phần 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 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 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ầnCompose.

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.

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

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:

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.

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

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.

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.

Các giai đoạn trong Jetpack Compose

Giống như hầu hết bộ công cụ giao diện người dùng khác, ứng dụng Compose sẽ hiển thị một khung qua nhiều giai đoạn (phase) riêng biệt Nếu chúng ta xem xét hệ thống Android View, thì thấy hệ thống này có ba giai đoạn chính: đo lường (measure), bố cục (layout) và bản vẽ (drawing) Compose thì rất giống nhưng có thêm một giai đoạn quan trọng gọi là sáng tác (composition) khi bắt đầu.

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ủ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 và LazyColumn và LazyRow là các trường hợp ngoại lệ đáng chú ý, 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 giả định rằng ba giai đoạn này xảy ra hầu như đối với mọi khung Tuy nhiên, khi xét về 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 không chạy một hàm có thể kết hợp nếu nó có thể sử dụng lại kết quả cũ, và giao diện người dùng Compose sẽ không tạo lại bố cục 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.

Khi bạn đọc giá trị của trạng thái tổng quan nhanh (snapshot state) của một trong các giai đoạn được liệt kê ở trên, Compose sẽ tự động theo dõi trạng thái của hoạt động khi giá trị được đọc Tính năng theo dõi này cho phép Compose thực thi lại trình đọc khi giá trị trạng thái thay đổi và là cơ sở để quan sát trạng thái trong Compose.Trạng thái thường được tạo bởi mutableStateOf(), sau đó truy cập bằng một trong hai cách: truy cập trực tiếp thuộc tính value hoặc sử dụng một đại diệ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 đó.

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

"setter" được dùng để truy cập và thiết lập 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 trên lại tương đương nhau.

Mỗi khối mã có thể được thực hiện 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ề giá trị của trạng thái và khởi động lại các phạm vi ở các giai đoạn khác nhau.

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

Như đã đề cập ở trên, có ba 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ầ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.

Lưu ý: Vị trí mà một phiên bản trạng thái được tạo và lưu trữ có ít liên quan đến các giai đoạn Nó chỉ quan trọng với thời điểm và nơi 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 thành phần và có thể là các giai đoạn tiếp theo Khi giá trị trạng thái thay đổi, trình soạn thảo lại sẽ lên lịch chạy lại tất cả hàm có thể kết hợp đã đọc giá trị 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 composition, 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.

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.v.

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 giá trị 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. Để chính xác hơn, bước đo lường và bước đặt vị trí sẽ có các phạm vi bắt đầu riêng biệt, nghĩa là việc đọc trạng thái trong bước đặt vị trí sẽ không gọi lại bước đo lường trước đó Tuy nhiên, hai bước này thường đan xen với nhau, vì vậy, một trạng thái được đọc trong bước đặt vị trí có thể ảnh hưởng đến các phạm vi khởi động lại khác thuộc bước đo lường.

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 giá trị trạng thái thay đổi, giao diện Compose chỉ chạy giai đoạn vẽ.

8.4 Tối ưu hóa việc đọc trạng thái

Ktor

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

 Ngôn ngữ Kotlin: Ktor được viết bằng Kotlin, một ngôn ngữ lập trình hiện đại, an toàn và linh hoạt Kotlin kế thừa các ưu điểm của Java như tính bảo mật, độ tin cậy và khả năng mở rộng, nhưng cung cấp cú pháp ngắn gọn hơn và hỗ trợ các tính năng tiên tiến như null safety và extension functions Việc sử dụng Kotlin giúp cho việc phát triển ứng dụng bằng Ktor trở nên dễ dàng và hiệu quả.

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

 Hỗ trợ đa nền tảng: Ktor không chỉ hỗ trợ việc phát triển trên nền tảng JVM mà còn hỗ trợ việc triển khai trên các nền tảng khác như Android và iOS thông qua Kotlin Multiplatform Điều này cho phép bạn xây dựng các ứng dụng web có khả năng chạy trên nhiều nền tảng một cách dễ dàng.

 Hỗ trợ các công nghệ hiện đại: Ktor tích hợp tốt với các công nghệ phổ biến trong cộng đồng phát triển, như JSON, HTML, gRPC, WebSocket và nhiều thứ khác Điều này giúp bạn dễ dàng làm việc với các công nghệ này trong việc xây dựng ứng dụng web.

 Hiệu suất và khả năng mở rộng: Ktor được thiết kế để có hiệu suất cao và khả năng mở rộng tốt Nó sử dụng cơ chế xử lý không đồng bộ và coroutine để xử lý nhiều yêu cầu cùng một lúc mà không gây chặn luồng chính Điều này giúp Ktor xử lý các yêu cầu một cách nhanh chóng và đáp ứng tốt với tải cao.

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

 Routing: Ktor cung cấp một hệ thống routing mạnh mẽ cho việc xác định các tuyến đường (routes) trong ứng dụng web Bằng cách sử dụng cú pháp DSL (Domain-Specific Language), bạn có thể dễ dàng định nghĩa các tuyến đường và xử lý các yêu cầu HTTP tương ứng Ktor hỗ trợ nhiều loại tuyến đường như GET, POST, PUT, DELETE và nhiều hơn nữa.

 Handlers: Ktor cho phép bạn định nghĩa các handlers để xử lý các yêu cầu HTTP từ khách hàng Mỗi tuyến đường có thể có một hoặc nhiều handlers, và bạn có thể xử lý các 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 thao tác logic khác trong các handlers này.

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

 Content Negotiation: Ktor hỗ trợ Content Negotiation để xử lý các loại định dạng dữ liệu khác nhau như JSON, HTML, XML và nhiều hơn nữa. Content Negotiation cho phép bạn nhận và gửi dữ liệu dưới 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 webHello World:

Trong ví dụ trên, chúng ta sử dụng hàm embeddedServer từ module Netty để khởi tạo một máy chủ Ktor Chúng ta định nghĩa một tuyến đường GET "/" để trả về chuỗi "Hello, World!" khi được truy cập Cuối cùng, chúng ta sử dụng start(wait 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ẽ, linh hoạt và dễ sử dụng cho Kotlin Với cú pháp đơn giản, khả năng tích hợp các công nghệ hiện đại và hiệu suất cao, Ktor là một lựa chọn tuyệt vời để xây dựng các ứng dụng web đa nền tảng Với sự hỗ trợ từ cộng đồng phát triển Kotlin, Ktor đang trở thành một trong những lựa chọn phổ biến cho việc phát triển ứng dụng trên nền tảng Kotlin.

XÂY DỰNG ỨNG DỤNG

Tổng quan

1.1 Tên ứng dụng: Yum - Ứng dụng hỗ trợ đầu bếp

1.2 Lý do chọn ứng dụng

Hiện nay thiết bị di dộng smart phone ngày càng phổ biến Việc đưa một ứng dụng lên các kho lưu trữ tương đối dễ dàng Hơn nữa, người sử dụng cũng sẵn sàng chi trả một khoảng tiền phù hợp cho những ứng dụng mà họ cảm thấy cần thiết hơn so với trước đây.

Hiện nay có có rất nhiều ứng dụng hỗ trợ nấu ăn trên nền tảng Android như Cookpad, Tasty, SideChef,… Những ứng dụng này chỉ tập trung vào một tính năng duy nhất đó chính là chia sẻ công thức món ăn

Việc các ứng dụng có thể giúp đỡ người nội trợ có thể tìm kiếm ý tưởng cho món ăn gia đình là cần thiết, tuy vậy vẫn là chưa đủ Những người nội trợ ngoài việc nấu ăn còn phải làm rất nhiều việc khác, và họ dễ dàng quên đi những điều cần thiết ví dụ như phải mua sắm những gì, hay đơn giản hơn là hôm nay cần phải nấu món gì. Đối với người nội trợ, việc có một người trợ thủ có thể giúp đỡ những công việc trên là một điều vô cùng cần thiết Vì thế tôi đã tạo ra chọn đề tài “Ứng dụng hỗ trợ đầu bếp” để giải quyết những vấn đề trên.

Phần mềm tạm gọi với tên “Yum”

1.3 Đối tượng hướng đến Đối tượng của ứng dụng hướng đến người đầu bếp ở mọi cấp độ từ nội trợ đến chuyên nghiệp

- Những người muốn tìm công thức món ăn phù hợp với nhu cầu

- Những người muốn quản lý thời gian cho công việc nấu ăn

- Những người muốn quản lý hiệu quả trong việc mua sắm hàng ngày

- Những người chưa hài lòng với ứng dụng hỗ trợ nấu ăn hiện tại

1.4 Môi trường phát triển ứng dụng

- Hệ điều hành phát triển: Microsoft Windows

- Hệ điều hành cài đặt: Android

- Cơ sở dữ liệu: MongoDB

- Công cụ phân tích thiết kế: Figma

- Công cụ phân chia nhiệm vụ: Notion

- Công cụ xây dựng ứng dụng: Android Studio, Intellij IDEA

- Ngôn ngữ sử dụng: Kotlin

- Công cụ quản lý source code: Github

- Học hỏi cách phát triển một ứng dụng trên di động qua quá trình làm đề tài.

- Phát triển khả năng của phát triển ứng dụng native

- Triển khai được một sản phẩm hoàn thiện, có ích cho người dùng.

- Hiểu được và áp dụng quy trình phát triển phần mềm.

1.6 Quy trình thực hiện các công việc chính

Hiện nay có rất nhiều quy trình phát triển phần mềm khác nhau Tuy nhiên theo yêu cầu của đề tài, nhóm đã sử dụng mô hình thác nước cải tiến Mô hình trên bao gồm các trình tự: xác định yêu cầu, phân tích, thiết kế, cài đặt, kiểm thử, bảo trì. Trong đó, kết quả của giai đoạn trước là cơ sở đầu vào của giai đoạn sau Vì vậy, nếu như có lỗi xảy ra, nhóm có thể quay lui để sửa lỗi và tối ưu phần mềm trong khi tiến độ hiện tại vẫn được duy trì.

Cụ thể các trình tự phát triển phần mềm của nhóm như sau:

- Xác định yêu cầu: Khảo sát yêu cầu người dùng, lập ra bảng các yêu cầu và quy định cụ thể cho phần mềm.

- Phân tích: Phân loại các yêu cầu và lập sơ đồ Use-case

- Thiết kế: Mô tả các thành phần của phần mềm một cách rõ ràng, gồm các bước:

 Thiết kế hệ thống, kiến trúc, các đối tượng.

 Thiết kế cơ sở dữ liệu.

 Cài đặt: Dựa theo những thiết kế và phân tích, tiến hành xây dựng ứng dụng thực tế.

 Kiểm thử: Chạy thực nghiệm và đánh giá, tìm và sửa lỗi.

Phân tích, thiết kế hệ thống

2.1 Xác định và mô hình hóa các yêu cầu phần mềm

2.1.1.1 Một số yêu cầu phần mềm phải có

- Có thể xem, tìm kiếm, xem theo danh mục các công thức món ăn mong muốn

- Có thể xem chi tiết các công thức món ăn: khẩu phần, thời gian nấu, nguyên liệu, cách làm, dinh dưỡng,…

- Có thể xếp hạng, bình luận cho công thức món ăn

- Có thể tạo mới bộ sưu tập món ăn, thêm các công thức món ăn mới

- Có thể tạo, chỉnh sửa, xóa danh sách mua sắm nguyên liệu, có thể tìm kiếm được nguyên liệu cần thiết

- Có thể đặt lịch nấu ăn dự kiến

- Thêm, xem, chỉnh sửa, xóa chi tiêu.

- Đồng bộ dữ liệu tài khoản trên mọi thiết bị đăng nhập.

2.1.1.2 Ràng buộc logic ban đầu

Phải có cơ sở dữ liệu món ăn ban đầu (tự tạo)

Tương thích với hệ điều hành Android 13 trở lên.

Hệ thống phải hoạt động liên tục 24/7.

Xét trong điều kiện mạng ổn định

STT Nghiệp vụ Tốc độ xử lý

1 Đăng kí đăng nhập Không quá 1s

2 Xem công thức món ăn Không quá 1s

3 Quản lý bộ sưu tập Không quá 1s

4 Quản lý danh sách mua sắm Không quá 1s

5 Quản lý lịch nấu ăn Không quá 1s

6 Quản lý tài khoản Không quá 1s

- Hệ thống phải có cơ chế toàn vẹn dữ liệu

- Đảm bảo người dùng không có quyền chỉnh sửa thông tin của người dùng khác hay truy cập vào những dữ liệu chi tiêu không thuộc quyền sở hữu của mình.

2.1.2 Mô hình hóa yêu cầu

- Xem công thức món ăn

- Quản lý bộ sưu tập cá nhân

- Quản lý danh sách mua sắm

- Quản lý lịch nấu ăn

Tên use case Mô tả Đăng nhập Đăng nhập vào hệ thống và sử dụng chức năng tương ứng

Xem công thức món ăn

Người dùng có thể xem danh sách công thức món ăn, chi tiết từng công thức và bình luận

Quản lý bộ sưu tập Người dùng có thể tạo bô sưu tập cá nhân, thêm các công thức món ăn vào bộ sưu tập

Quản lý danh sách mua sắm

Người dùng có thể tạo danh sách mua sắm từ công thức món ăn, sửa các thông số và xem danh sách theo món ăn hoặc danh mục

Quản lý lịch nấu ăn Người dùng có thể lên lịch nấu ăn từ công thức món ăn, thay đổi lịch nấu ăn, nhận thông báo Đặc tả use-case Đặc tả use case đăng nhập Đăng nhập

Tên use case Đăng nhập

Tác nhân sử dụng Người dùng

Mục đích Đăng nhập vào hệ thống và sử dụng chức năng quản lý tương ứng

Mô tả Nhập thông tin tài khoản và mật khẩu, hệ thống ghi nhận, xử lý và trả về kết quả

Tiền điều kiện Người dùng có tài khoản và mật khẩu đăng nhập vào hệ thống, ứng dụng chưa được đăng nhập Kích hoạt Mở phần mềm

3 Nhập thông tin tài khoản và mật khẩu

5 Xử lý thông tin và trả về kết quả

6 Ứng dụng được khởi động lại và mở khóa các chức năng cần thiết

Ngoại lệ Nếu kết quả kiểm tra thông tin đăng nhập là không hợp lệ, chương trình thông báo thông tin đăng nhập không hợp lệ Đăng kí tài khoản

Tên use case Đăng kí

Tác nhân sử dụng Người dùng

Mục đích Đăng kí tài khoản mới sau đó đăng nhập vào hệ thống và sử dụng chức năng quản lý tương ứng

Mô tả Nhập thông tin tài khoản và mật khẩu, hệ thống ghi nhận, xử lý và trả về kết quả Tiền điều kiện Ứng dụng chưa được đăng nhập

Kích hoạt Mở phần mềm

3 Nhập thông tin tài khoản và mật khẩu

5 Xử lý thông tin và trả về kết quả

6 Ứng dụng được khởi động lại và mở khóa các chức năng cần thiết

Ngoại lệ Không có Đặc tả use case quản lý chi tiêu

Xem công thức món ăn

Tên use case Xem công thức món ăn

Tác nhân sử dụng Người dùng

Mục đích Xem công thức món ăn

Mô tả Xem nguyên liệu cần có, cách chế biến, khẩu phần ăn, thời gian nấu của công thức món ăn Tiền điều kiện Không có

Kích hoạt Người dùng nhấn chọn công thức món ăn bất kì

Diễn biến chính 1 Người dùng nhấn chọn công thức món ăn bất kì ở màn hình chính

2 Màn hình chi tiết công thức món ăn được mở ra Diễn biến phụ Không có

Tên use case Tìm kiếm công thức món ăn

Tác nhân sử dụng Người dùng

Mục đích Tìm kiếm công thức món ăn tồn tại trong cơ sở dữ liệu

Mô tả Người dùng tìm kiếm một công thức món ăn nào đó đã có trong cơ sở dữ liệu Tiền điều kiện Món ăn đã có sẵn trong cơ sở dữ liệu

Kích hoạt Người dùng chọn chức năng tìm kiếm ở màn hình tìm kiếm Diễn biến chính 1 Người dùng bấm chọn màn hình tìm kiếm

2 Người dùng nhập tên món ăn cần tìm

3 Món ăn khớp với dữ kiện xuất hiện trên màn hình Diễn biến phụ Không có

Bình luận công thức món ăn

Tên use case Bình luận công thức

Tác nhân sử dụng Người dùng

Mục đích Bình luận và xếp hạng cho một công thức món ăn nào đó

Mô tả Người dùng bình luận và xếp hạng cho một công thức món ăn

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống và chọn một món ăn bất kì

Kích hoạt Người dùng chọn chức năng bình luận ở tab bình luận trong màn hình công thức Diễn biến chính 1 Chọn 1 công thức món ăn bất kì

3 Nhấn nút tạo mới bình luận

4 Viết bình luận và xếp hạng

5 Nhấn nút xác nhận Diễn biến phụ Không có

Ngoại lệ Nếu người dùng chưa đăng nhập, tại bước 2 sẽ thông báo người dùng chưa đăng nhập Đặc tả use case Quản lý bộ sưu tập

Tạo mới bộ sưu tập

Tên use case Tạo mới bộ sưu tập

Tác nhân sử dụng Người dùng

Mục đích Tạo mới một bộ sưu tập

Mô tả Người dùng tạo mới một bộ sưu tập để lưu công thức nấu ăn Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng nhấn nút tạo mới bộ sưu tập trong màn hình Diễn biến chính 1 Chọn màn hình tài khoản

2 Nhấn nút hình dấu cộng

3 Nhập tên bộ sưu tập và nhấn xác nhận Diễn biến phụ Không có

Tạo mới bộ sưu tập

Tên use case Thêm món ăn vào bộ sưu tập

Tác nhân sử dụng Người dùng

Mục đích Thêm món ăn vào bộ sưu tập

Mô tả Người dùng thêm món ăn cần lưu vào bộ sưu tập

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng nhấn nút thêm vào bộ sưu tập ở trang chi tiết món ăn Diễn biến chính 1 Người dùng chọn món ăn cần lưu vào bộ sưu tập

3 Bấm nút thêm vào bộ sưu tập

4 Chọn bộ sưu tập đã tạo muốn lưu vào và bấm nút xác nhận

Diễn biến phụ Không có

Ngoại lệ Không có Đặc tả use case Quản lý danh sách mua sắm

Thêm nguyên liệu vào danh sách mua sắm

Tên use case Thêm nguyên liệu vào danh sách mua sắm

Tác nhân sử dụng Người dùng

Mục đích Thêm nguyên liệu muốn mua vào danh sách mua sắm

Mô tả Người dùng thêm một nguyên liệu muốn mua vào danh sách mua sắmTiền điều kiện Nguyên liệu có sẵn trong cơ sở dữ liệu, người dùng đã đăng nhập Kích hoạt Người dùng chọn chức năng quản lý danh sách mua sắm Diễn biến chính 1 Người dùng nhấn vào màn hình giỏ hàng

2 Nhấn chọn thêm vào danh sách mua hàng

3 Nhập tên nguyên liệu cần thêm

4 Chọn nguyên liệu đã tìm thấy

5 Nguyên liệu được hiển thị trong danh sách mua sắm Diễn biến phụ Không có

Thêm vào danh sách mua sắm từ công thức món ăn

Tên use case Sửa thông tin người dùng

Tác nhân sử dụng Người dùng

Mục đích Thêm toàn bộ nguyên liệu muốn mua từ một công thức món ăn vào danh sách mua sắm

Mô tả Người dùng thêm thêm toàn bộ nguyên liệu muốn mua từ một công thức món ăn vào danh sách mua sắm Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng chọn chức năng quản lý danh sách mua hàng

Diễn biến chính 1 Chọn một công thức món ăn muốn thêm nguyên liệu vào danh sách mua sắm

3 Nhấn nút thêm tất cả vào danh sách mua sắm

4 Các nguyên liệu được thêm vào danh sách mua sắmDiễn biến phụ Không có

Chỉnh sửa thông tin nguyên liệu trong danh sách mua sắm

Tên use case Chỉnh sửa thông tin nguyên liệu trong danh sách mua sắm Tác nhân sử dụng Người dùng

Mục đích Chỉnh sửa thông tin nguyên liệu trong danh sách mua sắm

Mô tả Người dùng chỉnh sửa thông tin số lượng muốn mua, đơn vị hoặc tình trạng của nguyên liệu

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống, nguyên liệu cần chỉnh sửa đã nằm trong danh sách mua sắm Kích hoạt Người dùng chọn chức năng quản lý danh sách mua hàng

Diễn biến chính 1 Người dùng kéo ngang hàng chứa nguyên liệu cần chỉnh sửa

3 Chọn thông số số lượng và đơn vị

5 Nguyên liệu được cập nhật Diễn biến phụ Không có

Xóa nguyên liệu trong danh sách mua sắm

Tên use case Xóa nguyên liệu trong danh sách mua sắm

Tác nhân sử dụng Người dùng

Mục đích Xóa một nguyên liệu không mong muốn khỏi danh sách mua sắm

Mô tả Người dùng xóa một nguyên liệu khỏi danh sách

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống, nguyên liệu cần chỉnh sửa đã nằm trong danh sách mua sắm Kích hoạt Người dùng chọn chức năng quản lý danh sách mua hàng

Diễn biến chính 1 Người dùng kéo ngang hàng chứa nguyên liệu cần chỉnh sửa

3 Nguyên liệu được cập nhật Diễn biến phụ Không có

Sắp xếp nguyên liệu trong danh sách mua sắm

Tên use case Sắp xếp nguyên liệu trong danh sách mua sắm

Tác nhân sử dụng Người dùng

Mục đích Sắp xếp các nguyên liệu theo cách mong muốn

Mô tả Người dùng chọn cách sắp xếp nguyên liệu muốn xem

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống, nguyên liệu cần chỉnh sửa đã nằm trong danh sách mua sắm Kích hoạt Người dùng chọn chức năng quản lý danh sách mua hàng

Diễn biến chính 1 Người dùng chọn chức năng quản lý danh sách mua sắm

2 Nhấn chọn cách sắp xếp nguyên liệu mong muốn

3 Nguyên liệu được sắp xếp theo cách mong muốn Diễn biến phụ Không có

Ngoại lệ Không có Đặc tả use case quản lý lịch nấu ăn

Tên use case Thêm lịch nấu ăn

Tác nhân sử dụng Người dùng

Mục đích Tạo một lịch nấu ăn từ công thức món ăn

Mô tả Người dùng lên lịch nấu ăn từ một công thức món ăn Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng chọn chức năng lịch nấu ăn

Diễn biến chính 1 Người dùng chọn một công thức món ăn muốn lên lịch

3 Chọn nút thêm vào lịch nấu ăn

4 Món ăn đã được lên lịch 1 ngày sau

Diễn biến phụ Không có

Chỉnh sửa lịch nấu ăn

Tên use case Chỉnh sửa lịch nấu ăn

Tác nhân sử dụng Người dùng

Mục đích Thay đổi thời gian nấu

Mô tả Người dùng thay đổi lịch nấu ăn của món ăn đã lên lịch Tiền điều kiện Người dùng đã đăng nhập, món ăn đã được lên lịch Kích hoạt Người dùng chọn chức năng lịch nấu ăn

Diễn biến chính 1 Người dùng chọn tab giỏ hàng

2 Người dùng nhấn vào món ăn đã lên lịch

3 Chọn thời gian muốn chỉnh sửa thành

4 Nhấn xác nhận Diễn biến phụ Không có

Ngoại lệ Không có Đặc tả use case quản lý tài khoản

Thay đổi thông tin tài khoản

Tên use case Thay đổi thông tin tài khoản

Tác nhân sử dụng Người dùng

Mục đích Thay đổi thông tin tài khoản hiện tại

Mô tả Người dùng thay đổi tên và mô tả tài khoản

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng chọn chức năng quản lý tài khoản

Diễn biến chính 1 Người dùng nhấn vào màn hình quản lý tài khoản

3 Nhấn vào thay đổi thông tin

4 Nhập tên người dùng muốn đổi

5 Nhập mô tả muốn đổi

6 Bấm xác nhận Diễn biến phụ Không có

Ngoại lệ Không có Đăng xuất

Tên use case Đăng xuất

Tác nhân sử dụng Người dùng

Mục đích Đăng xuất tài khoản hiện tại

Mô tả Người dùng đăng xuất tài khoản hiện tại

Tiền điều kiện Người dùng đã đăng nhập vào hệ thống

Kích hoạt Người dùng chọn chức năng quản lý tài khoản

Diễn biến chính 1 Người dùng nhấn vào màn hình quản lý tài khoản

4 Ứng dụng khởi động lại Diễn biến phụ Không có

Mô hình client-server là một kiến trúc phân tán trong công nghệ thông tin,trong đó các máy tính hoặc thiết bị giao tiếp với nhau thông qua mạng để thực hiện các nhiệm vụ khác nhau Mô hình này chia thành hai phần chính:

 Client (Khách hàng): Đây là máy tính hoặc thiết bị yêu cầu dịch vụ hoặc tài nguyên từ máy chủ Client có khả năng gửi yêu cầu và nhận kết quả từ máy chủ Một số ví dụ về client có thể là máy tính cá nhân, máy tính xách tay, điện thoại di động hoặc máy tính bảng.

 Server (Máy chủ): Đây là máy tính hoặc thiết bị cung cấp dịch vụ hoặc tài nguyên cho client Máy chủ thường có tài nguyên phần cứng và phần mềm mạnh mẽ để xử lý yêu cầu từ client và cung cấp phản hồi tương ứng Một số ví dụ về server có thể là máy chủ web, máy chủ cơ sở dữ liệu hoặc máy chủ tệp.

Cài đặt và thử nghiệm

Source code: AnBuiii/Yum: Food recipe app using Jetpack Compose and Ktor (github.com)

STT Chức năng Mức độ hoàn thành

2 Xem công thức món ăn 100%

3 Quản lý bộ sưu tập 100%

4 Quản lý danh sách mua sắm 100%

5 Quản lý lịch nấu ăn 100%

- Hiểu rõ Jetpack Compose, xây dựng được một ứng dụng hoàn chỉnh sử dụng Jetpack Compose

- Hiểu rõ phát triển ứng dụng Android native và tận dụng thế mạnh của phát triển ứng dụng native

- Ứng dụng ML (nhận dạng nguyên liệu)

- Phát triển tính năng gợi ý món ăn theo nguyên liệu

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

HÌNH ẢNH LIÊN QUAN

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 - 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 (Trang 14)
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, khiế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, khiến (Trang 15)
Hình 2. Giá trị đại diện của MyComposable trong thành phần Compose. Nếu - 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 38)
Hình 4. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm - Tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 4. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm (Trang 40)
Hình 5. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm - Tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 5. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm (Trang 41)
Hình 6. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm - Tìm hiểu jetpack compose và xây dựng ứng dụng
Hình 6. Giá trị đại diện của MoviesScreen trong thành phần Compose khi thêm (Trang 42)

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

TÀI LIỆU LIÊN QUAN

w