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 Retrofit và Serialization (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.
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.
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ữ là 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ữ
- Nhấp chuột phải vào com.example.marsphotos rồi chọn New > Package (Mới > Gói).
- Trong hộp thoại, hãy nhập
data
. - 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). - 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. - 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ượngMarsPhoto
. Lệnh này được gọi qua coroutine, vì vậy, hãy khai báo lệnh bằngsuspend
.
import com.example.marsphotos.model.MarsPhoto
interface MarsPhotosRepository {
suspend fun getMarsPhotos(): List<MarsPhoto>
}
- 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ệnMarsPhotosRepository
. - 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.
- Bên trong lớp
NetworkMarsPhotosRepository
, hãy ghi đè hàm trừu tượnggetMarsPhotos()
. Hàm này trả về dữ liệu từ lệnh gọiMarsApi.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.
- Mở tệp
ui/screens/MarsViewModel.kt
. - Cuộn xuống phương thức
getMarsPhotos()
. - 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()
- 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.
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.
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
- 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). - 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. - 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ểuMarsPhotosRepository
. - 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ệnAppContainer
. - Từ
network/MarsApiService.kt
, hãy chuyển mã cho các biếnBASE_URL
,retrofit
vàretrofitService
vào lớpDefaultAppContainer
để 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)
}
}
- Đối với biến
BASE_URL
, hãy xoá từ khoáconst
. Bạn cần xoáconst
vìBASE_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ớpDefaultAppContainer
. Hãy đổi tên biến này theo quy ước camelcasebaseUrl
. - Đố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 đổiprivate
được thêm vì thuộc tínhmarsPhotosRepository
chỉ sử dụng biếnretrofitService
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. - Lớp
DefaultAppContainer
triển khai giao diệnAppContainer
nên chúng ta cần ghi đè thuộc tínhmarsPhotosRepository
. Sau biếnretrofitService
, 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)
}
}
- Mở tệp
data/MarsPhotosRepository.kt
. Chúng ta hiện đang truyềnretrofitService
đếnNetworkMarsPhotosRepository
và bạn cần sửa đổi lớpNetworkMarsPhotosRepository
. - Trong phần khai báo lớp
NetworkMarsPhotosRepository
, hãy thêm tham số hàm khởi tạomarsApiService
như minh hoạ trong đoạn mã sau.
import com.example.marsphotos.network.MarsApiService
class NetworkMarsPhotosRepository(
private val marsApiService: MarsApiService
) : MarsPhotosRepository {
- Trong lớp
NetworkMarsPhotosRepository
, trong hàmgetMarsPhotos()
, 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()
}
- 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.
- 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.
- 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). - 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() {
}
- Bên trong lớp
MarsPhotosApplication
, hãy khai báo biến có tên làcontainer
thuộc loạiAppContainer
để lưu trữ đối tượngDefaultAppContainer
. Biến này được khởi tạo trong lệnh gọi đếnonCreate()
, nên cần được đánh dấu bằng đối tượng sửa đổilateinit
.
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
- 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()
}
}
- 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
.
- Trong phần
application
, hãy thêm giá trị của tên lớp ứng dụng".MarsPhotosApplication"
cho thuộc tínhandroid: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ả.
- Mở tệp
ui/screens/MarsViewModel.kt
. - Trong phần khai báo lớp cho
MarsViewModel
, hãy thêm tham số hàm khởi tạo privatemarsPhotosRepository
thuộc kiểuMarsPhotosRepository
. 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(){
- 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()
- 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ượngViewModelProvider.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.
- 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)
}
}
}
- Mở tệp
theme/MarsPhotosApp.kt
, bên trong hàmMarsPhotosApp()
, cập nhậtviewModel()
để 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
.
- 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ộ
- 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).
- 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.
- Trong thư mục kiểm thử, hãy tạo một gói trong
com.example.marsphotos
có tên làfake
. - Tạo đối tượng Kotlin mới trong thư mục
fake
có tên làFakeDataSource
. - 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.
- Trong gói
fake
, hãy tạo một lớp mới có tên làFakeMarsApiService
. - Thiết lập lớp
FakeMarsApiService
để kế thừa từ giao diệnMarsApiService
.
class FakeMarsApiService : MarsApiService {
}
- Ghi đè hàm
getPhotos()
.
override suspend fun getPhotos(): List<MarsPhoto> {
}
- 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.
- Trong thư mục giả mạo, hãy tạo một lớp mới có tên là
NetworkMarsRepositoryTest
. - 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.
- Tạo một thực thể của
NetworkMarsPhotosRepository
và truyềnFakeMarsApiService
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.
- 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 đỏ.
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):
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.
- Sửa đổi trong
NetworkMarsRepositoryTest.kt
hàmnetworkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
thành một biểu thức.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
- Đặ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
.
- 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
.
- Tạo một lớp mới trong thư mục
fake
có tên làFakeNetworkMarsPhotosRepository
. - Mở rộng lớp này bằng giao diện
MarsPhotosRepository
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
- Ghi đè hàm
getMarsPhotos()
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
}
}
- Trả về
FakeDataSource.photosList
từ hàmgetMarsPhotos()
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
return FakeDataSource.photosList
}
}
Viết mã kiểm thử ViewModel
- Tạo một lớp mới tên là
MarsViewModelTest
. - 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()
- Đặ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{
}
- Trong phần thân hàm lambda của
runTest()
, hãy tạo một thực thể củaMarsViewModel
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()
)
}
- 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 đếnMarsPhotosRepository.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:
- Tạo một gói mới trong thư mục kiểm thử có tên là
rules
. - Trong thư mục quy tắc, hãy tạo một lớp mới có tên là
TestDispatcherRule
. - Mở rộng
TestDispatcherRule
bằngTestWatcher
. LớpTestWatcher
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(){
}
- Tạo tham số hàm khởi tạo
TestDispatcher
choTestDispatcherRule
.
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() {
}
- 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àmstarting()
của lớpTestWatcher
sẽ thực thi trước khi một quy trình kiểm thử nhất định thực thi. Ghi đè hàmstarting()
.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
}
}
- Thêm lệnh gọi vào
Dispatchers.setMain()
, truyền vàotestDispatcher
dưới dạng một đối số.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
}
- 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ứcfinished()
. Gọi hàmDispatchers.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.
- Mở tệp
MarsViewModelTest.kt
. - Trong lớp
MarsViewModelTest
, hãy tạo thực thể cho lớpTestDispatcherRule
và gán thực thể này cho thuộc tínhtestDispatcher
chỉ có thể đọc.
class MarsViewModelTest {
val testDispatcher = TestDispatcherRule()
...
}
- Để á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ínhtestDispatcher
.
class MarsViewModelTest {
@get:Rule
val testDispatcher = TestDispatcherRule()
...
}
- 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: