Kiến thức cơ bản về Kotlin cho Android 08.1: Lấy dữ liệu từ Internet

Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Hầu như mọi ứng dụng Android mà bạn tạo đều cần kết nối với Internet tại một thời điểm nào đó. Trong lớp học lập trình này và những lớp học lập trình tiếp theo, bạn sẽ tạo một ứng dụng kết nối với một dịch vụ web để truy xuất và hiển thị dữ liệu. Bạn cũng sẽ áp dụng những kiến thức đã học trong các lớp học lập trình trước đây về ViewModel, LiveDataRecyclerView.

Trong lớp học lập trình này, bạn sẽ dùng các thư viện do cộng đồng phát triển để xây dựng lớp mạng. Việc này giúp đơn giản hoá đáng kể quá trình tìm nạp dữ liệu và hình ảnh, đồng thời giúp ứng dụng tuân thủ một số phương pháp hay nhất của Android, chẳng hạn như tải hình ảnh trên một luồng ở chế độ nền và lưu vào bộ nhớ đệm các hình ảnh đã tải. Đối với các phần không đồng bộ hoặc không chặn trong mã (chẳng hạn như giao tiếp với lớp dịch vụ web), bạn sẽ sửa đổi ứng dụng để sử dụng coroutine của Kotlin. Bạn cũng sẽ cập nhật giao diện người dùng của ứng dụng nếu Internet bị chậm hoặc không hoạt động để cho người dùng biết chuyện gì đang xảy ra.

Kiến thức bạn cần có

  • Cách tạo và sử dụng các mảnh.
  • Cách điều hướng giữa các mảnh và sử dụng safeArgs để truyền dữ liệu giữa các mảnh.
  • Cách sử dụng các thành phần cấu trúc, bao gồm cả ViewModel, ViewModelProvider.Factory, LiveData và các phép biến đổi LiveData.
  • Cách sử dụng coroutine cho các tác vụ chạy trong thời gian dài.

Kiến thức bạn sẽ học được

  • Dịch vụ web REST là gì.
  • Sử dụng thư viện Retrofit để kết nối với một dịch vụ web REST trên Internet và nhận phản hồi.
  • Sử dụng thư viện Moshi để phân tích cú pháp phản hồi JSON thành đối tượng dữ liệu.

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng bắt đầu (starter app) để tạo yêu cầu API dịch vụ web và xử lý phản hồi.
  • Triển khai lớp mạng (network layer) cho ứng dụng bằng cách sử dụng thư viện Retrofit.
  • Phân tích cú pháp phản hồi JSON qua dịch vụ web thành dữ liệu trực tiếp của ứng dụng bằng thư viện Moshi.
  • Sử dụng tính năng hỗ trợ của Retrofit dành cho coroutine để đơn giản hoá mã.

Trong lớp học lập trình này (và các lớp học lập trình tiếp theo), bạn sẽ làm việc với một ứng dụng khởi đầu có tên là MarsRealEstate, ứng dụng này đăng tin rao bán bất động sản trên sao Hoả. Ứng dụng này kết nối với một dịch vụ web để truy xuất và hiển thị dữ liệu về tài sản, bao gồm cả các thông tin chi tiết như giá và việc tài sản có đang được bán hoặc cho thuê hay không. Những hình ảnh đại diện cho từng đối tượng là ảnh chụp sao Hoả thực tế được chụp qua thiết bị thám hiểm sao Hoả của NASA.

Phiên bản ứng dụng bạn xây dựng trong lớp học lập trình này sẽ không có nhiều hình ảnh trực quan, vì phiên bản này tập trung vào lớp kết nối mạng của ứng dụng để kết nối Internet và tải dữ liệu thuộc tính thô bằng dịch vụ web. Để đảm bảo dữ liệu được truy xuất và phân tích cú pháp chính xác, bạn sẽ chỉ in số lượng cơ sở lưu trú trên sao Hoả trong một chế độ xem văn bản:

.

Cấu trúc của ứng dụng MarsRealEstate có 2 mô-đun chính:

  • Một mảnh tổng quan chứa lưới hình ảnh thu nhỏ của các thuộc tính, được tạo bằng RecyclerView.
  • Một mảnh khung hiển thị chi tiết, chứa thông tin về từng thuộc tính.

Ứng dụng có một ViewModel cho mỗi mảnh. Trong lớp học lập trình này, bạn sẽ tạo một lớp cho dịch vụ mạng và ViewModel sẽ giao tiếp trực tiếp với lớp mạng đó. Điều này tương tự như những gì bạn đã làm trong các lớp học lập trình trước khi ViewModel giao tiếp với cơ sở dữ liệu Room.

Tổng quan ViewModel chịu trách nhiệm thực hiện lệnh gọi qua mạng để lấy thông tin về bất động sản trên sao Hoả. ViewModel chi tiết lưu giữ thông tin chi tiết cho một mảnh đất duy nhất trên sao Hoả xuất hiện trong mảnh chi tiết. Đối với mỗi ViewModel, bạn sử dụng LiveData với tính năng liên kết dữ liệu có nhận biết vòng đời để cập nhật giao diện người dùng ứng dụng khi dữ liệu thay đổi.

Bạn dùng thành phần Điều hướng để vừa di chuyển giữa hai mảnh, vừa truyền thuộc tính đã chọn dưới dạng một đối số.

Trong nhiệm vụ này, bạn sẽ tải và chạy ứng dụng khởi đầu cho MarsRealEstate, đồng thời làm quen với cấu trúc của dự án.

Bước 1: Khám phá các mảnh và thành phần điều hướng

  1. Tải ứng dụng khởi đầu MarsRealEstate xuống rồi mở ứng dụng này trong Android Studio.
  2. Kiểm tra app/java/MainActivity.kt. Ứng dụng này dùng các mảnh cho cả hai màn hình, nên nhiệm vụ duy nhất của hoạt động là tải bố cục của hoạt động.
  3. Kiểm tra app/res/layout/activity_main.xml. Bố cục hoạt động là máy chủ lưu trữ cho 2 mảnh, được xác định trong tệp điều hướng. Bố cục này tạo một NavHostFragment và bộ điều khiển điều hướng được liên kết với bố cục đó bằng tài nguyên nav_graph.
  4. Mở app/res/navigation/nav_graph.xml. Tại đây, bạn có thể thấy mối quan hệ điều hướng giữa hai mảnh. Biểu đồ điều hướng StartDestination trỏ đến overviewFragment, vì vậy, mảnh tổng quan sẽ được tạo thực thể khi ứng dụng khởi chạy.

Bước 2: Khám phá các tệp nguồnvà tính năng liên kết dữ liệu của Kotlin

  1. Trong ngăn Project (Dự án), hãy mở rộng app > java. Lưu ý rằng ứng dụng MarsRealEstate có 3 thư mục gói: detail, networkoverview. Các thành phần này tương ứng với 3 thành phần chính của ứng dụng: đoạn tổng quan và đoạn chi tiết, cũng như mã cho lớp mạng.
  2. Mở app/java/overview/OverviewFragment.kt. OverviewFragment khởi tạo OverviewViewModel một cách từng phần, tức là OverviewViewModel được tạo vào lần đầu tiên được sử dụng.
  3. Kiểm tra phương thức onCreateView(). Phương thức này tăng cường bố cục fragment_overview bằng cách sử dụng tính năng liên kết dữ liệu, thiết lập chủ sở hữu vòng đời liên kết với chính nó (this) và đặt giá trị cho biến viewModel trong đối tượng binding. Vì chúng ta đã đặt chủ sở hữu vòng đời, nên mọi LiveData được dùng trong tính năng liên kết dữ liệu sẽ tự động được theo dõi để phát hiện mọi thay đổi và giao diện người dùng sẽ được cập nhật theo đó.
  4. Mở app/java/overview/OverviewViewModel. Vì phản hồi là một LiveData và chúng ta đã đặt vòng đời cho biến liên kết, nên mọi thay đổi đối với biến này sẽ cập nhật giao diện người dùng của ứng dụng.
  5. Kiểm tra khối init. Khi ViewModel được tạo, phương thức này sẽ gọi phương thức getMarsRealEstateProperties().
  6. Kiểm tra phương thức getMarsRealEstateProperties(). Trong ứng dụng khởi động này, phương thức này chứa một phản hồi của phần giữ chỗ. Mục tiêu của lớp học lập trình này là cập nhật LiveData phản hồi trong ViewModel bằng cách sử dụng dữ liệu thực mà bạn lấy được trên Internet.
  7. Mở app/res/layout/fragment_overview.xml. Đây là bố cục cho mảnh tổng quan mà bạn sẽ làm việc trong lớp học lập trình này, bao gồm cả tính năng liên kết dữ liệu cho mô hình hiển thị. Thao tác này nhập OverviewViewModel, sau đó liên kết phản hồi từ ViewModel với TextView. Trong các lớp học lập trình sau này, bạn sẽ thay thế khung hiển thị văn bản bằng một lưới hình ảnh trong RecyclerView.
  8. Biên dịch và chạy ứng dụng. Trong phiên bản hiện tại của ứng dụng này, bạn chỉ thấy phản hồi khởi động "Set the Mars API Response here!" (Đặt Phản hồi API của Sao Hoả tại đây!).

Dữ liệu bất động sản trên sao Hoả được lưu trữ trên một máy chủ web dưới dạng dịch vụ web REST. Các dịch vụ web sử dụng kiến trúc REST được xây dựng bằng các giao thức và thành phần web tiêu chuẩn.

Thông qua URI, bạn sẽ đưa ra yêu cầu cho một dịch vụ web theo cách được chuẩn hoá. URL web mà bạn hay thấy thực chất là một loại URI và cả hai đều được dùng thay thế cho nhau trong suốt khoá học này. Ví dụ: trong ứng dụng dành cho bài học này, bạn truy xuất tất cả dữ liệu từ máy chủ sau:

https://android-kotlin-fun-mars-server.appspot.com

Nếu nhập URL sau đây vào trình duyệt, bạn sẽ nhận được danh sách tất cả tài sản bất động sản hiện có trên sao Hoả!

https://android-kotlin-fun-mars-server.appspot.com/realestate

Phản hồi của một dịch vụ web thường có định dạng JSON, một định dạng trao đổi để trình bày dữ liệu có cấu trúc. Bạn có thể tìm hiểu thêm về JSON trong nhiệm vụ tiếp theo, nhưng giải thích ngắn gọn là đối tượng JSON là một tập hợp các cặp khoá-giá trị, đôi khi được gọi là từ điển, bảng băm hoặc mảng kết hợp. Một tập hợp đối tượng JSON là một mảng JSON và đó là mảng bạn nhận được dưới dạng phản hồi qua một dịch vụ web.

Để đưa dữ liệu này vào ứng dụng, ứng dụng của bạn cần thiết lập một kết nối mạng và giao tiếp với máy chủ đó, sau đó nhận và phân tích cú pháp dữ liệu phản hồi thành một định dạng mà ứng dụng có thể sử dụng. Trong lớp học lập trình này, bạn sẽ dùng một thư viện ứng dụng REST có tên là Retrofit để thiết lập kết nối này.

Bước 1: Thêm phần phụ thuộc Retrofit vào Gradle

  1. Mở build.gradle (Module: app).
  2. Trong phần dependencies, hãy thêm các dòng sau cho các thư viện của Retrofit:
implementation "com.squareup.retrofit2:retrofit:$version_retrofit"
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"


Xin lưu ý rằng số phiên bản được xác định riêng trong tệp Gradle của dự án. Phần phụ thuộc đầu tiên là dành cho chính thư viện Retrofit 2 và phần phụ thuộc thứ hai là dành cho bộ chuyển đổi vô hướng Retrofit. Bộ chuyển đổi này cho phép Retrofit trả về kết quả JSON dưới dạng String. Cả hai thư viện hoạt động cùng nhau.

  1. Nhấp vào Sync Now (Đồng bộ hoá ngay) để xây dựng lại dự án với phần phụ thuộc mới.

Bước 2: Triển khai MarsApiService

Retrofit tạo một API mạng cho ứng dụng dựa trên nội dung từ dịch vụ web. Retrofit tìm nạp dữ liệu qua dịch vụ web và định tuyến dữ liệu này thông qua một thư viện chuyển đổi riêng biệt biết cách giải mã và trả lại dữ liệu dưới dạng các đối tượng hữu ích. Retrofit có tính năng hỗ trợ tích hợp cho các định dạng dữ liệu web phổ biến như XML và JSON. Cuối cùng, Retrofit sẽ tạo hầu hết lớp mạng cho bạn, bao gồm cả các thông tin chi tiết quan trọng như chạy các yêu cầu trên các luồng ở chế độ nền.

Lớp MarsApiService chứa lớp mạng cho ứng dụng; tức là đây là API mà ViewModel sẽ dùng để giao tiếp với dịch vụ web. Đây là lớp mà bạn sẽ triển khai API của dịch vụ Retrofit.

  1. Mở app/java/network/MarsApiService.kt. Hiện tại, tệp này chỉ chứa một thứ: một hằng số cho URL cơ sở của dịch vụ web.
private const val BASE_URL = 
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. Ngay bên dưới hằng số đó, hãy dùng trình tạo Retrofit để tạo một đối tượng Retrofit. Nhập retrofit2.Retrofitretrofit2.converter.scalars.ScalarsConverterFactory khi được yêu cầu.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()

Retrofit cần ít nhất 2 yếu tố để xây dựng API dịch vụ web: URI cơ sở dành cho dịch vụ web và một nhà máy chuyển đổi. Bộ chuyển đổi sẽ cho Retrofit biết cần làm gì với dữ liệu nhận được qua dịch vụ web. Trong trường hợp này, bạn muốn Retrofit tìm nạp phản hồi JSON qua dịch vụ web và trả về dưới dạng String. Retrofit có một ScalarsConverter hỗ trợ chuỗi và các kiểu dữ liệu chính khác, vì vậy bạn có thể gọi addConverterFactory() trên trình tạo bằng một thực thể của ScalarsConverterFactory. Cuối cùng, bạn gọi build() để tạo đối tượng Retrofit.

  1. Ngay bên dưới lệnh gọi tới trình tạo Retrofit, hãy xác định một giao diện xác định cách Retrofit giao tiếp với máy chủ web thông qua các yêu cầu HTTP. Nhập retrofit2.http.GETretrofit2.Call khi được yêu cầu.
interface MarsApiService {
    @GET("realestate")
    fun getProperties():
            Call<String>
}

Hiện tại, mục tiêu là lấy chuỗi phản hồi JSON qua dịch vụ web và bạn chỉ cần một phương thức để làm việc đó: getProperties(). Để cho Retrofit biết phương thức này sẽ làm gì, hãy sử dụng chú thích @GET và chỉ định đường dẫn hoặc điểm cuối cho phương thức dịch vụ web đó. Trong trường hợp này, điểm cuối có tên là realestate. Khi phương thức getProperties() được gọi, Retrofit sẽ thêm điểm cuối realestate vào URL cơ sở (mà bạn đã xác định trong trình tạo Retrofit) và tạo một đối tượng Call. Đối tượng Call đó được dùng để bắt đầu yêu cầu.

  1. Bên dưới giao diện MarsApiService, hãy xác định một đối tượng công khai có tên là MarsApi để khởi động dịch vụ Retrofit.
object MarsApi {
    val retrofitService : MarsApiService by lazy { 
       retrofit.create(MarsApiService::class.java) }
}

Phương thức create() của Retrofit sẽ tạo chính dịch vụ Retrofit bằng giao diện MarsApiService. Vì lệnh gọi này tốn kém và ứng dụng chỉ cần một thực thể dịch vụ Retrofit, nên bạn sẽ hiển thị dịch vụ này cho phần còn lại của ứng dụng bằng cách sử dụng một đối tượng công khai có tên là MarsApi và khởi động dịch vụ Retrofit một cách chậm trễ tại đó. Giờ đây, khi bạn đã hoàn tất mọi bước thiết lập, mỗi khi ứng dụng của bạn gọi MarsApi.retrofitService, ứng dụng sẽ nhận được một đối tượng Retrofit singleton triển khai MarsApiService.

Bước 3: Gọi dịch vụ web trong OverviewViewModel

  1. Mở app/java/overview/OverviewViewModel.kt. Di chuyển xuống phương thức getMarsRealEstateProperties().
private fun getMarsRealEstateProperties() {
   _response.value = "Set the Mars API Response here!"
}

Đây là phương thức mà bạn sẽ gọi dịch vụ Retrofit và xử lý chuỗi JSON được trả về. Hiện tại, chỉ có một chuỗi giữ chỗ cho phản hồi.

  1. Xoá dòng giữ chỗ đặt phản hồi thành "Set the Mars API Response here!" (Đặt Phản hồi API của Sao Hoả tại đây!)
  2. Bên trong getMarsRealEstateProperties(), hãy thêm mã như minh hoạ bên dưới. Nhập retrofit2.Callbackcom.example.android.marsrealestate.network.MarsApi khi được yêu cầu.

    Phương thức MarsApi.retrofitService.getProperties() trả về một đối tượng Call. Sau đó, bạn có thể gọi enqueue() trên đối tượng đó để bắt đầu yêu cầu mạng trên một luồng ở chế độ nền.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<String> {
})
  1. Nhấp vào từ object được gạch chân bằng màu đỏ. Chọn Code > Implement methods (Mã > Triển khai phương thức). Chọn cả onResponse()onFailure() trong danh sách.


    Android Studio sẽ thêm mã có các TODO trong mỗi phương thức:
override fun onFailure(call: Call<String>, t: Throwable) {
       TODO("not implemented") 
}

override fun onResponse(call: Call<String>, 
   response: Response<String>) {
       TODO("not implemented") 
}
  1. Trong onFailure(), hãy xoá TODO và đặt _response thành thông báo lỗi, như minh hoạ bên dưới. _response là một chuỗi LiveData xác định nội dung hiển thị trong khung hiển thị văn bản. Mỗi trạng thái cần cập nhật _response LiveData.

    Lệnh gọi lại onFailure() sẽ được gọi khi phản hồi của dịch vụ web không thành công. Đối với phản hồi này, hãy đặt trạng thái _response thành "Failure: " được nối với thông báo từ đối số Throwable.
override fun onFailure(call: Call<String>, t: Throwable) {
   _response.value = "Failure: " + t.message
}
  1. Trong onResponse(), hãy xoá TODO và đặt _response thành nội dung phản hồi. Lệnh gọi lại onResponse() được gọi khi yêu cầu thành công và dịch vụ web trả về một phản hồi.
override fun onResponse(call: Call<String>, 
   response: Response<String>) {
      _response.value = response.body()
}

Bước 4: Xác định quyền truy cập Internet

  1. Biên dịch và chạy ứng dụng MarsRealEstate. Lưu ý rằng ứng dụng đóng ngay lập tức và gặp lỗi.
  2. Nhấp vào thẻ Logcat trong Android Studio rồi ghi chú lỗi trong nhật ký, bắt đầu bằng một dòng như thế này:
Process: com.example.android.marsrealestate, PID: 10646
java.lang.SecurityException: Permission denied (missing INTERNET permission?)

Thông báo lỗi cho biết có thể ứng dụng của bạn bị thiếu quyền INTERNET. Kết nối Internet dẫn đến nhiều mối lo ngại về bảo mật, do đó, các ứng dụng đều không có kết nối Internet theo mặc định. Bạn cần phải cho Android biết rõ rằng ứng dụng cần truy cập Internet.

  1. Mở app/manifests/AndroidManifest.xml. Thêm dòng này ngay trước thẻ <application>:
<uses-permission android:name="android.permission.INTERNET" />
  1. Biên dịch và chạy lại ứng dụng. Nếu mọi thứ đều hoạt động bình thường với kết nối Internet của bạn, bạn sẽ thấy văn bản JSON chứa dữ liệu về Tài sản trên sao Hoả.
  2. Nhấn vào nút Back (Quay lại) trong thiết bị hoặc trình mô phỏng để đóng ứng dụng.
  3. Chuyển thiết bị hoặc trình mô phỏng sang chế độ trên máy bay, sau đó mở lại ứng dụng trong trình đơn Gần đây hoặc khởi động lại ứng dụng trong Android Studio.


  1. Tắt lại chế độ trên máy bay.

