Thêm kho lưu trữ và DI thủ công

1. Trước khi bắt đầu

Giới thiệu

Trong lớp học lập trình trước, bạn đã tìm hiểu cách lấy dữ liệu từ một dịch vụ web thông qua việc yêu cầu ViewModel truy xuất URL của ảnh chụp sao Hoả từ mạng bằng dịch vụ API. Mặc dù có tác dụng và dễ triển khai, nhưng phương pháp này khó mở rộng khi ứng dụng của bạn phát triển và cần hoạt động với nhiều nguồn dữ liệu. Để giải quyết vấn đề này, theo các phương pháp hay nhất về cấu trúc Android, bạn nên phân tách lớp giao diện người dùng và lớp dữ liệu.

Trong lớp học lập trình này, bạn sẽ tái cấu trúc ứng dụng Mars Photos thành các lớp dữ liệu và giao diện người dùng riêng biệt. Bạn sẽ tìm hiểu cách triển khai mẫu kho lưu trữ và sử dụng tính năng chèn phần phụ thuộc. Việc chèn phần phụ thuộc tạo ra một cấu trúc mã linh hoạt hơn để hỗ trợ phát triển và kiểm thử.

Điều kiện tiên quyết

  • Biết cách truy xuất JSON từ dịch vụ web REST và phân tích cú pháp dữ liệu đó thành các đối tượng Kotlin bằng cách sử dụng thư viện RetrofitSerialization (kotlinx.serialization).
  • Có kiến thức về cách sử dụng dịch vụ web REST.
  • Có thể triển khai coroutine trong ứng dụng.

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

  • Mẫu kho lưu trữ
  • Chèn phần phụ thuộc

Sản phẩm bạn sẽ tạo ra

  • Sửa đổi ứng dụng Mars Photos để phân tách ứng dụng này thành một lớp giao diện người dùng và một lớp dữ liệu.
  • Trong khi phân tách lớp dữ liệu, bạn sẽ triển khai mẫu kho lưu trữ.
  • Sử dụng tính năng chèn phần phụ thuộc để tạo một cơ sở mã có kết nối lỏng lẻo.

Những gì bạn cần

  • Một máy tính sử dụng một trình duyệt web hiện đại (chẳng hạn như trình duyệt Chrome phiên bản mới nhất)

Lấy đoạn mã khởi đầu

Để bắt đầu, hãy tải mã khởi đầu xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho đoạn mã:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter

Bạn có thể xem đoạn mã này trong kho lưu trữ GitHub Mars Photos.

2. Phân tách lớp giao diện người dùng và lớp dữ liệu

Tại sao tách thành các lớp khác nhau?

Việc phân tách mã thành các lớp khác nhau giúp ứng dụng của bạn mạnh mẽ hơn, dễ mở rộng và kiểm thử hơn. Việc có nhiều lớp với ranh giới được xác định rõ cũng giúp nhiều nhà phát triển dễ dàng làm việc trên cùng một ứng dụng hơn mà không ảnh hưởng tiêu cực đến nhau.

Cấu trúc ứng dụng được đề xuất của Android chỉ rõ rằng ứng dụng cần có ít nhất một lớp giao diện người dùng và một lớp dữ liệu.

Trong lớp học lập trình này, bạn cần tập trung vào lớp dữ liệu và thực hiện các thay đổi để ứng dụng tuân theo các phương pháp hay nhất được đề xuất.

Lớp dữ liệu là gì?

Lớp dữ liệu chịu trách nhiệm về logic nghiệp vụ của ứng dụng và việc tìm nguồn cũng như lưu dữ liệu cho ứng dụng. Lớp dữ liệu hiển thị dữ liệu cho lớp giao diện người dùng bằng cách sử dụng mẫu Luồng dữ liệu một chiều. Dữ liệu có thể đến từ nhiều nguồn, chẳng hạn như yêu cầu mạng, cơ sở dữ liệu cục bộ hoặc từ một tệp trên thiết bị.

Một ứng dụng thậm chí có thể có nhiều nguồn dữ liệu. Khi được mở, ứng dụng sẽ truy xuất dữ liệu từ cơ sở dữ liệu cục bộ trên thiết bị, đây là nguồn đầu tiên. Trong khi chạy, ứng dụng gửi yêu cầu mạng đến nguồn thứ hai để truy xuất dữ liệu mới.

Bằng cách đặt dữ liệu trong một lớp riêng biệt với đoạn mã giao diện người dùng, bạn có thể thay đổi nội dung trong phần này mà không lo ảnh hưởng đến các phần khác của mã ứng dụng. Phương pháp này là một phần của nguyên tắc thiết kế có tên là phân tách tính năng. Mỗi phần của mã ứng dụng tập trung vào một nhiệm vụ riêng và đóng gói hoạt động bên trong của nó khỏi các đoạn mã khác. Đóng gói là một hình thức ẩn cách hoạt động nội bộ của một đoạn mã khỏi các phần mã khác. Khi một phần mã cần tương tác với một phần mã khác, điều đó sẽ diễn ra thông qua một giao diện.

Nhiệm vụ của lớp giao diện người dùng là hiển thị dữ liệu được cung cấp. Giao diện người dùng không còn truy xuất dữ liệu do đó là nhiệm vụ của lớp dữ liệu.

Lớp dữ liệu được tạo thành từ một hoặc nhiều kho lưu trữ. Bản thân kho lưu trữ không chứa hoặc chứa nhiều nguồn dữ liệu.

dbf927072d3070f0.png

Các phương pháp hay nhất yêu cầu ứng dụng phải có kho lưu trữ cho từng loại nguồn dữ liệu mà ứng dụng dùng.

