Computer >> Máy Tính >  >> Hệ thống >> Android

Cách xử lý các Dịch vụ web RESTful bằng Retrofit, OkHttp, Gson, Glide và Coroutines

Loạt ứng dụng Kriptofolio - Phần 5

Ngày nay, hầu hết mọi ứng dụng Android đều kết nối với internet để nhận / gửi dữ liệu. Bạn chắc chắn nên học cách xử lý các Dịch vụ Web RESTful, vì việc triển khai đúng cách của chúng là kiến ​​thức cốt lõi trong khi tạo các ứng dụng hiện đại.

Phần này sẽ phức tạp. Chúng tôi sẽ kết hợp nhiều thư viện cùng một lúc để có được kết quả hoạt động. Tôi sẽ không nói về cách Android gốc để xử lý các yêu cầu internet, bởi vì trong thế giới thực không ai sử dụng nó. Mọi ứng dụng tốt không cố gắng phát minh lại bánh xe mà thay vào đó sử dụng các thư viện của bên thứ ba phổ biến nhất để giải quyết các vấn đề chung. Sẽ là quá phức tạp để tạo lại chức năng mà các thư viện tốt này phải cung cấp.

Nội dung sê-ri

  • Giới thiệu:Lộ trình xây dựng ứng dụng Android hiện đại trong năm 2018–2019
  • Phần 1:Giới thiệu về các nguyên tắc SOLID
  • Phần 2:Cách bắt đầu xây dựng ứng dụng Android của bạn:tạo Mockups, giao diện người dùng và bố cục XML
  • Phần 3:Tất cả về Kiến trúc đó:khám phá các mẫu kiến ​​trúc khác nhau và cách sử dụng chúng trong ứng dụng của bạn
  • Phần 4:Cách triển khai Dependency Injection trong ứng dụng của bạn với Dagger 2
  • Phần 5:Xử lý các Dịch vụ Web RESTful bằng Retrofit, OkHttp, Gson, Glide và Coroutines (bạn đang ở đây)

Retrofit, OkHttp và Gson là gì?

Retrofit là một REST Client cho Java và Android. Thư viện này, theo ý kiến ​​của tôi, là thư viện quan trọng nhất để học, vì nó sẽ thực hiện công việc chính. Nó giúp việc truy xuất và tải lên JSON (hoặc dữ liệu có cấu trúc khác) tương đối dễ dàng thông qua dịch vụ web dựa trên REST.

Trong Retrofit, bạn định cấu hình bộ chuyển đổi nào được sử dụng để tuần tự hóa dữ liệu. Thông thường để tuần tự hóa và giải mã hóa các đối tượng đến và đi từ JSON, bạn sử dụng thư viện Java mã nguồn mở - Gson. Ngoài ra, nếu cần, bạn có thể thêm bộ chuyển đổi tùy chỉnh vào Retrofit để xử lý XML hoặc các giao thức khác.

Để thực hiện các yêu cầu HTTP, Retrofit sử dụng thư viện OkHttp. OkHttp là một ứng dụng HTTP / SPDY thuần túy chịu trách nhiệm về mọi hoạt động mạng cấp thấp, thao tác lưu vào bộ nhớ đệm, yêu cầu và phản hồi. Ngược lại, Retrofit là một bản xây dựng trừu tượng REST cấp cao trên OkHttp. Trang bị thêm được kết hợp chặt chẽ với OkHttp và sử dụng nó một cách chuyên sâu.

Bây giờ bạn biết rằng mọi thứ đều có liên quan chặt chẽ với nhau, chúng ta sẽ sử dụng tất cả 3 thư viện này cùng một lúc. Mục tiêu đầu tiên của chúng tôi là lấy tất cả danh sách tiền điện tử bằng cách sử dụng Retrofit từ Internet. Chúng tôi sẽ sử dụng một lớp đánh chặn OkHttp đặc biệt để xác thực API CoinMarketCap khi thực hiện cuộc gọi đến máy chủ. Chúng tôi sẽ lấy lại kết quả dữ liệu JSON và sau đó chuyển đổi nó bằng thư viện Gson.

Thiết lập nhanh cho Retrofit 2 chỉ để dùng thử trước

Khi học một điều gì đó mới, tôi muốn thử nó trong thực tế ngay khi có thể. Chúng tôi sẽ áp dụng cách tiếp cận tương tự với Retrofit 2 để bạn hiểu nó nhanh hơn. Ngay bây giờ, đừng lo lắng về chất lượng mã hoặc bất kỳ nguyên tắc lập trình hoặc tối ưu hóa nào - chúng tôi sẽ chỉ viết một số mã để làm cho Retrofit 2 hoạt động trong dự án của chúng tôi và thảo luận về chức năng của nó.

Làm theo các bước sau để thiết lập Retrofit 2 trên dự án ứng dụng My Crypto Coins:

Trước tiên, cấp quyền INTERNET cho ứng dụng

Chúng tôi sẽ thực thi các yêu cầu HTTP trên một máy chủ có thể truy cập qua Internet. Cấp quyền này bằng cách thêm các dòng sau vào tệp kê khai của bạn:

<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.baruckis.mycryptocoins">

    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

Sau đó, bạn nên thêm các phần phụ thuộc thư viện

Tìm phiên bản Retrofit mới nhất. Ngoài ra, bạn nên biết rằng Retrofit không đi kèm với bộ chuyển đổi JSON tích hợp. Vì chúng tôi sẽ nhận được phản hồi ở định dạng JSON, nên chúng tôi cũng cần đưa trình chuyển đổi vào trong phần phụ thuộc theo cách thủ công. Chúng tôi sẽ sử dụng phiên bản Gson của trình chuyển đổi JSON mới nhất của Google. Hãy thêm những dòng này vào tệp gradle của bạn:

// 3rd party
// HTTP client - Retrofit with OkHttp
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
// JSON converter Gson for JSON to Java object mapping
implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit"

Như bạn nhận thấy từ nhận xét của tôi, phụ thuộc OkHttp đã được vận chuyển cùng với phụ thuộc Retrofit 2. Các phiên bản chỉ là một tệp gradle riêng biệt để thuận tiện:

def versions = [:]

versions.retrofit = "2.4.0"

ext.versions = versions

Tiếp theo, thiết lập giao diện Trang bị thêm

Đó là giao diện khai báo các yêu cầu của chúng tôi và các loại yêu cầu của chúng. Ở đây chúng tôi xác định API ở phía máy khách.

/**
 * REST API access points.
 */
interface ApiService {