Lúc này, bạn đã nhận được phản hồi JSON qua dịch vụ web sao Hoả. Đây là một khởi đầu tuyệt vời. Nhưng những gì bạn thực sự cần là các đối tượng Kotlin, không phải là một chuỗi JSON lớn. Có một thư viện tên là Moshi, đây là một trình phân tích cú pháp JSON cho Android để chuyển đổi chuỗi JSON thành các đối tượng Kotlin. Retrofit có một bộ chuyển đổi dùng được với Moshi, vì vậy đây là thư viện tuyệt vời dành cho mục đích hiện tại của bạn.

Trong nhiệm vụ này, bạn sử dụng thư viện Moshi cùng Retrofit để phân tích cú pháp phản hồi JSON qua dịch vụ web thành các đối tượng Kotlin hữu ích biểu thị thông tin về tài sản trên sao Hoả. Bạn thay đổi ứng dụng để hiện số lượng thuộc tính sao Hoả được trả về (thay vì cho thấy JSON thô).

Bước 1: Thêm phần phụ thuộc của thư viện Moshi

  1. Mở build.gradle (Module: app).
  2. Trong phần phụ thuộc, hãy thêm mã dưới đây để thêm các phần phụ thuộc Moshi. Tương tự như Retrofit, $version_moshi được xác định riêng trong tệp Gradle cấp dự án. Các phần phụ thuộc này hỗ trợ thêm cho thư viện Moshi JSON cốt lõi và cho khả năng hỗ trợ Kotlin của Moshi.
implementation "com.squareup.moshi:moshi:$version_moshi"
implementation "com.squareup.moshi:moshi-kotlin:$version_moshi"
  1. Xác định vị trí dòng dành cho trình chuyển đổi vô hướng Retrofit trong khối dependencies:
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"
  1. Thay đổi dòng đó để sử dụng converter-moshi:
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
  1. Nhấp vào Sync Now (Đồng bộ hoá ngay) để xây dựng lại dự án với phần phụ thuộc mới.

Bước 2: Triển khai lớp dữ liệu MarsProperty

Mục mẫu của phản hồi JSON mà bạn nhận được qua dịch vụ web có dạng như sau:

[{"price":450000,
"id":"424906",
"type":"rent",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"},
...]

Phản hồi JSON ở trên là một mảng, được biểu thị bằng các dấu ngoặc vuông. Mảng này chứa các đối tượng JSON được đặt trong dấu ngoặc nhọn. Mỗi đối tượng chứa một tập hợp các cặp tên-giá trị, được phân tách bằng dấu hai chấm. Tên được đặt trong dấu ngoặc kép. Giá trị có thể là số hoặc chuỗi, đồng thời chuỗi cũng được đặt trong dấu ngoặc kép. Ví dụ: price cho tài sản này là 4.500.000.000 VND và img_src là một URL, đây là vị trí của tệp hình ảnh trên máy chủ.

Trong ví dụ trên, lưu ý rằng mỗi mục tài sản trên sao Hoả có các cặp giá trị và khoá JSON như sau:

  • price: giá của tài sản trên Sao Hoả, ở dạng số.
  • id: mã nhận dạng thuộc tính ở dạng chuỗi.
  • type: "rent" hoặc "buy".
  • img_src: URL của hình ảnh dưới dạng chuỗi.

Moshi phân tích cú pháp dữ liệu JSON này và chuyển đổi thành các đối tượng Kotlin. Để làm việc này, bạn cần có một lớp dữ liệu Kotlin để lưu trữ các kết quả đã phân tích cú pháp. Do đó, bước tiếp theo là tạo lớp đó.

  1. Mở app/java/network/MarsProperty.kt.
  2. Thay thế định nghĩa lớp MarsProperty hiện có bằng mã sau:
data class MarsProperty(
   val id: String, val img_src: String,
   val type: String,
   val price: Double
)

Hãy lưu ý rằng mỗi biến trong lớp MarsProperty tương ứng với một tên khoá trong đối tượng JSON. Để khớp với các kiểu trong JSON, bạn sử dụng đối tượng String cho tất cả các giá trị, ngoại trừ price. Đây là một Double. Bạn có thể dùng Double để biểu thị bất kỳ số JSON nào.

Khi phân tích cú pháp JSON, Moshi sẽ so khớp các khoá theo tên rồi điền thông tin thích hợp vào đối tượng dữ liệu.

  1. Thay thế dòng dành cho khoá img_src bằng dòng dưới đây. Nhập com.squareup.moshi.Json khi có yêu cầu.
@Json(name = "img_src") val imgSrcUrl: String,

Đôi khi tên khoá trong phản hồi JSON có thể làm cho các thuộc tính Kotlin nhầm lẫn hoặc không khớp với kiểu mã hoá của bạn. Ví dụ: trong tệp JSON, khoá img_src sử dụng dấu gạch dưới, trong khi các thuộc tính Kotlin thường sử dụng chữ hoa và chữ thường ("kiểu lạc đà").

Để sử dụng tên biến trong lớp dữ liệu khác với tên khoá trong phản hồi JSON, hãy sử dụng chú thích @Json. Trong ví dụ này, tên của biến trong lớp dữ liệu là imgSrcUrl. Biến này được liên kết với thuộc tính JSON img_src bằng @Json(name = "img_src").

Bước 3: Cập nhật MarsApiService và OverviewViewModel

Khi đã có lớp dữ liệu MarsProperty, giờ đây, bạn có thể cập nhật API mạng và ViewModel để thêm dữ liệu Moshi.

  1. Mở network/MarsApiService.kt. Bạn có thể thấy lỗi thiếu lớp cho ScalarsConverterFactory. Lý do chính là sự thay đổi về phần phụ thuộc Retrofit mà bạn thực hiện ở Bước 1. Bạn sẽ sớm khắc phục những lỗi đó.
  2. Ở đầu tệp, ngay trước trình tạo Retrofit, hãy thêm mã sau để tạo thực thể Moshi. Nhập com.squareup.moshi.Moshicom.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory khi được yêu cầu.
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

Tương tự như những gì bạn đã làm với Retrofit, ở đây bạn sẽ tạo một đối tượng moshi bằng trình tạo Moshi. Để chú thích của Moshi dùng được với Kotlin, hãy thêm KotlinJsonAdapterFactory, sau đó gọi build().

  1. Thay đổi trình tạo Retrofit để dùng MoshiConverterFactory thay vì ScalarConverterFactory, rồi truyền vào thực thể moshi mà bạn vừa tạo. Nhập retrofit2.converter.moshi.MoshiConverterFactory khi có yêu cầu.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()
  1. Xoá cả dữ liệu đã nhập cho ScalarConverterFactory.

Đoạn mã cần xoá:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. Hãy cập nhật giao diện MarsApiService để Retrofit trả về một danh sách đối tượng MarsProperty, thay vì trả về Call<String>.
interface MarsApiService {
   @GET("realestate")
   fun getProperties():
      Call<List<MarsProperty>>
}
  1. Mở OverviewViewModel.kt. Di chuyển xuống lệnh gọi đến getProperties().enqueue() trong phương thức getMarsRealEstateProperties().
  2. Thay đổi đối số thành enqueue() từ Callback<String> thành Callback<List<MarsProperty>>. Nhập com.example.android.marsrealestate.network.MarsProperty khi có yêu cầu.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<List<MarsProperty>> {
  1. Trong onFailure(), hãy thay đổi đối số từ Call<String> thành Call<List<MarsProperty>>:
override fun onFailure(call: Call<List<MarsProperty>>, t: Throwable) {
  1. Thực hiện cùng một thay đổi đối với cả hai đối số thành onResponse():
override fun onResponse(call: Call<List<MarsProperty>>, 
   response: Response<List<MarsProperty>>) {
  1. Trong nội dung của onResponse(), hãy thay thế giá trị được chỉ định hiện có cho _response.value bằng giá trị được chỉ định như dưới đây. Vì response.body() hiện là danh sách các đối tượng MarsProperty, nên kích thước của danh sách đó là số lượng thuộc tính đã được phân tích cú pháp. Thông báo phản hồi này in số lượng cơ sở lưu trú đó:
_response.value = 
   "Success: ${response.body()?.size} Mars properties retrieved"
  1. Đảm bảo chế độ trên máy bay đang tắt. Biên dịch và chạy ứng dụng. Lần này thông báo sẽ cho thấy số lượng thuộc tính được trả về qua dịch vụ web:

Giờ đây, dịch vụ API Retrofit đang chạy, nhưng dịch vụ này sử dụng một lệnh gọi lại với 2 phương thức gọi lại mà bạn phải triển khai. Một phương thức xử lý thành công và một phương thức khác xử lý thất bại, đồng thời kết quả thất bại sẽ báo cáo các trường hợp ngoại lệ. Mã của bạn sẽ hiệu quả hơn và dễ đọc hơn nếu bạn có thể sử dụng các coroutine có tính năng xử lý ngoại lệ, thay vì sử dụng các lệnh gọi lại. Retrofit có một thư viện tích hợp coroutine.

Trong nhiệm vụ này, bạn sẽ chuyển đổi dịch vụ mạng và ViewModel để sử dụng các coroutine.

Bước 1: Thêm các phần phụ thuộc của coroutine

  1. Mở build.gradle (Module: app).
  2. Trong phần phụ thuộc, hãy thêm hỗ trợ cho các thư viện coroutine Kotlin cốt lõi và thư viện coroutine Retrofit:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines"

implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter"
  1. Nhấp vào Sync Now (Đồng bộ hoá ngay) để xây dựng lại dự án với phần phụ thuộc mới.

Bước 2: Cập nhật MarsApiService và OverviewViewModel

  1. Trong MarsApiService.kt, hãy cập nhật trình tạo Retrofit để sử dụng CoroutineCallAdapterFactory. Trình tạo đầy đủ giờ đây có dạng như sau:
private val retrofit = Retrofit.Builder()
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .baseUrl(BASE_URL)
        .build()

Trình chuyển đổi lệnh gọi bổ sung khả năng cho Retrofit tạo các API trả về một giá trị khác ngoài lớp Call mặc định. Trong trường hợp này, CoroutineCallAdapterFactory cho phép chúng ta thay thế đối tượng CallgetProperties() trả về bằng đối tượng Deferred.

  1. Trong phương thức getProperties(), hãy thay đổi Call<List<MarsProperty>> thành Deferred<List<MarsProperty>>. Nhập kotlinx.coroutines.Deferred khi có yêu cầu. Phương thức getProperties() đầy đủ sẽ có dạng như sau:
@GET("realestate")
fun getProperties():
   Deferred<List<MarsProperty>>

Giao diện Deferred xác định một tác vụ coroutine trả về giá trị kết quả (Deferred kế thừa từ Job). Giao diện Deferred bao gồm một phương thức có tên là await(). Phương thức này khiến mã của bạn chờ mà không bị chặn cho đến khi giá trị sẵn sàng, sau đó giá trị đó sẽ được trả về.

  1. Mở OverviewViewModel.kt. Ngay trước khối init, hãy thêm một tác vụ coroutine:
private var viewModelJob = Job()
  1. Tạo một phạm vi coroutine cho tác vụ mới đó bằng trình điều phối chính:
private val coroutineScope = CoroutineScope(
   viewModelJob + Dispatchers.Main )

Trình điều phối Dispatchers.Main sử dụng luồng giao diện người dùng cho công việc của mình. Vì Retrofit thực hiện mọi hoạt động trên một luồng nền, nên không có lý do gì để sử dụng bất kỳ luồng nào khác cho phạm vi này. Nhờ đó, bạn có thể dễ dàng cập nhật giá trị của MutableLiveData khi nhận được kết quả.

  1. Xoá toàn bộ mã bên trong getMarsRealEstateProperties(). Bạn sẽ sử dụng coroutine ở đây thay vì lệnh gọi đến enqueue() và các lệnh gọi lại onFailure()onResponse().
  2. Bên trong getMarsRealEstateProperties(), hãy khởi chạy coroutine:
coroutineScope.launch { 

}


Để sử dụng đối tượng Deferred mà Retrofit trả về cho tác vụ mạng, bạn phải ở trong một coroutine. Vì vậy, ở đây, bạn sẽ chạy coroutine mà bạn vừa tạo. Bạn vẫn đang thực thi mã trên luồng chính, nhưng giờ đây, bạn đang cho phép các coroutine quản lý cơ chế xử lý đồng thời.

  1. Bên trong khối khởi động, hãy gọi getProperties() trên đối tượng retrofitService:
var getPropertiesDeferred = MarsApi.retrofitService.getProperties()

Việc gọi getProperties() từ dịch vụ MarsApi sẽ tạo và bắt đầu lệnh gọi mạng trên một luồng ở chế độ nền, trả về đối tượng Deferred cho tác vụ đó.

  1. Cũng bên trong khối khởi chạy, hãy thêm khối try/catch để xử lý các ngoại lệ:
try {

} catch (e: Exception) {
  
}
  1. Trong khối try {}, hãy gọi await() trên đối tượng Deferred:
var listResult = getPropertiesDeferred.await()

Khi gọi await() trên đối tượng Deferred, kết quả sẽ được trả về từ lệnh gọi mạng khi giá trị đã sẵn sàng. Phương thức await() không chặn, vì vậy, dịch vụ Mars API sẽ truy xuất dữ liệu từ mạng mà không chặn luồng hiện tại. Điều này rất quan trọng vì chúng ta đang trong phạm vi của luồng giao diện người dùng. Sau khi tác vụ hoàn tất, mã của bạn sẽ tiếp tục thực thi từ nơi nó dừng lại. Đây là bên trong try {} để bạn có thể bắt các trường hợp ngoại lệ.

  1. Cũng trong khối try {}, sau phương thức await(), hãy cập nhật thông báo phản hồi cho phản hồi thành công:
_response.value = 
   "Success: ${listResult.size} Mars properties retrieved"
  1. Bên trong khối catch {}, hãy xử lý phản hồi lỗi:
_response.value = "Failure: ${e.message}"


Phương thức getMarsRealEstateProperties() hoàn chỉnh lúc này sẽ có dạng như sau:

private fun getMarsRealEstateProperties() {
   coroutineScope.launch {
       var getPropertiesDeferred = 
          MarsApi.retrofitService.getProperties()
       try {          
           _response.value = 
              "Success: ${listResult.size} Mars properties retrieved"
       } catch (e: Exception) {
           _response.value = "Failure: ${e.message}"
       }
   }
}
  1. Ở cuối lớp, hãy thêm một lệnh gọi lại onCleared() bằng mã sau:
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}

Quá trình tải dữ liệu sẽ dừng khi ViewModel bị huỷ, vì OverviewFragment sử dụng ViewModel này sẽ biến mất. Để ngừng tải khi ViewModel bị huỷ, hãy ghi đè onCleared() để huỷ công việc.

  1. Biên dịch và chạy ứng dụng. Lần này, bạn sẽ nhận được kết quả tương tự như trong nhiệm vụ trước (báo cáo về số lượng thuộc tính), nhưng có mã và quy trình xử lý lỗi đơn giản hơn.

Dự án Android Studio: MarsRealEstateNetwork

Dịch vụ web REST

  • Dịch vụ web là một dịch vụ trên Internet giúp ứng dụng của bạn đưa ra yêu cầu và nhận về dữ liệu.
  • Các dịch vụ web phổ biến sử dụng kiến trúc REST. Các dịch vụ web cung cấp kiến trúc REST được gọi là dịch vụ RESTful. Các dịch vụ web RESTful được xây dựng bằng các giao thức và thành phần web tiêu chuẩn.
  • Thông qua URI, bạn sẽ đưa ra yêu cầu cho một dịch vụ web REST theo cách chuẩn hoá.
  • Để sử dụng dịch vụ web, ứng dụng phải thiết lập một kết nối mạng và kết nối với dịch vụ này. Sau đó, ứng dụng phải nhận và phân tích cú pháp dữ liệu phản hồi thành định dạng mà ứng dụng sử dụng được.
  • Thư viện Retrofit là một thư viện ứng dụng khách cho phép ứng dụng của bạn đưa ra yêu cầu đến một dịch vụ web REST.
  • Sử dụng các bộ chuyển đổi để cho Retrofit biết cần làm gì với dữ liệu mà ứng dụng gửi đến và nhận về qua dịch vụ web. Ví dụ: trình chuyển đổi ScalarsConverter coi dữ liệu dịch vụ web là String hoặc một kiểu dữ liệu gốc khác.
  • Để cho phép ứng dụng tạo kết nối Internet, hãy thêm quyền "android.permission.INTERNET" vào tệp kê khai Android.

Phân tích cú pháp JSON

  • Phản hồi qua dịch vụ web thường có định dạng JSON, một định dạng trao đổi phổ biến để trình bày dữ liệu có cấu trúc.
  • Mỗi đối tượng JSON là một tập hợp cặp khoá-giá trị. Tập hợp này đôi khi được gọi là từ điển, bảng băm hoặc mảng kết hợp.
  • Một tập hợp đối tượng JSON là một mảng JSON. Bạn nhận được một mảng JSON dưới dạng phản hồi qua một dịch vụ web.
  • Khoá trong cặp khoá-giá trị được đặt trong dấu ngoặc kép. Giá trị có thể là số hoặc chuỗi. Các chuỗi cũng được đặt trong dấu ngoặc kép.
  • Thư viện Moshi là trình phân tích cú pháp JSON cho Android, giúp chuyển đổi chuỗi JSON thành các đối tượng Kotlin. Retrofit có một bộ chuyển đổi dành cho Moshi.
  • Moshi đối sánh các khoá trong phản hồi JSON với các thuộc tính trong đối tượng dữ liệu cùng tên.
  • Để sử dụng tên thuộc tính khác cho một khoá, hãy chú thích thuộc tính đó bằng chú thích @Json và tên khoá JSON.

Retrofit và coroutine

  • Trình chuyển đổi lệnh gọi cho phép Retrofit tạo các API trả về một giá trị khác ngoài lớp Call mặc định. Sử dụng lớp CoroutineCallAdapterFactory để thay thế Call bằng một coroutine Deferred.
  • Sử dụng phương thức await() trên đối tượng Deferred để khiến mã coroutine của bạn chờ mà không bị chặn cho đến khi giá trị sẵn sàng, sau đó giá trị sẽ được trả về.

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Tài liệu về Kotlin:

Khác:

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Trả lời các câu hỏi sau

Câu hỏi 1

Đâu là 2 yếu tố chính mà Retrofit cần để xây dựng API dịch vụ web?

▢ URI cơ sở dành cho dịch vụ web và truy vấn GET.

▢ URI cơ sở dành cho dịch vụ web và factory chuyển đổi.

▢ Kết nối mạng với dịch vụ web và mã thông báo uỷ quyền.

▢ Factory chuyển đổi và trình phân tích cú pháp nội dung phản hồi.

Câu hỏi 2

Thư viện Moshi dùng để làm gì?

▢ Để khôi phục dữ liệu của một dịch vụ web.

▢ Để tương tác với Retrofit nhằm đưa ra yêu cầu dịch vụ web.

▢ Để phân tích cú pháp phản hồi JSON qua một dịch vụ web thành các đối tượng dữ liệu Kotlin.

▢ Để đổi tên các đối tượng Kotlin nhằm khớp với khoá trong phản hồi JSON.

Câu hỏi 3

Bộ chuyển đổi lệnh gọi trong Retrofit dùng để làm gì?

▢ Phương thức cho phép Retrofit dùng coroutine.

▢ Chúng điều chỉnh phản hồi của dịch vụ web thành các đối tượng dữ liệu trong Kotlin.

▢ Chúng thay đổi một lệnh gọi Retrofit thành một lệnh gọi dịch vụ web.

▢ Chúng bổ sung khả năng trả về một đối tượng khác với lớp Call mặc định trong Retrofit.

Bắt đầu bài học tiếp theo: 8.2 Tải và hiển thị hình ảnh từ Internet

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.