Trong lớp học lập trình này, ứng dụng có một nguồn dữ liệu nên sẽ có một kho lưu trữ sau khi bạn tái cấu trúc mã. Đối với ứng dụng này, kho lưu trữ truy xuất dữ liệu từ Internet sẽ hoàn thành trách nhiệm của nguồn dữ liệu. Để thực hiện điều đó, kho lưu trữ gửi yêu cầu mạng đến một API. Nếu đoạn mã ở nguồn dữ liệu phức tạp hoặc có thêm các nguồn dữ liệu bổ sung, thì trách nhiệm của nguồn dữ liệu sẽ được đóng gói trong các lớp nguồn dữ liệu riêng biệt và kho lưu trữ sẽ chịu trách nhiệm quản lý tất cả các nguồn dữ liệu.

Kho lưu trữ là gì?

Một lớp kho lưu trữ thường:

  • Hiển thị dữ liệu cho phần còn lại của ứng dụng.
  • Tập trung các thay đổi đối với dữ liệu.
  • Giải quyết xung đột giữa nhiều nguồn dữ liệu.
  • Loại bỏ nguồn dữ liệu khỏi phần còn lại của ứng dụng.
  • Chứa logic nghiệp vụ.

Ứng dụng Mars Photos có một nguồn dữ liệu duy nhất, đó là lệnh gọi API mạng. Ứng dụng này không có bất kỳ logic nghiệp vụ nào vì chỉ truy xuất dữ liệu. Dữ liệu sẽ hiển thị với ứng dụng thông qua lớp kho lưu trữ, lớp này đã loại bỏ nguồn của dữ liệu.

ff7a7cd039402747.png

3. Tạo lớp dữ liệu

Trước tiên, bạn cần tạo lớp kho lưu trữ. Theo hướng dẫn cho nhà phát triển Android, các lớp kho lưu trữ được đặt tên theo dữ liệu mà chúng chịu trách nhiệm. Quy ước đặt tên kho lưu trữkiểu dữ liệu + Repository. Trong ứng dụng của bạn, kho lưu trữ có tên là MarsPhotosRepository.

Tạo kho lưu trữ

  1. Nhấp chuột phải vào com.example.marsphotos rồi chọn New > Package (Mới > Gói).
  2. Trong hộp thoại, hãy nhập data.
  3. Nhấp chuột phải vào gói data rồi chọn New > Kotlin Class/File (Mới > Lớp/tệp Kotlin).
  4. Trong hộp thoại, hãy chọn Interface (Giao diện) rồi nhập MarsPhotosRepository làm tên giao diện.
  5. Bên trong giao diện MarsPhotosRepository, hãy thêm một hàm trừu tượng có tên là getMarsPhotos(). Hàm này sẽ trả về danh sách các đối tượng MarsPhoto. Lệnh này được gọi qua coroutine, vì vậy, hãy khai báo lệnh bằng suspend.
import com.example.marsphotos.model.MarsPhoto

interface MarsPhotosRepository {
    suspend fun getMarsPhotos(): List<MarsPhoto>
}
  1. Bên dưới phần khai báo giao diện, hãy tạo một lớp có tên là NetworkMarsPhotosRepository để triển khai giao diện MarsPhotosRepository.
  2. Thêm giao diện MarsPhotosRepository vào phần khai báo lớp.

Vì bạn đã không ghi đè phương thức trừu tượng của giao diện nên thông báo lỗi sẽ xuất hiện. Bước tiếp theo chúng ta sẽ xử lý lỗi này.

Ảnh chụp màn hình Android Studio cho thấy giao diện MarsPhotosRepository và lớp NetworkMarsPhotosRepository

  1. Bên trong lớp NetworkMarsPhotosRepository, hãy ghi đè hàm trừu tượng getMarsPhotos(). Hàm này trả về dữ liệu từ lệnh gọi MarsApi.retrofitService.getPhotos().
import com.example.marsphotos.network.MarsApi

class NetworkMarsPhotosRepository() : MarsPhotosRepository {
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
       return MarsApi.retrofitService.getPhotos()
   }
}

Tiếp theo, bạn cần cập nhật mã ViewModel để sử dụng kho lưu trữ, từ đó lấy dữ liệu theo các phương pháp hay nhất về Android.

  1. Mở tệp ui/screens/MarsViewModel.kt.
  2. Cuộn xuống phương thức getMarsPhotos().
  3. Thay thế dòng "val listResult = MarsApi.retrofitService.getPhotos()" bằng đoạn mã sau:
import com.example.marsphotos.data.NetworkMarsPhotosRepository

val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()

5313985852c151aa.png

  1. Chạy ứng dụng. Lưu ý rằng kết quả hiển thị sẽ giống với kết quả trước đó.

Thay vì ViewModel trực tiếp gửi yêu cầu mạng về dữ liệu, kho lưu trữ sẽ cung cấp dữ liệu. ViewModel không còn trực tiếp tham chiếu mã MarsApi nữa. biểu đồ luồng cho biết cách truy cập trực tiếp vào lớp dữ liệu ngay từ Viewmodel trước đó. Chúng ta hiện có kho lưu trữ ảnh sao Hoả

Phương pháp này giúp mã truy xuất dữ liệu được kết nối lỏng lẻo từ ViewModel. Kết nối lỏng lẻo cho phép thay đổi ViewModel hoặc kho lưu trữ mà không ảnh hưởng tiêu cực đến phần khác, miễn là kho lưu trữ có một hàm tên là getMarsPhotos().

Giờ đây, chúng ta có thể thay đổi quy trình triển khai triển khai bên trong kho lưu trữ mà không ảnh hưởng đến phương thức gọi. Đối với các ứng dụng lớn hơn, thay đổi này có thể hỗ trợ nhiều phương thức gọi.

4. Chèn phần phụ thuộc

Trong nhiều trường hợp, lớp thường yêu cầu đối tượng của các lớp khác để hoạt động. Khi một lớp yêu cầu một lớp khác, lớp được yêu cầu sẽ gọi là phần phụ thuộc.

Trong các ví dụ sau, đối tượng Car phụ thuộc vào đối tượng Engine.

Một lớp có thể lấy các đối tượng được yêu cầu này theo hai cách. Cách thứ nhất là để lớp này tạo thực thể cho chính đối tượng được yêu cầu.

interface Engine {
    fun start()
}

class GasEngine : Engine {
    override fun start() {
        println("GasEngine started!")
    }
}

class Car {

    private val engine = GasEngine()

    fun start() {
        engine.start()
    }
}

fun main() {
    val car = Car()
    car.start()
}

Cách thứ hai là truyền đối tượng được yêu cầu vào dưới dạng đối số.

interface Engine {
    fun start()
}

class GasEngine : Engine {
    override fun start() {
        println("GasEngine started!")
    }
}

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = GasEngine()
    val car = Car(engine)
    car.start()
}

Rất dễ để có được một lớp tạo thực thể cho đối tượng được yêu cầu, nhưng phương pháp này khiến mã không linh hoạt và khó kiểm thử hơn vì lớp và đối tượng được yêu cầu được kết nối chặt chẽ.

Lớp gọi cần gọi hàm khởi tạo của đối tượng, đó là một chi tiết triển khai. Nếu hàm khởi tạo thay đổi thì mã gọi cũng cần thay đổi.

Để mã linh hoạt và dễ điều chỉnh hơn, lớp không được tạo thực thể cho các đối tượng mà nó phụ thuộc. Các đối tượng mà lớp phụ thuộc phải được tạo thực thể bên ngoài lớp rồi truyền vào. Phương pháp này sẽ giúp đoạn mã linh hoạt hơn vì lớp không còn được cố định giá trị (hardcode) vào một đối tượng cụ thể nữa. Việc triển khai đối tượng được yêu cầu có thể thay đổi mà không cần sửa đổi mã gọi.

Tiếp tục với ví dụ trước, nếu cần ElectricEngine, bạn có thể tạo và truyền mã này vào lớp Car. Bạn không cần phải sửa đổi lớp Car theo bất kỳ cách nào.

interface Engine {
    fun start()
}

class ElectricEngine : Engine {
    override fun start() {
        println("ElectricEngine started!")
    }
}

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = ElectricEngine()
    val car = Car(engine)
    car.start()
}

Quá trình truyền đối tượng được yêu cầu gọi là chèn phần phụ thuộc (DI), hay còn gọi là đảo ngược quyền kiểm soát.

DI là khi một phần phụ thuộc được cung cấp trong thời gian chạy thay vì được mã hoá cứng vào lớp gọi.

Việc chèn phần phụ thuộc:

  • Giúp tái sử dụng mã. Mã không phụ thuộc vào một đối tượng cụ thể nên linh hoạt hơn.
  • Giúp tái cấu trúc dễ dàng hơn. Mã có kết nối lỏng lẻo, nên việc tái cấu trúc một phần mã không ảnh hưởng đến phần mã khác.
  • Giúp hỗ trợ kiểm thử. Có thể truyền đối tượng kiểm thử vào khi kiểm thử.

Một ví dụ cho thấy cách DI hỗ trợ kiểm thử là khi kiểm thử mã gọi mạng. Đối với quy trình kiểm thử này, bạn thực sự đang cố kiểm thử để đảm bảo rằng lệnh gọi mạng được thực hiện và dữ liệu được trả về. Nếu phải trả tiền mỗi lần gửi yêu cầu mạng trong quá trình kiểm thử, thì bạn có thể quyết định bỏ qua việc kiểm thử mã này để tránh tốn kém. Bây giờ, hãy tưởng tượng là chúng ta có thể giả mạo yêu cầu mạng để kiểm thử. Bạn sẽ hài lòng hơn (và tiết kiệm chi phí hơn) đến mức nào? Để kiểm thử, bạn có thể truyền một đối tượng kiểm thử đến kho lưu trữ. Kho lưu trữ sẽ trả về dữ liệu giả khi được gọi mà thực chất không thực hiện lệnh gọi mạng nào. 1ea410d6670b7670.png

Chúng ta muốn làm cho ViewModel dễ kiểm thử, nhưng điều này hiện phụ thuộc vào một kho lưu trữ thực hiện lệnh gọi mạng thực tế. Khi kiểm thử với kho lưu trữ sản xuất thực tế, kho lưu trữ này thực hiện nhiều lệnh gọi mạng. Để khắc phục vấn đề này, thay vì ViewModel tạo kho lưu trữ, chúng ta cần có cách quyết định và truyền một thực thể kho lưu trữ để dùng cho việc sản xuất và kiểm thử linh động.

Quy trình này được thực hiện bằng cách triển khai một vùng chứa ứng dụng cung cấp kho lưu trữ cho MarsViewModel.

Vùng chứa là một đối tượng chứa các phần phụ thuộc mà ứng dụng yêu cầu. Những phần phụ thuộc này được dùng trên toàn bộ ứng dụng, nên cần nằm ở một vị trí chung mà mọi hoạt động đều có thể sử dụng. Bạn có thể tạo một lớp con của lớp Application và lưu trữ tệp tham chiếu đến vùng chứa.

Tạo vùng chứa ứng dụng

  1. Nhấp chuột phải vào gói data rồi chọn New > Kotlin Class/File (Mới > Lớp/tệp Kotlin).
  2. Trong hộp thoại, hãy chọn Interface (Giao diện) rồi nhập AppContainer làm tên giao diện.
  3. Bên trong giao diện AppContainer, hãy thêm một thuộc tính trừu tượng có tên là marsPhotosRepository thuộc kiểu MarsPhotosRepository. 7ed26c6dcf607a55.png
  4. Bên dưới phần định nghĩa giao diện, hãy tạo một lớp có tên là DefaultAppContainer. Lớp này sẽ triển khai giao diện AppContainer.
  5. Từ network/MarsApiService.kt, hãy chuyển mã cho các biến BASE_URL, retrofitretrofitService vào lớp DefaultAppContainer để tất cả các biến đó nằm trong vùng chứa giúp duy trì phần phụ thuộc.