    // The @GET annotation tells retrofit that this request is a get type request.
    // The string value tells retrofit that the path of this request is
    // baseUrl + v1/cryptocurrency/listings/latest + query parameter.
    @GET("v1/cryptocurrency/listings/latest")
    // Annotation @Query is used to define query parameter for request. Finally the request url will
    // look like that https://sandbox-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?convert=EUR.
    fun getAllCryptocurrencies(@Query("convert") currency: String): Call<CryptocurrenciesLatest>
    // The return type for this function is Call with its type CryptocurrenciesLatest.
}

Và thiết lập lớp dữ liệu

Các lớp dữ liệu là POJO (Đối tượng Java cũ thuần túy) đại diện cho phản hồi của các lệnh gọi API mà chúng tôi sẽ thực hiện.

/**
 * Data class to handle the response from the server.
 */
data class CryptocurrenciesLatest(
        val status: Status,
        val data: List<Data>
) {

    data class Data(
            val id: Int,
            val name: String,
            val symbol: String,
            val slug: String,
            // The annotation to a model property lets you pass the serialized and deserialized
            // name as a string. This is useful if you don't want your model class and the JSON
            // to have identical naming.
            @SerializedName("circulating_supply")
            val circulatingSupply: Double,
            @SerializedName("total_supply")
            val totalSupply: Double,
            @SerializedName("max_supply")
            val maxSupply: Double,
            @SerializedName("date_added")
            val dateAdded: String,
            @SerializedName("num_market_pairs")
            val numMarketPairs: Int,
            @SerializedName("cmc_rank")
            val cmcRank: Int,
            @SerializedName("last_updated")
            val lastUpdated: String,
            val quote: Quote
    ) {

        data class Quote(
                // For additional option during deserialization you can specify value or alternative
                // values. Gson will check the JSON for all names we specify and try to find one to
                // map it to the annotated property.
                @SerializedName(value = "USD", alternate = ["AUD", "BRL", "CAD", "CHF", "CLP",
                    "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
                    "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD",
                    "THB", "TRY", "TWD", "ZAR"])
                val currency: Currency
        ) {

            data class Currency(
                    val price: Double,
                    @SerializedName("volume_24h")
                    val volume24h: Double,
                    @SerializedName("percent_change_1h")
                    val percentChange1h: Double,
                    @SerializedName("percent_change_24h")
                    val percentChange24h: Double,
                    @SerializedName("percent_change_7d")
                    val percentChange7d: Double,
                    @SerializedName("market_cap")
                    val marketCap: Double,
                    @SerializedName("last_updated")
                    val lastUpdated: String
            )
        }
    }

    data class Status(
            val timestamp: String,
            @SerializedName("error_code")
            val errorCode: Int,
            @SerializedName("error_message")
            val errorMessage: String,
            val elapsed: Int,
            @SerializedName("credit_count")
            val creditCount: Int
    )
}

Tạo một lớp đánh chặn đặc biệt để xác thực khi thực hiện cuộc gọi đến máy chủ

Đây là trường hợp cụ thể đối với bất kỳ API nào yêu cầu xác thực để nhận được phản hồi thành công. Bộ chặn là một cách mạnh mẽ để tùy chỉnh các yêu cầu của bạn. Chúng tôi sẽ chặn yêu cầu thực tế và thêm tiêu đề yêu cầu riêng lẻ, tiêu đề này sẽ xác thực cuộc gọi bằng Khóa API do Cổng nhà phát triển API chuyên nghiệp CoinMarketCap cung cấp. Để có được của bạn, bạn cần đăng ký ở đó.

/**
 * Interceptor used to intercept the actual request and
 * to supply your API Key in REST API calls via a custom header.
 */
class AuthenticationInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val newRequest = chain.request().newBuilder()
                // TODO: Use your API Key provided by CoinMarketCap Professional API Developer Portal.
                .addHeader("X-CMC_PRO_API_KEY", "CMC_PRO_API_KEY")
                .build()

        return chain.proceed(newRequest)
    }
}

Cuối cùng, thêm mã này vào hoạt động của chúng tôi để xem Retrofit hoạt động

Tôi muốn làm bẩn bàn tay của bạn càng sớm càng tốt, vì vậy tôi đặt mọi thứ vào một chỗ. Đây không phải là cách chính xác nhưng là cách nhanh nhất thay vào đó chỉ để xem nhanh kết quả trực quan.

class AddSearchActivity : AppCompatActivity(), Injectable {

    private lateinit var listView: ListView
    private lateinit var listAdapter: AddSearchListAdapter

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ...

        // Later we will setup Retrofit correctly, but for now we do all in one place just for quick start.
        setupRetrofitTemporarily()
    }

    ...

    private fun setupRetrofitTemporarily() {

        // We need to prepare a custom OkHttp client because need to use our custom call interceptor.
        // to be able to authenticate our requests.
        val builder = OkHttpClient.Builder()
        // We add the interceptor to OkHttpClient.
        // It will add authentication headers to every call we make.
        builder.interceptors().add(AuthenticationInterceptor())
        val client = builder.build()


        val api = Retrofit.Builder() // Create retrofit builder.
                .baseUrl("https://sandbox-api.coinmarketcap.com/") // Base url for the api has to end with a slash.
                .addConverterFactory(GsonConverterFactory.create()) // Use GSON converter for JSON to POJO object mapping.
                .client(client) // Here we set the custom OkHttp client we just created.
                .build().create(ApiService::class.java) // We create an API using the interface we defined.


        val adapterData: MutableList<Cryptocurrency> = ArrayList<Cryptocurrency>()

        val currentFiatCurrencyCode = "EUR"

        // Let's make asynchronous network request to get all latest cryptocurrencies from the server.
        // For query parameter we pass "EUR" as we want to get prices in euros.
        val call = api.getAllCryptocurrencies("EUR")
        val result = call.enqueue(object : Callback<CryptocurrenciesLatest> {

            // You will always get a response even if something wrong went from the server.
            override fun onFailure(call: Call<CryptocurrenciesLatest>, t: Throwable) {

                Snackbar.make(findViewById(android.R.id.content),
                        // Throwable will let us find the error if the call failed.
                        "Call failed! " + t.localizedMessage,
                        Snackbar.LENGTH_INDEFINITE).show()
            }

            override fun onResponse(call: Call<CryptocurrenciesLatest>, response: Response<CryptocurrenciesLatest>) {

                // Check if the response is successful, which means the request was successfully
                // received, understood, accepted and returned code in range [200..300).
                if (response.isSuccessful) {

                    // If everything is OK, let the user know that.
                    Toast.makeText(this@AddSearchActivity, "Call OK.", Toast.LENGTH_LONG).show();

                    // Than quickly map server response data to the ListView adapter.
                    val cryptocurrenciesLatest: CryptocurrenciesLatest? = response.body()
                    cryptocurrenciesLatest!!.data.forEach {
                        val cryptocurrency = Cryptocurrency(it.name, it.cmcRank.toShort(),
                                0.0, it.symbol, currentFiatCurrencyCode, it.quote.currency.price,
                                0.0, it.quote.currency.percentChange1h,
                                it.quote.currency.percentChange7d, it.quote.currency.percentChange24h,
                                0.0)
                        adapterData.add(cryptocurrency)
                    }

                    listView.visibility = View.VISIBLE
                    listAdapter.setData(adapterData)

                }
                // Else if the response is unsuccessful it will be defined by some special HTTP
                // error code, which we can show for the user.
                else Snackbar.make(findViewById(android.R.id.content),
                        "Call error with HTTP status code " + response.code() + "!",
                        Snackbar.LENGTH_INDEFINITE).show()

            }

        })

    }

   ...
}

Bạn có thể khám phá mã ở đây. Hãy nhớ rằng đây chỉ là phiên bản triển khai đơn giản ban đầu để bạn hiểu rõ hơn.

Thiết lập chính xác cuối cùng cho Retrofit 2 với OkHttp 3 và Gson

Được rồi, sau một thử nghiệm nhanh, đã đến lúc đưa việc triển khai Trang bị thêm này lên cấp độ tiếp theo. Chúng tôi đã lấy dữ liệu thành công nhưng không chính xác. Chúng tôi đang thiếu các trạng thái như tải, lỗi và thành công. Mã của chúng tôi được trộn lẫn mà không có sự tách biệt của mối quan tâm. Đó là một sai lầm phổ biến khi viết tất cả mã của bạn trong một hoạt động hoặc một phân đoạn. Lớp hoạt động của chúng tôi dựa trên giao diện người dùng và chỉ nên chứa logic xử lý các tương tác với giao diện người dùng và hệ điều hành.

Thực sự thì sau khi thiết lập nhanh chóng này, tôi đã làm việc rất nhiều và thực hiện nhiều thay đổi. Không có điểm nào để đặt tất cả các mã đã được thay đổi trong bài viết. Thay vào đó, bạn nên duyệt qua repo mã Phần 5 cuối cùng tại đây. Tôi đã nhận xét mọi thứ rất tốt và mã của tôi phải rõ ràng để bạn hiểu. Nhưng tôi sẽ nói về những điều quan trọng nhất mà tôi đã làm và tại sao tôi lại làm chúng.

Bước đầu tiên để cải thiện là bắt đầu sử dụng Dependency Injection. Hãy nhớ từ phần trước, chúng ta đã có Dagger 2 được thực hiện bên trong dự án một cách chính xác. Vì vậy, tôi đã sử dụng nó để thiết lập Trang bị thêm.

/**
 * AppModule will provide app-wide dependencies for a part of the application.
 * It should initialize objects used across our application, such as Room database, Retrofit, Shared Preference, etc.
 */
@Module(includes = [ViewModelsModule::class])
class AppModule() {
    ...

    @Provides
    @Singleton
    fun provideHttpClient(): OkHttpClient {
        // We need to prepare a custom OkHttp client because need to use our custom call interceptor.
        // to be able to authenticate our requests.
        val builder = OkHttpClient.Builder()
        // We add the interceptor to OkHttpClient.
        // It will add authentication headers to every call we make.
        builder.interceptors().add(AuthenticationInterceptor())

        // Configure this client not to retry when a connectivity problem is encountered.
        builder.retryOnConnectionFailure(false)

        // Log requests and responses.
        // Add logging as the last interceptor, because this will also log the information which
        // you added or manipulated with previous interceptors to your request.
        builder.interceptors().add(HttpLoggingInterceptor().apply {
            // For production environment to enhance apps performance we will be skipping any
            // logging operation. We will show logs just for debug builds.
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
        })
        return builder.build()
    }

    @Provides
    @Singleton
    fun provideApiService(httpClient: OkHttpClient): ApiService {
        return Retrofit.Builder() // Create retrofit builder.
                .baseUrl(API_SERVICE_BASE_URL) // Base url for the api has to end with a slash.
                .addConverterFactory(GsonConverterFactory.create()) // Use GSON converter for JSON to POJO object mapping.
                .addCallAdapterFactory(LiveDataCallAdapterFactory())
                .client(httpClient) // Here we set the custom OkHttp client we just created.
                .build().create(ApiService::class.java) // We create an API using the interface we defined.
    }

    ...
}

Bây giờ như bạn thấy, Retrofit được tách ra khỏi lớp hoạt động như lẽ phải. Nó sẽ chỉ được khởi chạy một lần và được sử dụng trên toàn ứng dụng.

Như bạn có thể đã nhận thấy trong khi tạo phiên bản trình tạo Retrofit, chúng tôi đã thêm một bộ điều hợp cuộc gọi Retrofit đặc biệt bằng cách sử dụng addCallAdapterFactory . Theo mặc định, Retrofit trả về Call<T> , nhưng đối với dự án của chúng tôi, chúng tôi yêu cầu nó trả về LiveData<T> loại hình. Để làm được điều đó, chúng ta cần thêm LiveDataCallAdapter bằng cách sử dụng LiveDataCallAdapterFactory .

/**
 * A Retrofit adapter that converts the Call into a LiveData of ApiResponse.
 * @param <R>
</R> */
class LiveDataCallAdapter<R>(private val responseType: Type) :
        CallAdapter<R, LiveData<ApiResponse<R>>> {

    override fun responseType() = responseType

    override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> {
        return object : LiveData<ApiResponse<R>>() {
            private var started = AtomicBoolean(false)
            override fun onActive() {
                super.onActive()
                if (started.compareAndSet(false, true)) {
                    call.enqueue(object : Callback<R> {
                        override fun onResponse(call: Call<R>, response: Response<R>) {
                            postValue(ApiResponse.create(response))
                        }

                        override fun onFailure(call: Call<R>, throwable: Throwable) {
                            postValue(ApiResponse.create(throwable))
                        }
                    })
                }
            }
        }
    }
}
class LiveDataCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
            returnType: Type,
            annotations: Array<Annotation>,
            retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (CallAdapter.Factory.getRawType(returnType) != LiveData::class.java) {
            return null
        }
        val observableType = CallAdapter.Factory.getParameterUpperBound(0, returnType as ParameterizedType)
        val rawObservableType = CallAdapter.Factory.getRawType(observableType)
        if (rawObservableType != ApiResponse::class.java) {
            throw IllegalArgumentException("type must be a resource")
        }
        if (observableType !is ParameterizedType) {
            throw IllegalArgumentException("resource must be parameterized")
        }
        val bodyType = CallAdapter.Factory.getParameterUpperBound(0, observableType)
        return LiveDataCallAdapter<Any>(bodyType)
    }
}

Bây giờ chúng ta sẽ nhận được LiveData<T> thay vì Call<T> là kiểu trả về từ các phương thức dịch vụ Retrofit được xác định trong ApiService giao diện.

Một bước quan trọng khác cần thực hiện là bắt đầu sử dụng mẫu Kho lưu trữ. Tôi đã nói về nó trong Phần 3. Kiểm tra lược đồ kiến ​​trúc MVVM của chúng tôi từ bài đăng đó để nhớ nó đi đâu.

Cách xử lý các Dịch vụ web RESTful bằng Retrofit, OkHttp, Gson, Glide và Coroutines

Như bạn thấy trong hình, Kho lưu trữ là một lớp riêng biệt cho dữ liệu. Đó là nguồn liên hệ duy nhất của chúng tôi để nhận hoặc gửi dữ liệu. Khi chúng tôi sử dụng Kho lưu trữ, chúng tôi đang tuân theo nguyên tắc tách biệt các mối quan tâm. Chúng tôi có thể có các nguồn dữ liệu khác nhau (như trong trường hợp của chúng tôi là dữ liệu liên tục từ cơ sở dữ liệu SQLite và dữ liệu từ các dịch vụ web), nhưng Kho lưu trữ sẽ luôn là nguồn trung thực duy nhất cho tất cả dữ liệu ứng dụng.

Thay vì liên lạc trực tiếp với việc triển khai Retrofit của chúng tôi, chúng tôi sẽ sử dụng Kho lưu trữ cho việc đó. Đối với mỗi loại thực thể, chúng ta sẽ có một Kho lưu trữ riêng.

/**
 * The class for managing multiple data sources.
 */
@Singleton
class CryptocurrencyRepository @Inject constructor(
        private val context: Context,
        private val appExecutors: AppExecutors,
        private val myCryptocurrencyDao: MyCryptocurrencyDao,
        private val cryptocurrencyDao: CryptocurrencyDao,
        private val api: ApiService,
        private val sharedPreferences: SharedPreferences
) {

    // Just a simple helper variable to store selected fiat currency code during app lifecycle.
    // It is needed for main screen currency spinner. We set it to be same as in shared preferences.
    var selectedFiatCurrencyCode: String = getCurrentFiatCurrencyCode()


    ...
  

    // The Resource wrapping of LiveData is useful to update the UI based upon the state.
    fun getAllCryptocurrencyLiveDataResourceList(fiatCurrencyCode: String, shouldFetch: Boolean = false, callDelay: Long = 0): LiveData<Resource<List<Cryptocurrency>>> {
        return object : NetworkBoundResource<List<Cryptocurrency>, CoinMarketCap<List<CryptocurrencyLatest>>>(appExecutors) {

            // Here we save the data fetched from web-service.
            override fun saveCallResult(item: CoinMarketCap<List<CryptocurrencyLatest>>) {

                val list = getCryptocurrencyListFromResponse(fiatCurrencyCode, item.data, item.status?.timestamp)

                cryptocurrencyDao.reloadCryptocurrencyList(list)
                myCryptocurrencyDao.reloadMyCryptocurrencyList(list)
            }

            // Returns boolean indicating if to fetch data from web or not, true means fetch the data from web.
            override fun shouldFetch(data: List<Cryptocurrency>?): Boolean {
                return data == null || shouldFetch
            }

            override fun fetchDelayMillis(): Long {
                return callDelay
            }

            // Contains the logic to get data from the Room database.
            override fun loadFromDb(): LiveData<List<Cryptocurrency>> {

                return Transformations.switchMap(cryptocurrencyDao.getAllCryptocurrencyLiveDataList()) { data ->
                    if (data.isEmpty()) {
                        AbsentLiveData.create()
                    } else {
                        cryptocurrencyDao.getAllCryptocurrencyLiveDataList()
                    }
                }
            }

            // Contains the logic to get data from web-service using Retrofit.
            override fun createCall(): LiveData<ApiResponse<CoinMarketCap<List<CryptocurrencyLatest>>>> = api.getAllCryptocurrencies(fiatCurrencyCode)

        }.asLiveData()
    }


    ...


    fun getCurrentFiatCurrencyCode(): String {
        return sharedPreferences.getString(context.resources.getString(R.string.pref_fiat_currency_key), context.resources.getString(R.string.pref_default_fiat_currency_value))
                ?: context.resources.getString(R.string.pref_default_fiat_currency_value)
    }


    ...


    private fun getCryptocurrencyListFromResponse(fiatCurrencyCode: String, responseList: List<CryptocurrencyLatest>?, timestamp: Date?): ArrayList<Cryptocurrency> {

        val cryptocurrencyList: MutableList<Cryptocurrency> = ArrayList()

        responseList?.forEach {
            val cryptocurrency = Cryptocurrency(it.id, it.name, it.cmcRank.toShort(),
                    it.symbol, fiatCurrencyCode, it.quote.currency.price,
                    it.quote.currency.percentChange1h,
                    it.quote.currency.percentChange7d, it.quote.currency.percentChange24h, timestamp)
            cryptocurrencyList.add(cryptocurrency)
        }

        return cryptocurrencyList as ArrayList<Cryptocurrency>
    }

}

Như bạn nhận thấy trong CryptocurrencyRepository mã lớp, tôi đang sử dụng NetworkBoundResource lớp trừu tượng. Nó là gì và tại sao chúng ta cần nó?

NetworkBoundResource là một lớp trợ giúp nhỏ nhưng rất quan trọng sẽ cho phép chúng tôi duy trì sự đồng bộ hóa giữa cơ sở dữ liệu cục bộ và dịch vụ web. Mục tiêu của chúng tôi là xây dựng một ứng dụng hiện đại sẽ hoạt động trơn tru ngay cả khi thiết bị của chúng tôi ngoại tuyến. Ngoài ra với sự trợ giúp của lớp này, chúng tôi sẽ có thể hiển thị các trạng thái mạng khác nhau như lỗi hoặc tải cho người dùng một cách trực quan.

NetworkBoundResource bắt đầu bằng cách quan sát cơ sở dữ liệu cho tài nguyên. Khi mục nhập được tải lần đầu tiên từ cơ sở dữ liệu, nó sẽ kiểm tra xem kết quả có đủ tốt để gửi đi hay không hoặc liệu nó có nên được tìm nạp lại từ mạng hay không. Lưu ý rằng cả hai trường hợp này đều có thể xảy ra cùng một lúc, do bạn có thể muốn hiển thị dữ liệu đã lưu trong bộ nhớ cache trong khi cập nhật dữ liệu đó từ mạng.

Nếu cuộc gọi mạng hoàn tất thành công, nó sẽ lưu phản hồi vào cơ sở dữ liệu và khởi tạo lại luồng. Nếu yêu cầu mạng không thành công, NetworkBoundResource gửi trực tiếp một thất bại.

/**
 * A generic class that can provide a resource backed by both the sqlite database and the network.
 *
 *
 * You can read more about it in the [Architecture
 * Guide](https://developer.android.com/arch).
 * @param <ResultType> - Type for the Resource data.
 * @param <RequestType> - Type for the API response.
</RequestType></ResultType> */

// It defines two type parameters, ResultType and RequestType,
// because the data type returned from the API might not match the data type used locally.
abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor(private val appExecutors: AppExecutors) {

    // The final result LiveData.
    private val result = MediatorLiveData<Resource<ResultType>>()

    init {
        // Send loading state to UI.
        result.value = Resource.loading(null)
        @Suppress("LeakingThis")
        val dbSource = loadFromDb()
        result.addSource(dbSource) { data ->
            result.removeSource(dbSource)
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource)
            } else {
                result.addSource(dbSource) { newData ->
                    setValue(Resource.successDb(newData))
                }
            }
        }
    }

    @MainThread
    private fun setValue(newValue: Resource<ResultType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    // Fetch the data from network and persist into DB and then send it back to UI.
    private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
        val apiResponse = createCall()
        // We re-attach dbSource as a new source, it will dispatch its latest value quickly.
        result.addSource(dbSource) { newData ->
            setValue(Resource.loading(newData))
        }

        // Create inner function as we want to delay it.
        fun fetch() {
            result.addSource(apiResponse) { response ->
                result.removeSource(apiResponse)
                result.removeSource(dbSource)
                when (response) {
                    is ApiSuccessResponse -> {
                        appExecutors.diskIO().execute {
                            saveCallResult(processResponse(response))
                            appExecutors.mainThread().execute {
                                // We specially request a new live data,
                                // otherwise we will get immediately last cached value,
                                // which may not be updated with latest results received from network.
                                result.addSource(loadFromDb()) { newData ->
                                    setValue(Resource.successNetwork(newData))
                                }
                            }
                        }
                    }
                    is ApiEmptyResponse -> {
                        appExecutors.mainThread().execute {
                            // reload from disk whatever we had
                            result.addSource(loadFromDb()) { newData ->
                                setValue(Resource.successDb(newData))
                            }
                        }
                    }
                    is ApiErrorResponse -> {
                        onFetchFailed()
                        result.addSource(dbSource) { newData ->
                            setValue(Resource.error(response.errorMessage, newData))
                        }
                    }
                }
            }
        }

        // Add delay before call if needed.
        val delay = fetchDelayMillis()
        if (delay > 0) {
            Handler().postDelayed({ fetch() }, delay)
        } else fetch()

    }

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    protected open fun onFetchFailed() {}

    // Returns a LiveData object that represents the resource that's implemented
    // in the base class.
    fun asLiveData() = result as LiveData<Resource<ResultType>>

    @WorkerThread
    protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body

    // Called to save the result of the API response into the database.
    @WorkerThread
    protected abstract fun saveCallResult(item: RequestType)

    // Called with the data in the database to decide whether to fetch
    // potentially updated data from the network.
    @MainThread
    protected abstract fun shouldFetch(data: ResultType?): Boolean

    // Make a call to the server after some delay for better user experience.
    protected open fun fetchDelayMillis(): Long = 0

    // Called to get the cached data from the database.
    @MainThread
    protected abstract fun loadFromDb(): LiveData<ResultType>

    // Called to create the API call.
    @MainThread
    protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

Dưới mui xe, NetworkBoundResource lớp được tạo bằng cách sử dụng MediatorLiveData và khả năng quan sát nhiều nguồn LiveData cùng một lúc. Ở đây chúng ta có hai nguồn LiveData:cơ sở dữ liệu và phản hồi cuộc gọi mạng. Cả hai LiveData đó đều được bao bọc thành một MediatorLiveData, được hiển thị bởi NetworkBoundResource .

Cách xử lý các Dịch vụ web RESTful bằng Retrofit, OkHttp, Gson, Glide và Coroutines
NetworkBoundResource

Hãy xem xét kỹ hơn cách thức NetworkBoundResource sẽ hoạt động trong ứng dụng của chúng tôi. Hãy tưởng tượng người dùng sẽ khởi chạy ứng dụng và nhấp vào nút hành động nổi ở góc dưới cùng bên phải. Ứng dụng sẽ khởi chạy màn hình thêm tiền điện tử. Bây giờ chúng ta có thể phân tích NetworkBoundResource cách sử dụng bên trong nó.

Nếu ứng dụng được cài đặt mới và là lần khởi chạy đầu tiên, thì sẽ không có bất kỳ dữ liệu nào được lưu trữ bên trong cơ sở dữ liệu cục bộ. Vì không có dữ liệu để hiển thị, giao diện người dùng của thanh tiến trình tải sẽ được hiển thị. Trong khi đó, ứng dụng sẽ thực hiện một cuộc gọi yêu cầu đến máy chủ thông qua một dịch vụ web để nhận tất cả danh sách tiền điện tử.

Nếu phản hồi không thành công thì giao diện người dùng thông báo lỗi sẽ được hiển thị với khả năng thử lại cuộc gọi bằng cách nhấn một nút. Cuối cùng khi một cuộc gọi yêu cầu thành công, thì dữ liệu phản hồi sẽ được lưu vào cơ sở dữ liệu SQLite cục bộ.

Nếu chúng ta quay lại cùng một màn hình vào lần sau, ứng dụng sẽ tải dữ liệu từ cơ sở dữ liệu thay vì thực hiện lại cuộc gọi với internet. Nhưng người dùng có thể yêu cầu cập nhật dữ liệu mới bằng cách triển khai chức năng kéo để làm mới. Thông tin dữ liệu cũ sẽ được hiển thị trong khi cuộc gọi mạng đang diễn ra. Tất cả điều này được thực hiện với sự trợ giúp của NetworkBoundResource .

Một lớp khác được sử dụng trong Kho lưu trữ của chúng tôi và LiveDataCallAdapter nơi tất cả "điều kỳ diệu" xảy ra là ApiResponse . Trên thực tế ApiResponse chỉ là một trình bao bọc chung đơn giản xung quanh Retrofit2.Response lớp chuyển đổi từng phản hồi thành một phiên bản của LiveData.

/**
 * Common class used by API responses. ApiResponse is a simple wrapper around the Retrofit2.Call
 * class that convert responses to instances of LiveData.
 * @param <CoinMarketCapType> the type of the response object
</T> */
@Suppress("unused") // T is used in extending classes
sealed class ApiResponse<CoinMarketCapType> {
    companion object {
        fun <CoinMarketCapType> create(error: Throwable): ApiErrorResponse<CoinMarketCapType> {
            return ApiErrorResponse(error.message ?: "Unknown error.")
        }

        fun <CoinMarketCapType> create(response: Response<CoinMarketCapType>): ApiResponse<CoinMarketCapType> {
            return if (response.isSuccessful) {
                val body = response.body()
                if (body == null || response.code() == 204) {
                    ApiEmptyResponse()
                } else {
                    ApiSuccessResponse(body = body)
                }
            } else {

                // Convert error response to JSON object.
                val gson = Gson()
                val type = object : TypeToken<CoinMarketCap<CoinMarketCapType>>() {}.type
                val errorResponse: CoinMarketCap<CoinMarketCapType> = gson.fromJson(response.errorBody()!!.charStream(), type)

                val msg = errorResponse.status?.errorMessage ?: errorResponse.message
                val errorMsg = if (msg.isNullOrEmpty()) {
                    response.message()
                } else {
                    msg
                }
                ApiErrorResponse(errorMsg ?: "Unknown error.")
            }
        }
    }
}

/**
 * Separate class for HTTP 204 resposes so that we can make ApiSuccessResponse's body non-null.
 */
class ApiEmptyResponse<CoinMarketCapType> : ApiResponse<CoinMarketCapType>()

data class ApiSuccessResponse<CoinMarketCapType>(val body: CoinMarketCapType) : ApiResponse<CoinMarketCapType>()

data class ApiErrorResponse<CoinMarketCapType>(val errorMessage: String) : ApiResponse<CoinMarketCapType>()

Bên trong lớp trình bao bọc này, nếu phản hồi của chúng tôi có lỗi, chúng tôi sử dụng thư viện Gson để chuyển đổi lỗi thành đối tượng JSON. Tuy nhiên, nếu phản hồi thành công, thì bộ chuyển đổi Gson cho ánh xạ đối tượng JSON sang POJO sẽ được sử dụng. Chúng tôi đã thêm nó khi tạo phiên bản trình tạo trang bị thêm với GsonConverterFactory bên trong Dagger AppModule hàm provideApiService .

Lướt để tải hình ảnh

Glide là gì? Từ tài liệu:

Glide là một khung tải hình ảnh và quản lý phương tiện mã nguồn mở nhanh chóng và hiệu quả dành cho Android, gói phần mềm giải mã phương tiện, bộ nhớ đệm và bộ nhớ đệm đĩa cũng như tổng hợp tài nguyên vào một giao diện đơn giản và dễ sử dụng.
Trọng tâm chính của Glide là làm cho việc cuộn bất kỳ loại danh sách hình ảnh nào mượt mà và nhanh nhất có thể, nhưng nó cũng hiệu quả đối với hầu hết mọi trường hợp bạn cần tìm nạp, thay đổi kích thước và hiển thị hình ảnh từ xa.

Nghe có vẻ giống như một thư viện phức tạp cung cấp nhiều tính năng hữu ích mà bạn không muốn tự mình phát triển tất cả. Trong ứng dụng My Crypto Coins, chúng tôi có một số màn hình danh sách mà chúng tôi cần hiển thị nhiều biểu trưng tiền điện tử - ảnh được chụp từ internet cùng một lúc - và vẫn đảm bảo trải nghiệm cuộn mượt mà cho người dùng. Vì vậy, thư viện này hoàn toàn phù hợp với nhu cầu của chúng tôi. Ngoài ra, thư viện này rất phổ biến đối với các nhà phát triển Android.

Các bước thiết lập dự án ứng dụng Glide on My Crypto Coins:

Khai báo phần phụ thuộc

Tải phiên bản Glide mới nhất. Một lần nữa, các phiên bản là một tệp riêng biệt versions.gradle cho dự án.

// Glide
implementation "com.github.bumptech.glide:glide:$versions.glide"
kapt "com.github.bumptech.glide:compiler:$versions.glide"
// Glide's OkHttp3 integration.
implementation "com.github.bumptech.glide:okhttp3-integration:$versions.glide"+"@aar"

Bởi vì chúng tôi muốn sử dụng thư viện mạng OkHttp trong dự án của mình cho tất cả các hoạt động mạng, chúng tôi cần bao gồm tích hợp Glide cụ thể cho nó thay vì tích hợp mặc định. Ngoài ra, vì Glide sẽ thực hiện một yêu cầu mạng để tải hình ảnh qua internet, chúng tôi cần bao gồm quyền INTERNET trong AndroidManifest.xml của chúng tôi tệp - nhưng chúng tôi đã làm điều đó với thiết lập Trang bị thêm.

Tạo AppGlideModule

Glide v4, mà chúng tôi sẽ sử dụng, cung cấp một API được tạo cho Ứng dụng. Nó sẽ sử dụng bộ xử lý chú thích để tạo ra một API cho phép các ứng dụng mở rộng API của Glide và bao gồm các thành phần được cung cấp bởi các thư viện tích hợp. Đối với bất kỳ ứng dụng nào để truy cập API Glide đã tạo, chúng tôi cần bao gồm một AppGlideModule được chú thích thích hợp thực hiện. Chỉ có thể có một triển khai duy nhất của API đã tạo và chỉ một AppGlideModule mỗi ứng dụng.

Hãy tạo một lớp mở rộng AppGlideModule ở đâu đó trong dự án ứng dụng của bạn:

/**
 * Glide v4 uses an annotation processor to generate an API that allows applications to access all
 * options in RequestBuilder, RequestOptions and any included integration libraries in a single
 * fluent API.
 *
 * The generated API serves two purposes:
 * Integration libraries can extend Glide’s API with custom options.
 * Applications can extend Glide’s API by adding methods that bundle commonly used options.
 *
 * Although both of these tasks can be accomplished by hand by writing custom subclasses of
 * RequestOptions, doing so is challenging and produces a less fluent API.
 */
@GlideModule
class AppGlideModule : AppGlideModule()

Ngay cả khi ứng dụng của chúng tôi không thay đổi bất kỳ cài đặt bổ sung nào hoặc triển khai bất kỳ phương pháp nào trong AppGlideModule , chúng tôi vẫn cần phải triển khai nó để sử dụng Glide. Bạn không bắt buộc phải triển khai bất kỳ phương pháp nào trong AppGlideModule để tạo ra API. Bạn có thể để trống lớp miễn là nó mở rộng AppGlideModule và được chú thích bằng @GlideModule .

Sử dụng API do Glide tạo

Khi sử dụng AppGlideModule , các ứng dụng có thể sử dụng API bằng cách bắt đầu tất cả các lần tải với GlideApp.with() . Đây là mã cho thấy cách tôi đã sử dụng Glide để tải và hiển thị biểu trưng tiền điện tử trong màn hình thêm tiền điện tử trong danh sách tất cả tiền điện tử.

class AddSearchListAdapter(val context: Context, private val cryptocurrencyClickCallback: ((Cryptocurrency) -> Unit)?) : BaseAdapter() {

    ...

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        ...

        val itemBinding: ActivityAddSearchListItemBinding

        ...

        // We make an Uri of image that we need to load. Every image unique name is its id.
        val imageUri = Uri.parse(CRYPTOCURRENCY_IMAGE_URL).buildUpon()
                .appendPath(CRYPTOCURRENCY_IMAGE_SIZE_PX)
                .appendPath(cryptocurrency.id.toString() + CRYPTOCURRENCY_IMAGE_FILE)
                .build()

        // Glide generated API from AppGlideModule.
        GlideApp
                // We need to provide context to make a call.
                .with(itemBinding.root)
                // Here you specify which image should be loaded by providing Uri.
                .load(imageUri)
                // The way you combine and execute multiple transformations.
                // WhiteBackground is our own implemented custom transformation.
                // CircleCrop is default transformation that Glide ships with.
                .transform(MultiTransformation(WhiteBackground(), CircleCrop()))
                // The target ImageView your image is supposed to get displayed in.
                .into(itemBinding.itemImageIcon.imageview_front)

        ...

        return itemBinding.root
    }

    ...

}

Như bạn thấy, bạn có thể bắt đầu sử dụng Glide chỉ với vài dòng mã và để nó thực hiện tất cả công việc khó khăn cho bạn. Nó khá đơn giản.

Kotlin Coroutines

Trong khi xây dựng ứng dụng này, chúng tôi sẽ phải đối mặt với các tình huống khi chúng tôi sẽ chạy các tác vụ tốn thời gian như ghi dữ liệu vào cơ sở dữ liệu hoặc đọc từ nó, tìm nạp dữ liệu từ mạng và các tác vụ khác. Tất cả các tác vụ phổ biến này mất nhiều thời gian hơn để hoàn thành so với luồng chính của khung Android cho phép.

Chuỗi chính là một chuỗi xử lý tất cả các bản cập nhật cho giao diện người dùng. Các nhà phát triển được yêu cầu không chặn nó để tránh ứng dụng bị đóng băng hoặc thậm chí gặp sự cố với hộp thoại Ứng dụng không phản hồi. Kotlin coroutines sẽ giải quyết vấn đề này cho chúng ta bằng cách giới thiệu sự an toàn của luồng chính. Đây là phần còn thiếu cuối cùng mà chúng tôi muốn thêm vào ứng dụng My Crypto Coins.

Coroutines là một tính năng Kotlin chuyển đổi các lệnh gọi lại không đồng bộ cho các tác vụ chạy dài, chẳng hạn như truy cập cơ sở dữ liệu hoặc mạng, thành mã tuần tự. Với coroutines, bạn có thể viết mã không đồng bộ, theo truyền thống được viết bằng cách sử dụng mẫu Gọi lại, sử dụng kiểu đồng bộ. Giá trị trả về của một hàm sẽ cung cấp kết quả của lệnh gọi không đồng bộ. Mã được viết tuần tự thường dễ đọc hơn và thậm chí có thể sử dụng các tính năng ngôn ngữ như ngoại lệ.

Vì vậy, chúng tôi sẽ sử dụng coroutines ở mọi nơi trong ứng dụng này, nơi chúng tôi cần đợi cho đến khi có kết quả từ một tác vụ đang chạy lâu dài và hơn là tiếp tục thực thi. Hãy xem một cách triển khai chính xác cho ViewModel của chúng tôi, nơi chúng tôi sẽ thử lấy lại dữ liệu mới nhất từ ​​máy chủ cho các loại tiền điện tử của chúng tôi được hiển thị trên màn hình chính.

Đầu tiên hãy thêm coroutines vào dự án:

// Coroutines support libraries for Kotlin.

// Dependencies for coroutines.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines"

// Dependency is for the special UI context that can be passed to coroutine builders that use
// the main thread dispatcher to dispatch events on the main thread.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"

Sau đó, chúng tôi sẽ tạo lớp trừu tượng sẽ trở thành lớp cơ sở được sử dụng cho bất kỳ ViewModel nào cần có chức năng chung như coroutines trong trường hợp của chúng tôi:

abstract class BaseViewModel : ViewModel() {

    // In Kotlin, all coroutines run inside a CoroutineScope.
    // A scope controls the lifetime of coroutines through its job.
    private val viewModelJob = Job()
    // Since uiScope has a default dispatcher of Dispatchers.Main, this coroutine will be launched
    // in the main thread.
    val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)


    // onCleared is called when the ViewModel is no longer used and will be destroyed.
    // This typically happens when the user navigates away from the Activity or Fragment that was
    // using the ViewModel.
    override fun onCleared() {
        super.onCleared()
        // When you cancel the job of a scope, it cancels all coroutines started in that scope.
        // It's important to cancel any coroutines that are no longer required to avoid unnecessary
        // work and memory leaks.
        viewModelJob.cancel()
    }
}

Ở đây chúng tôi tạo phạm vi điều tra cụ thể, phạm vi này sẽ kiểm soát thời gian tồn tại của các đăng quang thông qua công việc của nó. Như bạn thấy, phạm vi cho phép bạn chỉ định một trình điều phối mặc định kiểm soát luồng nào chạy một quy trình. Khi ViewModel không còn được sử dụng, chúng tôi hủy viewModelJob và với điều đó, mọi quy trình đăng ký bắt đầu bởi uiScope cũng sẽ bị hủy.

Cuối cùng, triển khai chức năng thử lại:

/**
 * The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way.
 * The ViewModel class allows data to survive configuration changes such as screen rotations.
 */

// ViewModel will require a CryptocurrencyRepository so we add @Inject code into ViewModel constructor.
class MainViewModel @Inject constructor(val context: Context, val cryptocurrencyRepository: CryptocurrencyRepository) : BaseViewModel() {

    ...

    val mediatorLiveDataMyCryptocurrencyResourceList = MediatorLiveData<Resource<List<MyCryptocurrency>>>()
    private var liveDataMyCryptocurrencyResourceList: LiveData<Resource<List<MyCryptocurrency>>>
    private val liveDataMyCryptocurrencyList: LiveData<List<MyCryptocurrency>>

    ...

    // This is additional helper variable to deal correctly with currency spinner and preference.
    // It is kept inside viewmodel not to be lost because of fragment/activity recreation.
    var newSelectedFiatCurrencyCode: String? = null

    // Helper variable to store state of swipe refresh layout.
    var isSwipeRefreshing: Boolean = false


    init {
        ...

        // Set a resource value for a list of cryptocurrencies that user owns.
        liveDataMyCryptocurrencyResourceList = cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList(cryptocurrencyRepository.getCurrentFiatCurrencyCode())


        // Declare additional variable to be able to reload data on demand.
        mediatorLiveDataMyCryptocurrencyResourceList.addSource(liveDataMyCryptocurrencyResourceList) {
            mediatorLiveDataMyCryptocurrencyResourceList.value = it
        }

        ...
    }

   ...

    /**
     * On retry we need to run sequential code. First we need to get owned crypto coins ids from
     * local database, wait for response and only after it use these ids to make a call with
     * retrofit to get updated owned crypto values. This can be done using Kotlin Coroutines.
     */
    fun retry(newFiatCurrencyCode: String? = null) {

        // Here we store new selected currency as additional variable or reset it.
        // Later if call to server is unsuccessful we will reuse it for retry functionality.
        newSelectedFiatCurrencyCode = newFiatCurrencyCode

        // Launch a coroutine in uiScope.
        uiScope.launch {
            // Make a call to the server after some delay for better user experience.
            updateMyCryptocurrencyList(newFiatCurrencyCode, SERVER_CALL_DELAY_MILLISECONDS)
        }
    }

    // Refresh the data from local database.
    fun refreshMyCryptocurrencyResourceList() {
        refreshMyCryptocurrencyResourceList(cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList(cryptocurrencyRepository.getCurrentFiatCurrencyCode()))
    }

    // To implement a manual refresh without modifying your existing LiveData logic.
    private fun refreshMyCryptocurrencyResourceList(liveData: LiveData<Resource<List<MyCryptocurrency>>>) {
        mediatorLiveDataMyCryptocurrencyResourceList.removeSource(liveDataMyCryptocurrencyResourceList)
        liveDataMyCryptocurrencyResourceList = liveData
        mediatorLiveDataMyCryptocurrencyResourceList.addSource(liveDataMyCryptocurrencyResourceList)
        { mediatorLiveDataMyCryptocurrencyResourceList.value = it }
    }

    private suspend fun updateMyCryptocurrencyList(newFiatCurrencyCode: String? = null, callDelay: Long = 0) {

        val fiatCurrencyCode: String = newFiatCurrencyCode
                ?: cryptocurrencyRepository.getCurrentFiatCurrencyCode()

        isSwipeRefreshing = true

        // The function withContext is a suspend function. The withContext immediately shifts
        // execution of the block into different thread inside the block, and back when it
        // completes. IO dispatcher is suitable for execution the network requests in IO thread.
        val myCryptocurrencyIds = withContext(Dispatchers.IO) {
            // Suspend until getMyCryptocurrencyIds() returns a result.
            cryptocurrencyRepository.getMyCryptocurrencyIds()
        }

        // Here we come back to main worker thread. As soon as myCryptocurrencyIds has a result
        // and main looper is available, coroutine resumes on main thread, and
        // [getMyCryptocurrencyLiveDataResourceList] is called.
        // We wait for background operations to complete, without blocking the original thread.
        refreshMyCryptocurrencyResourceList(
                cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList
                (fiatCurrencyCode, true, myCryptocurrencyIds, callDelay))
    }

    ...
}

Ở đây chúng tôi gọi một hàm được đánh dấu bằng từ khóa Kotlin đặc biệt suspend cho coroutines. Điều này có nghĩa là hàm tạm ngừng thực thi cho đến khi kết quả sẵn sàng, sau đó nó tiếp tục lại từ nơi nó đã dừng với kết quả. Trong khi nó bị treo để chờ kết quả, nó sẽ bỏ chặn chuỗi mà nó đang chạy.

Ngoài ra, trong một hàm tạm ngưng, chúng ta có thể gọi một hàm tạm dừng khác. Như bạn thấy, chúng tôi thực hiện điều đó bằng cách gọi hàm tạm ngưng mới được đánh dấu withContext được thực thi trên chuỗi khác.

Ý tưởng của tất cả mã này là chúng ta có thể kết hợp nhiều lệnh gọi để tạo thành mã tuần tự đẹp mắt. Đầu tiên, chúng tôi yêu cầu lấy id của tiền điện tử mà chúng tôi sở hữu từ cơ sở dữ liệu cục bộ và chờ phản hồi. Chỉ sau khi chúng tôi nhận được nó, chúng tôi mới sử dụng id phản hồi để thực hiện một cuộc gọi mới với Retrofit để nhận các giá trị tiền điện tử cập nhật đó. Đó là chức năng thử lại của chúng tôi.

Chúng tôi đã thành công! Suy nghĩ cuối cùng, kho lưu trữ, ứng dụng và bản trình bày

Xin chúc mừng, tôi rất vui nếu bạn đã đạt được đến cùng. Tất cả những điểm quan trọng nhất để tạo ứng dụng này đã được đề cập. Có rất nhiều thứ mới được thực hiện trong phần này và rất nhiều thứ không được đề cập trong bài viết này, nhưng tôi đã nhận xét mã của tôi ở mọi nơi rất tốt nên bạn không nên lạc vào nó. Kiểm tra mã cuối cùng cho phần 5 này tại đây trên GitHub:

Xem nguồn trên GitHub.

Thách thức lớn nhất đối với cá nhân tôi không phải là học công nghệ mới, không phải để phát triển ứng dụng, mà là viết tất cả những bài báo này. Thực sự tôi rất hài lòng với bản thân vì đã hoàn thành thử thách này. Học tập và phát triển rất dễ dàng so với việc dạy người khác, nhưng đó là nơi bạn có thể hiểu chủ đề tốt hơn. Lời khuyên của tôi nếu bạn đang tìm kiếm cách tốt nhất để học những điều mới là bắt đầu tự mình tạo ra một thứ gì đó ngay lập tức. Tôi hứa bạn sẽ học được rất nhiều và nhanh chóng.

Tất cả các bài viết này dựa trên phiên bản 1.0.0 của ứng dụng “Kriptofolio” (trước đây là “My Crypto Coins”) mà bạn có thể tải xuống dưới dạng tệp APK riêng biệt tại đây. Nhưng tôi sẽ rất vui nếu bạn trực tiếp cài đặt và đánh giá phiên bản ứng dụng mới nhất từ ​​cửa hàng:

Tải xuống trên Google Play

Ngoài ra, vui lòng truy cập trang web trình bày đơn giản này mà tôi đã thực hiện cho dự án này:

Kriptofolio.app

Cách xử lý các Dịch vụ web RESTful bằng Retrofit, OkHttp, Gson, Glide và Coroutines

Ačiū! Cảm ơn vì đã đọc! Ban đầu tôi đã xuất bản bài đăng này cho blog cá nhân của mình www.baruckis.com vào ngày 11 tháng 5 năm 2019.