import retrofit2.Retrofit
import com.example.marsphotos.network.MarsApiService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType

class DefaultAppContainer : AppContainer {

    private const val BASE_URL =
        "https://android-kotlin-fun-mars-server.appspot.com"

    private val retrofit: Retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

    private val retrofitService: MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }

}
  1. Đối với biến BASE_URL, hãy xoá từ khoá const. Bạn cần xoá constBASE_URL không còn là biến cấp cao nhất nữa và hiện là một thuộc tính của lớp DefaultAppContainer. Hãy đổi tên biến này theo quy ước camelcase baseUrl.
  2. Đối với biến retrofitService, hãy thêm đối tượng sửa đổi chế độ hiển thị private. Đối tượng sửa đổi private được thêm vì thuộc tính marsPhotosRepository chỉ sử dụng biến retrofitService bên trong lớp. Do đó, không cần có quyền truy cập biến này bên ngoài lớp.
  3. Lớp DefaultAppContainer triển khai giao diện AppContainer nên chúng ta cần ghi đè thuộc tính marsPhotosRepository. Sau biến retrofitService, hãy thêm mã sau:
override val marsPhotosRepository: MarsPhotosRepository by lazy {
    NetworkMarsPhotosRepository(retrofitService)
}

Lớp DefaultAppContainer đã hoàn thành sẽ có dạng như sau:

class DefaultAppContainer : AppContainer {

    private val baseUrl =
        "https://android-kotlin-fun-mars-server.appspot.com"

    /**
     * Use the Retrofit builder to build a retrofit object using a kotlinx.serialization converter
     */
    private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(baseUrl)
        .build()

    private val retrofitService: MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }

    override val marsPhotosRepository: MarsPhotosRepository by lazy {
        NetworkMarsPhotosRepository(retrofitService)
    }
}
  1. Mở tệp data/MarsPhotosRepository.kt. Chúng ta hiện đang truyền retrofitService đến NetworkMarsPhotosRepository và bạn cần sửa đổi lớp NetworkMarsPhotosRepository.
  2. Trong phần khai báo lớp NetworkMarsPhotosRepository, hãy thêm tham số hàm khởi tạo marsApiService như minh hoạ trong đoạn mã sau.
import com.example.marsphotos.network.MarsApiService

class NetworkMarsPhotosRepository(
    private val marsApiService: MarsApiService
) : MarsPhotosRepository {
  1. Trong lớp NetworkMarsPhotosRepository, trong hàm getMarsPhotos(), hãy thay đổi câu lệnh trả về để truy xuất dữ liệu từ marsApiService.
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
  1. Xoá dữ liệu nhập sau khỏi tệp MarsPhotosRepository.kt.
// Remove
import com.example.marsphotos.network.MarsApi

Từ tệp network/MarsApiService.kt, chúng ta đã chuyển tất cả mã ra khỏi đối tượng. Bây giờ, chúng ta có thể xoá phần khai báo đối tượng còn lại vì không cần dùng nữa.

  1. Xoá đoạn mã sau:
object MarsApi {

}

5. Đính kèm vùng chứa ứng dụng vào ứng dụng

Các bước trong phần này kết nối đối tượng ứng dụng với vùng chứa ứng dụng như minh hoạ trong hình sau.

92e7d7b79c4134f0.png

  1. Nhấp chuột phải vào com.example.marsphotos rồi chọn New > Kotlin Class/File (Mới > Lớp/tệp Kotlin).
  2. Trong hộp thoại, hãy nhập MarsPhotosApplication. Lớp này kế thừa từ đối tượng ứng dụng nên bạn cần thêm lớp này vào phần khai báo lớp.
import android.app.Application

class MarsPhotosApplication : Application() {
}
  1. Bên trong lớp MarsPhotosApplication, hãy khai báo biến có tên là container thuộc loại AppContainer để lưu trữ đối tượng DefaultAppContainer. Biến này được khởi tạo trong lệnh gọi đến onCreate(), nên cần được đánh dấu bằng đối tượng sửa đổi lateinit.
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer

lateinit var container: AppContainer
override fun onCreate() {
    super.onCreate()
    container = DefaultAppContainer()
}
  1. Tệp MarsPhotosApplication.kt hoàn chỉnh sẽ có dạng như mã sau:
package com.example.marsphotos

import android.app.Application
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer

class MarsPhotosApplication : Application() {
    lateinit var container: AppContainer
    override fun onCreate() {
        super.onCreate()
        container = DefaultAppContainer()
    }
}
  1. Bạn cần cập nhật tệp kê khai Android để ứng dụng dùng lớp ứng dụng mà bạn vừa xác định. Mở tệp manifests/AndroidManifest.xml.

759144e4e0634ed8.png

  1. Trong phần application, hãy thêm giá trị của tên lớp ứng dụng ".MarsPhotosApplication" cho thuộc tính android:name.
<application
   android:name=".MarsPhotosApplication"
   android:allowBackup="true"
...
</application>

6. Thêm kho lưu trữ vào ViewModel

Sau khi bạn hoàn tất các bước này, ViewModel có thể gọi đối tượng kho lưu trữ để truy xuất dữ liệu sao Hoả.

7425864315cb5e6f.png

  1. Mở tệp ui/screens/MarsViewModel.kt.
  2. Trong phần khai báo lớp cho MarsViewModel, hãy thêm tham số hàm khởi tạo private marsPhotosRepository thuộc kiểu MarsPhotosRepository. Giá trị cho tham số hàm khởi tạo đến từ vùng chứa ứng dụng vì ứng dụng đang dùng tính năng chèn phần phụ thuộc.
import com.example.marsphotos.data.MarsPhotosRepository

class MarsViewModel(private val marsPhotosRepository: MarsPhotosRepository) : ViewModel(){
  1. Trong hàm getMarsPhotos(), hãy xoá dòng mã sau vì marsPhotosRepository hiện đang được điền sẵn trong lệnh gọi hàm khởi tạo.
val marsPhotosRepository = NetworkMarsPhotosRepository()
  1. Vì khung Android không cho phép truyền các giá trị ViewModel trong hàm khởi tạo khi được tạo, nên chúng ta sẽ triển khai một đối tượng ViewModelProvider.Factory để có thể khắc phục hạn chế này.

Mẫu nhà máy là một mẫu dùng để tạo đối tượng. Đối tượng MarsViewModel.Factory dùng vùng chứa ứng dụng để truy xuất marsPhotosRepository, sau đó truyền kho lưu trữ này đến ViewModel khi đối tượng ViewModel được tạo.

  1. Bên dưới hàm getMarsPhotos(), hãy nhập mã cho đối tượng đồng hành này.

Đối tượng đồng hành có một thực thể của đối tượng được mọi người sử dụng mà không cần tạo thực thể mới của đối tượng tốn kém. Đây là chi tiết triển khai và khi tách riêng chi tiết này, chúng ta có thể thực hiện các thay đổi mà không ảnh hưởng đến những phần khác của mã ứng dụng.

APPLICATION_KEY là một phần của đối tượng ViewModelProvider.AndroidViewModelFactory.Companion và dùng để tìm đối tượng MarsPhotosApplication của ứng dụng. Đối tượng này có thuộc tính container dùng để truy xuất kho lưu trữ dùng để chèn phần phụ thuộc.

import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.marsphotos.MarsPhotosApplication

companion object {
   val Factory: ViewModelProvider.Factory = viewModelFactory {
       initializer {
           val application = (this[APPLICATION_KEY] as MarsPhotosApplication)
           val marsPhotosRepository = application.container.marsPhotosRepository
           MarsViewModel(marsPhotosRepository = marsPhotosRepository)
       }
   }
}
  1. Mở tệp theme/MarsPhotosApp.kt, bên trong hàm MarsPhotosApp(), cập nhật viewModel() để sử dụng phương thức factory.
Surface(
            // ...
        ) {
            val marsViewModel: MarsViewModel =
   viewModel(factory = MarsViewModel.Factory)
            // ...
        }

Biến marsViewModel này được điền sẵn bằng lệnh gọi đến hàm viewModel(). Hàm này nhận MarsViewModel.Factory từ đối tượng đồng hành dưới dạng một đối số để tạo ViewModel.

  1. Chạy ứng dụng để xác nhận rằng ứng dụng vẫn hoạt động như trước đây.

Chúc mừng bạn đã tái cấu trúc ứng dụng Mars Photos để dùng kho lưu trữ và tính năng chèn phần phụ thuộc! Bằng cách triển khai lớp dữ liệu thông qua kho lưu trữ, giao diện người dùng và mã nguồn dữ liệu đã được phân tách để tuân theo các phương pháp hay nhất về Android.

Bằng tính năng chèn phần phụ thuộc, bạn có thể dễ dàng kiểm thử ViewModel. Giờ đây, ứng dụng của bạn sẽ linh hoạt, mạnh mẽ hơn và sẵn sàng để mở rộng quy mô.

Bây giờ, bạn cần kiểm thử những cải tiến mà mình đã thực hiện. Việc kiểm thử giúp mã của bạn hoạt động như dự kiến và giảm khả năng gặp lỗi khi bạn tiếp tục xử lý mã.

7. Thiết lập quy trình kiểm thử cục bộ

Trong các phần trước, bạn đã triển khai một kho lưu trữ để trừu tượng hoá hoạt động tương tác trực tiếp với dịch vụ API REST từ ViewModel. Phương pháp này giúp bạn kiểm thử các đoạn mã nhỏ có mục đích giới hạn. Quy trình kiểm thử các đoạn mã nhỏ có chức năng giới hạn sẽ dễ xây dựng, triển khai và dễ hiểu hơn so với quy trình kiểm thử được viết cho các đoạn mã lớn có nhiều chức năng.

Bạn cũng đã triển khai kho lưu trữ bằng cách sử dụng giao diện, tính kế thừa và tính năng chèn phần phụ thuộc. Trong các phần tiếp theo, bạn sẽ tìm hiểu lý do các phương pháp hay nhất về cấu trúc này giúp bạn dễ dàng kiểm thử hơn. Ngoài ra, bạn đã sử dụng coroutine của Kotlin để tạo yêu cầu mạng. Quy trình kiểm thử mã sử dụng coroutine đòi hỏi phải thực hiện các bước bổ sung để tính đến việc thực thi mã không đồng bộ. Các bước này sẽ được đề cập ở phần sau trong lớp học lập trình này.

Thêm các phần phụ thuộc kiểm thử cục bộ

Thêm các phần phụ thuộc sau vào app/build.gradle.kts.

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")

Tạo thư mục kiểm thử cục bộ

  1. Tạo thư mục kiểm thử cục bộ bằng cách nhấp chuột phải vào thư mục src trong khung hiển thị dự án rồi chọn New > Directory > test/java (Mới > Thư mục > kiểm thử/java).
  2. Tạo một gói mới trong thư mục kiểm thử có tên là com.example.marsphotos.

8. Tạo dữ liệu và phần phụ thuộc giả mạo để kiểm thử

Trong phần này, bạn sẽ tìm hiểu cách tính năng chèn phần phụ thuộc giúp bạn viết quy trình kiểm thử cục bộ. Trong phần trước của lớp học lập trình, bạn đã tạo một kho lưu trữ phụ thuộc vào dịch vụ API. Sau đó, bạn đã sửa đổi ViewModel để thành phần này phụ thuộc vào kho lưu trữ.

Mỗi quy trình kiểm thử cục bộ chỉ cần kiểm thử một nội dung Ví dụ: khi kiểm thử chức năng của mô hình hiển thị, bạn không nên kiểm thử chức năng của kho lưu trữ hoặc dịch vụ API. Tương tự, khi kiểm thử kho lưu trữ, bạn không nên kiểm thử dịch vụ API.

Khi sử dụng giao diện rồi chèn phần phụ thuộc để bao gồm lớp kế thừa từ các giao diện đó, bạn có thể mô phỏng chức năng của các phần phụ thuộc đó bằng cách sử dụng lớp giả mạo chỉ dành cho mục đích kiểm thử. Thông qua việc chèn nguồn dữ liệu và lớp giả mạo cho mục đích kiểm thử, bạn có thể kiểm thử mã một cách riêng biệt và đảm bảo khả năng lặp lại cũng như tính nhất quán.

Điều đầu tiên bạn cần là dữ liệu giả mạo để sử dụng trong các lớp giả mạo mà bạn tạo sau này.

  1. Trong thư mục kiểm thử, hãy tạo một gói trong com.example.marsphotos có tên là fake.
  2. Tạo đối tượng Kotlin mới trong thư mục fake có tên là FakeDataSource.
  3. Trong đối tượng này, hãy tạo một thuộc tính được đặt thành danh sách các đối tượng MarsPhoto. Danh sách không nhất thiết phải dài nhưng cần chứa ít nhất hai đối tượng.
object FakeDataSource {

   const val idOne = "img1"
   const val idTwo = "img2"
   const val imgOne = "url.1"
   const val imgTwo = "url.2"
   val photosList = listOf(
       MarsPhoto(
           id = idOne,
           imgSrc = imgOne
       ),
       MarsPhoto(
           id = idTwo,
           imgSrc = imgTwo
       )
   )
}

Như đã đề cập trước đó trong lớp học lập trình này, kho lưu trữ phụ thuộc vào dịch vụ API. Để tạo quy trình kiểm thử kho lưu trữ, phải có một dịch vụ API giả mạo trả về dữ liệu giả mạo bạn vừa tạo. Nếu dịch vụ API giả này được truyền vào kho lưu trữ, kho lưu trữ sẽ nhận được dữ liệu giả khi các phương thức trong dịch vụ API giả được gọi.

  1. Trong gói fake, hãy tạo một lớp mới có tên là FakeMarsApiService.
  2. Thiết lập lớp FakeMarsApiService để kế thừa từ giao diện MarsApiService.
class FakeMarsApiService : MarsApiService {
}
  1. Ghi đè hàm getPhotos().
override suspend fun getPhotos(): List<MarsPhoto> {
}
  1. Trả về danh sách ảnh giả mạo từ phương thức getPhotos().
override suspend fun getPhotos(): List<MarsPhoto> {
   return FakeDataSource.photosList
}

Hãy nhớ rằng nếu bạn vẫn chưa rõ về mục đích của lớp này thì cũng không sao! Việc sử dụng lớp giả mạo này sẽ được giải thích chi tiết hơn trong phần tiếp theo.

9. Viết mã kiểm thử kho lưu trữ

Trong phần này, bạn sẽ kiểm thử phương thức getMarsPhotos() của lớp NetworkMarsPhotosRepository. Phần này giải thích cách sử dụng các lớp giả mạo và trình bày cách kiểm thử coroutine.

  1. Trong thư mục giả mạo, hãy tạo một lớp mới có tên là NetworkMarsRepositoryTest.
  2. Tạo một phương thức mới trong lớp bạn vừa tạo có tên là networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() và chú thích phương thức đó bằng @Test.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
}

Để kiểm thử kho lưu trữ, bạn cần có một thực thể của NetworkMarsPhotosRepository. Hãy nhớ rằng lớp này phụ thuộc vào giao diện MarsApiService. Tại đây, bạn sử dụng dịch vụ API giả mạo từ phần trước.

  1. Tạo một thực thể của NetworkMarsPhotosRepository và truyền FakeMarsApiService dưới dạng tham số marsApiService.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
    val repository = NetworkMarsPhotosRepository(
       marsApiService = FakeMarsApiService()
    )
}

Khi truyền dịch vụ API giả mạo, mọi lệnh gọi đến thuộc tính marsApiService trong kho lưu trữ sẽ dẫn tới lệnh gọi đến FakeMarsApiService. Bằng cách truyền lớp giả mạo cho phần phụ thuộc, bạn có thể kiểm soát chính xác dữ liệu mà phần phụ thuộc trả về. Phương pháp này đảm bảo mã bạn đang kiểm thử không phụ thuộc vào mã hoặc API chưa kiểm thử có thể thay đổi hoặc gặp sự cố không lường trước. Những tình huống như vậy có thể khiến quy trình kiểm thử không thành công, ngay cả khi bạn viết đúng mã. Dữ liệu giả mạo giúp tạo ra một môi trường kiểm thử nhất quán hơn, giảm bớt tình trạng không ổn định và tạo điều kiện cho các quy trình kiểm thử ngắn gọn chỉ kiểm thử một chức năng duy nhất.

  1. Xác nhận rằng dữ liệu mà phương thức getMarsPhotos() trả về là FakeDataSource.photosList.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
    val repository = NetworkMarsPhotosRepository(
       marsApiService = FakeMarsApiService()
    )assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}

Lưu ý rằng trong IDE (môi trường phát triển tích hợp), lệnh gọi phương thức getMarsPhotos() được gạch chân màu đỏ.

2bd5f8999e0f3ec2.png

Nếu di chuột qua phương thức này, bạn có thể thấy chú thích là "Suspend function 'getMarsPhotos' should be called only from a coroutine or another suspend function" (Chỉ nên gọi hàm tạm ngưng 'getMarsPhotos' từ một coroutine hoặc một hàm tạm ngưng khác):

d2d3b6d770677ef6.png

Trong data/MarsPhotosRepository.kt, khi xem cách triển khai getMarsPhotos() trong NetworkMarsPhotosRepository, bạn sẽ thấy rằng hàm getMarsPhotos() là một hàm tạm ngưng.

class NetworkMarsPhotosRepository(
   private val marsApiService: MarsApiService
) : MarsPhotosRepository {
   /** Fetches list of MarsPhoto from marsApi*/
   override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}

Hãy nhớ khi gọi hàm này từ MarsViewModel, bạn đã gọi phương thức này từ một coroutine bằng cách gọi từ một lambda đã truyền đến viewModelScope.launch(). Bạn cũng phải gọi các hàm tạm ngưng, chẳng hạn như getMarsPhotos(), từ một coroutine trong quy trình kiểm thử. Tuy nhiên, bạn cần áp dụng phương pháp khác. Phần tiếp theo sẽ thảo luận về cách giải quyết vấn đề này.

Kiểm thử coroutine

Trong phần này, bạn sẽ sửa đổi quy trình kiểm thử networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() để chạy phần thân của phương thức kiểm thử từ một coroutine.

  1. Sửa đổi trong NetworkMarsRepositoryTest.kt hàm networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() thành một biểu thức.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
  1. Đặt biểu thức này bằng với hàm runTest(). Phương thức này yêu cầu một lambda.
...
import kotlinx.coroutines.test.runTest
...

@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
    runTest {}

Thư viện kiểm thử coroutine cung cấp hàm runTest(). Hàm này lấy phương thức mà bạn đã truyền vào lambda và chạy phương thức đó từ TestScope kế thừa từ CoroutineScope.

  1. Chuyển nội dung của hàm kiểm thử vào hàm lambda.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
   runTest {
       val repository = NetworkMarsPhotosRepository(
           marsApiService = FakeMarsApiService()
       )
       assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
   }

Lưu ý rằng đường màu đỏ bên dưới getMarsPhotos() hiện đã biến mất. Nếu bạn chạy được, có nghĩa là quy trình kiểm thử này đã thành công!

10. Viết mã kiểm thử ViewModel

Trong phần này, bạn sẽ viết mã kiểm thử cho hàm getMarsPhotos() từ MarsViewModel. MarsViewModel phụ thuộc vào MarsPhotosRepository. Do đó, để viết mã kiểm thử này, bạn cần tạo một MarsPhotosRepository giả mạo. Ngoài ra, bên cạnh việc sử dụng phương thức runTest(), bạn cần thực hiện một số bước bổ sung để xem xét coroutine.

Tạo kho lưu trữ giả mạo

Mục tiêu của bước này là tạo một lớp giả mạo kế thừa từ giao diện MarsPhotosRepository và ghi đè hàm getMarsPhotos() để trả về dữ liệu giả mạo. Phương pháp này tương tự như phương pháp bạn đã thực hiện với dịch vụ API giả mạo, nhưng khác ở chỗ lớp này mở rộng giao diện MarsPhotosRepository thay vì MarsApiService.

  1. Tạo một lớp mới trong thư mục fake có tên là FakeNetworkMarsPhotosRepository.
  2. Mở rộng lớp này bằng giao diện MarsPhotosRepository.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
  1. Ghi đè hàm getMarsPhotos().
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
   }
}
  1. Trả về FakeDataSource.photosList từ hàm getMarsPhotos().
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
       return FakeDataSource.photosList
   }
}

Viết mã kiểm thử ViewModel

  1. Tạo một lớp mới tên là MarsViewModelTest.
  2. Tạo một hàm có tên là marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() rồi chú thích hàm đó bằng @Test.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
  1. Đặt hàm này thành một biểu thức sẽ được đặt thành kết quả của phương thức runTest(), nhằm đảm bảo quy trình kiểm thử chạy từ một coroutine, giống như bài kiểm thử kho lưu trữ trong phần trước.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
    runTest{
    }
  1. Trong phần thân hàm lambda của runTest(), hãy tạo một thực thể của MarsViewModel và truyền vào đó một thực thể của kho lưu trữ giả mạo mà bạn đã tạo.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
    runTest{
        val marsViewModel = MarsViewModel(
            marsPhotosRepository = FakeNetworkMarsPhotosRepository()
         )
    }
  1. Xác nhận rằng marsUiState của thực thể ViewModel khớp với kết quả của lệnh gọi thành công đến MarsPhotosRepository.getMarsPhotos().
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
   runTest {
       val marsViewModel = MarsViewModel(
           marsPhotosRepository = FakeNetworkMarsPhotosRepository()
       )
       assertEquals(
           MarsUiState.Success("Success: ${FakeDataSource.photosList.size} Mars " +
                   "photos retrieved"),
           marsViewModel.marsUiState
       )
   }

Quy trình kiểm thử này sẽ không thành công nếu bạn cố chạy nguyên trạng. Lỗi sẽ có dạng như ví dụ sau:

Exception in thread "Test worker @coroutine#1" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used

Hãy nhớ rằng MarsViewModel gọi kho lưu trữ bằng viewModelScope.launch(). Lệnh này sẽ khởi động một coroutine mới trong trình điều phối coroutine mặc định, được gọi là trình điều phối Main. Trình điều phối Main bao bọc luồng giao diện người dùng Android. Nguyên nhân gây ra lỗi trước đó là do luồng giao diện người dùng Android không có trong kiểm thử đơn vị. Quy trình kiểm thử đơn vị được thực thi trên máy trạm của bạn, chứ không phải trên thiết bị Android hay Trình mô phỏng. Nếu mã trong quy trình kiểm thử đơn vị cục bộ tham chiếu đến trình điều phối Main, thì một ngoại lệ (như ngoại lệ ở trên) sẽ được trả về khi chạy quy trình kiểm thử đơn vị. Để khắc phục vấn đề này, bạn phải xác định rõ trình điều phối mặc định khi chạy kiểm thử đơn vị. Hãy chuyển đến phần tiếp theo để tìm hiểu cách thực hiện.

Tạo trình điều phối kiểm thử

Vì trình điều phối Main chỉ có trong ngữ cảnh giao diện người dùng, nên bạn phải thay thế trình điều phối đó bằng một trình điều phối phù hợp để kiểm thử đơn vị. Thư viện Coroutine của Kotlin cung cấp trình điều phối coroutine cho mục đích này có tên là TestDispatcher. Bạn cần dùng TestDispatcher thay vì trình điều phối Main cho mọi quy trình kiểm thử đơn vị có coroutine mới được tạo, như trường hợp với hàm getMarsPhotos() từ mô hình hiển thị.

Để thay thế trình điều phối Main bằng TestDispatcher trong mọi trường hợp, hãy sử dụng hàm Dispatchers.setMain(). Bạn có thể sử dụng hàm Dispatchers.resetMain() để đặt lại trình điều phối luồng về trình điều phối Main. Để tránh lặp lại mã sẽ thay thế trình điều phối Main trong mỗi quy trình kiểm thử, bạn có thể trích xuất mã này vào quy tắc kiểm thử JUnit. TestRule cung cấp cách kiểm soát môi trường chạy kiểm thử. TestRule có thể thêm các bước kiểm tra bổ sung, thực hiện thao tác thiết lập hoặc dọn dẹp cần thiết cho quy trình kiểm thử, hoặc giám sát phiên chạy thử nghiệm để báo cáo ở những nơi khác. Bạn có thể dễ dàng chia sẻ TestRule giữa các lớp kiểm thử.

Tạo một lớp riêng để viết TestRule sẽ thay thế trình điều phối Main. Để triển khai TestRule tuỳ chỉnh, hãy hoàn tất các bước sau:

  1. Tạo một gói mới trong thư mục kiểm thử có tên là rules.
  2. Trong thư mục quy tắc, hãy tạo một lớp mới có tên là TestDispatcherRule.
  3. Mở rộng TestDispatcherRule bằng TestWatcher. Lớp TestWatcher cho phép bạn thao tác trong nhiều giai đoạn thực thi của một quy trình kiểm thử.
class TestDispatcherRule(): TestWatcher(){

}
  1. Tạo tham số hàm khởi tạo TestDispatcher cho TestDispatcherRule.

Tham số này cho phép sử dụng các trình điều phối khác nhau, chẳng hạn như StandardTestDispatcher. Tham số hàm khởi tạo cần có một giá trị mặc định được đặt thành thực thể của đối tượng UnconfinedTestDispatcher. Lớp UnconfinedTestDispatcher kế thừa từ lớp TestDispatcher và chỉ định rằng không được thực thi các nhiệm vụ theo bất kỳ thứ tự cụ thể nào. Mẫu thực thi này phù hợp với các quy trình kiểm thử đơn giản vì coroutine được xử lý tự động. Không giống như UnconfinedTestDispatcher, lớp StandardTestDispatcher cho phép toàn quyền kiểm soát quá trình thực thi coroutine. Đây là cách phù hợp cho các quy trình kiểm thử phức tạp yêu cầu phương pháp thủ công, nhưng không cần thiết cho các quy trình kiểm thử trong lớp học lập trình này.

class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {

}
  1. Mục tiêu chính của quy tắc kiểm thử này là thay thế trình điều phối Main bằng một trình điều phối kiểm thử trước khi quy trình kiểm thử bắt đầu thực thi. Hàm starting() của lớp TestWatcher sẽ thực thi trước khi một quy trình kiểm thử nhất định thực thi. Ghi đè hàm starting().
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {

    }
}
  1. Thêm lệnh gọi vào Dispatchers.setMain(), truyền vào testDispatcher dưới dạng một đối số.
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }
}
  1. Sau khi phiên chạy thử nghiệm hoàn tất, hãy đặt lại trình điều phối Main bằng cách ghi đè phương thức finished(). Gọi hàm Dispatchers.resetMain().
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

Quy tắc TestDispatcherRule đã sẵn sàng để sử dụng lại.

  1. Mở tệp MarsViewModelTest.kt.
  2. Trong lớp MarsViewModelTest, hãy tạo thực thể cho lớp TestDispatcherRule và gán thực thể này cho thuộc tính testDispatcher chỉ có thể đọc.
class MarsViewModelTest {

    val testDispatcher = TestDispatcherRule()
    ...
}
  1. Để áp dụng quy tắc này cho các quy trình kiểm thử của bạn, hãy thêm chú thích @get:Rule vào thuộc tính testDispatcher.
class MarsViewModelTest {
    @get:Rule
    val testDispatcher = TestDispatcherRule()
    ...
}
  1. Chạy lại quy trình kiểm thử. Hãy xác nhận rằng lần kiểm thử này thành công.

11. Lấy mã giải pháp

Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh sau:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout coil-starter

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén rồi mở trong Android Studio.

Nếu bạn muốn tham khảo đoạn mã giải pháp cho lớp học lập trình này, hãy xem trên GitHub.

12. Kết luận

Chúc mừng bạn đã hoàn thành lớp học lập trình này và tái cấu trúc ứng dụng Mars Photos để triển khai mẫu kho lưu trữ cũng như tính năng chèn phần phụ thuộc!

Mã của ứng dụng đang tuân theo các phương pháp hay nhất của Android dành cho lớp dữ liệu, tức là mã này linh hoạt, mạnh mẽ và dễ mở rộng hơn.

Những thay đổi này cũng giúp ứng dụng dễ kiểm thử hơn. Lợi ích này rất quan trọng vì mã có thể tiếp tục phát triển, đồng thời đảm bảo rằng mã vẫn hoạt động như dự kiến.

Đừng quên chia sẻ công việc của bạn trên mạng xã hội với #AndroidBasics!

13. Tìm hiểu thêm

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

Các tài liệu khác: