如何使用Retrofit,OkHttp,Gson,Glide和Coroutines處理RESTful Web服務

Kriptofolio應用程序系列-第5部分 (Kriptofolio app series — Part 5)

These days almost every Android app connects to internet to get/send data. You should definitely learn how to handle RESTful Web Services, as their correct implementation is the core knowledge while creating modern apps.

如今,幾乎每個Android應用程序都可以連接到互聯網以獲取/發送數據。 您絕對應該學習如何處理RESTful Web服務,因為正確的實現是創建現代應用程序時的核心知識。

This part is going to be complicated. We are going to combine multiple libraries at once to get a working result. I am not going to talk about the native Android way to handle internet requests, because in the real world nobody uses it. Every good app does not try to reinvent the wheel but instead uses the most popular third party libraries to solve common problems. It would be too complicated to recreate the functionality that these well-made libraries have to offer.

這部分將變得復雜。 我們將一次合并多個庫以獲得工作結果。 我不會討論處理互聯網請求的原生Android方法,因為在現實世界中沒有人使用它。 每個優秀的應用程序都不會嘗試重新發明輪子,而是使用最受歡迎的第三方庫來解決常見問題。 重新創建這些精巧庫必須提供的功能將太復雜。

系列內容 (Series content)

  • Introduction: A roadmap to build a modern Android app in 2018–2019

    簡介:2018-2019年構建現代Android應用程序的路線圖

  • Part 1: An introduction to the SOLID principles

    第1部分:SOLID原理簡介

  • Part 2: How to start building your Android app: creating Mockups, UI, and XML layouts

    第2部分:如何開始構建Android應用:創建樣機,UI和XML布局

  • Part 3: All about that Architecture: exploring different architecture patterns and how to use them in your app

    第3部分:有關該架構的全部內容:探索不同的架構模式以及如何在您的應用程序中使用它們

  • Part 4: How to implement Dependency Injection in your app with Dagger 2

    第4部分:如何使用Dagger 2在您的應用程序中實現依賴注入

  • Part 5: Handle RESTful Web Services using Retrofit, OkHttp, Gson, Glide and Coroutines (you’re here)

    第5部分:使用Retrofit,OkHttp,Gson,Glide和Coroutines處理RESTful Web服務(在這里)

什么是Retrofit,OkHttp和Gson? (What is Retrofit, OkHttp and Gson?)

Retrofit is a REST Client for Java and Android. This library, in my opinion, is the most important one to learn, as it will do the main job. It makes it relatively easy to retrieve and upload JSON (or other structured data) via a REST based webservice.

Retrofit是用于Java和Android的REST客戶端。 我認為,該庫是最重要的學習庫,因為它將完成主要工作。 它使通過基于REST的Web服務檢索和上傳JSON(或其他結構化數據)相對容易。

In Retrofit you configure which converter is used for the data serialization. Typically to serialize and deserialize objects to and from JSON you use an open-source Java library — Gson. Also if you need, you can add custom converters to Retrofit to process XML or other protocols.

在翻新中,可以配置哪個轉換器用于數據序列化。 通常,要使用JSON對對象進行序列化和反序列化,請使用開源Java庫-Gson。 另外,如果需要,您可以將自定義轉換器添加到Retrofit以處理XML或其他協議。

For making HTTP requests Retrofit uses the OkHttp library. OkHttp is a pure HTTP/SPDY client responsible for any low-level network operations, caching, requests and responses manipulation. In contrast, Retrofit is a high-level REST abstraction build on top of OkHttp. Retrofit is strongly coupled with OkHttp and makes intensive use of it.

為了發出HTTP請求,Retrofit使用OkHttp庫。 OkHttp是一個純HTTP / SPDY客戶端,負責任何低級網絡操作,緩存,請求和響應操作。 相反,Retrofit是在OkHttp之上的高級REST抽象構建。 改型與OkHttp緊密結合,并大量使用它。

Now that you know that everything is closely related, we are going to use all these 3 libraries at once. Our first goal is to get all the cryptocurrencies list using Retrofit from the Internet. We will use a special OkHttp interceptor class for CoinMarketCap API authentication when making a call to the server. We will get back a JSON data result and then convert it using the Gson library.

既然您知道所有內容都息息相關,那么我們將立即使用這三個庫。 我們的首要目標是使用Internet上的Retrofit獲取所有加密貨幣列表。 調用服務器時,我們將使用特殊的OkHttp攔截器類進行CoinMarketCap API身份驗證。 我們將獲取JSON數據結果,然后使用Gson庫對其進行轉換。

快速安裝Retrofit 2,請先嘗試 (Quick setup for Retrofit 2 just to try it first)

When learning something new, I like to try it out in practice as soon as I can. We will apply a similar approach with Retrofit 2 for you to understand it better more quickly. Don’t worry right now about code quality or any programming principles or optimizations — we’ll just write some code to make Retrofit 2 work in our project and discuss what it does.

學習新知識時,我希望盡快在實踐中進行嘗試。 我們將對Retrofit 2應用類似的方法,以使您更快地更好地理解它。 現在不必擔心代碼質量或任何編程原則或優化-我們將只編寫一些代碼以使Retrofit 2在我們的項目中工作并討論其功能。

Follow these steps to set up Retrofit 2 on My Crypto Coins app project:

請按照以下步驟在My Crypto Coins應用程序項目上設置Retrofit 2:

首先,授予該應用程序的INTERNET權限 (First, give INTERNET permission for the app)

We are going to execute HTTP requests on a server accessible via the Internet. Give this permission by adding these lines to your Manifest file:

我們將在可通過Internet訪問的服務器上執行HTTP請求。 通過將以下行添加到清單文件中來授予此權限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.baruckis.mycryptocoins"><uses-permission android:name="android.permission.INTERNET" />...
</manifest>

然后,您應該添加庫依賴項 (Then you should add library dependencies)

Find the latest Retrofit version. Also you should know that Retrofit doesn’t ship with an integrated JSON converter. Since we will get responses in JSON format, we need to include the converter manually in the dependencies too. We are going to use latest Google’s JSON converter Gson version. Let’s add these lines to your gradle file:

查找最新的Retrofit版本 。 另外,您應該知道Retrofit并未附帶集成的JSON轉換器。 由于我們將獲得JSON格式的響應,因此我們也需要手動將轉換器包含在依賴項中。 我們將使用最新的Google JSON轉換器Gson版本 。 讓我們將這些行添加到gradle文件中:

// 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"

As you noticed from my comment, the OkHttp dependency is already shipped with the Retrofit 2 dependency. Versions is just a separate gradle file for convenience:

正如您從我的評論中注意到的那樣,Retrofit 2依賴項已經附帶了OkHttp依賴項。 為了方便起見,版本只是一個單獨的gradle文件:

def versions = [:]versions.retrofit = "2.4.0"ext.versions = versions

下一步設置改造界面 (Next set up the Retrofit interface)

It’s an interface that declares our requests and their types. Here we define the API on the client side.

這是一個聲明我們的請求及其類型的接口。 在這里,我們在客戶端定義API。

/*** 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.
}

并設置數據類 (And set up the data class)

Data classes are POJOs (Plain Old Java Objects) that represent the responses of the API calls we’re going to make.

數據類是POJO(普通的舊Java對象),代表我們將要進行的API調用的響應。

/*** 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)
}

調用服務器時,創建用于身份驗證的特殊攔截器類 (Create a special interceptor class for authentication when making a call to the server)

This is the case particular for any API that requires authentication to get a successful response. Interceptors are a powerful way to customize your requests. We are going to intercept the actual request and to add individual request headers, which will validate the call with an API Key provided by CoinMarketCap Professional API Developer Portal. To get yours, you need to register there.

對于任何需要身份驗證才能獲得成功響應的API而言,情況尤其如此。 攔截器是自定義您的請求的強大方法。 我們將截取實際的請求并添加單個請求標頭,這將使用CoinMarketCap專業API開發人員門戶提供的API密鑰來驗證調用。 要獲得您的證書,您需要在此注冊。

/*** 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)}
}

最后,將此代碼添加到我們的活動中以查看翻新工作 (Finally, add this code to our activity to see Retrofit working)

I wanted to get your hands dirty as soon as possible, so I put everything in one place. This is not the correct way, but it’s the fastest instead just to see a visual result quickly.

我想盡快弄臟你的手,所以我將所有東西都放在一個地方。 這不是正確的方法,而是最快的方法,只是快速查看視覺結果。

class AddSearchActivity : AppCompatActivity(), Injectable {private lateinit var listView: ListViewprivate 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.VISIBLElistAdapter.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()}})}...
}

You can explore the code here. Remember this is only an initial simplified implementation version for you to get the idea better.

您可以在此處探索代碼。 請記住,這只是一個初始的簡化實現版本,可以幫助您更好地理解。

使用OkHttp 3和Gson的Retrofit 2的最終正確設置 (Final correct setup for Retrofit 2 with OkHttp 3 and Gson)

Ok after a quick experiment, it is time to bring this Retrofit implementation to the next level. We already got the data successfully but not correctly. We are missing the states like loading, error and success. Our code is mixed without separation of concerns. It’s a common mistake to write all your code in an activity or a fragment. Our activity class is UI based and should only contain logic that handles UI and operating system interactions.

經過快速的實驗之后,現在可以將Retrofit實施提升到一個新的水平。 我們已經成功獲取了數據,但不正確。 我們缺少諸如加載,錯誤和成功之類的狀態。 我們的代碼混合在一起,沒有關注點的分離。 在活動或片段中編寫所有代碼是一個常見的錯誤。 我們的活動類基于UI,并且應僅包含處理UI和操作系統交互的邏輯。

Actually, after this quick setup, I worked a lot and made many changes. There is no point to put all the code that was changed in the article. Better instead you should browse the final Part 5 code repo here. I have commented everything very well and my code should be clear for you to understand. But I am going to talk about most important things I have done and why I did them.

實際上,在完成此快速設置之后,我做了很多工作并進行了許多更改。 沒有必要在文章中放置所有已更改的代碼。 更好的是,您應該在此處瀏覽最終的第5部分代碼存儲庫。 我對所有內容的評論都非常好,我的代碼應該清晰易懂。 但是我將談論我所做的最重要的事情以及為什么要做這些。

The first step to improve was to start using Dependency Injection. Remember from the previous part we already have Dagger 2 implemented inside the project correctly. So I used it for the Retrofit setup.

改進的第一步是開始使用依賴注入。 請記住,在上一部分中,我們已經在項目內部正確實現了Dagger 2。 因此,我將其用于翻新設置。

/*** 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@Singletonfun 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@Singletonfun 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.}...
}

Now as you see, Retrofit is separated from the activity class as it should be. It will be initialized only once and used app-wide.

現在您可以看到,Retrofit與活動類已經分開了。 它將僅初始化一次,并在整個應用范圍內使用。

As you may have noticed while creating the Retrofit builder instance, we added a special Retrofit calls adapter using addCallAdapterFactory. By default, Retrofit returns a Call<T>, but for our project we require it to return a LiveData<T> type. In order to do that we need to add LiveDataCallAdapter by using LiveDataCallAdapterFactory.

正如您在創建Retrofit構建器實例時可能已經注意到的那樣,我們使用addCallAdapterFactory添加了一個特殊的Retrofit調用適配器。 默認情況下,Retrofit返回Call<T> ,但是對于我們的項目,我們要求它返回LiveData<T>類型。 為了做到這一點,我們需要添加LiveDataCallAdapter使用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() = responseTypeoverride 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)}
}

Now we will get LiveData<T> instead of Call<T> as the return type from Retrofit service methods defined in the ApiService interface.

現在,我們將從ApiService接口中定義的Retrofit服務方法中獲得LiveData<T>而不是Call<T>作為返回類型。

Another important step to make is to start using the Repository pattern. I have talked about it in Part 3. Check out our MVVM architecture schema from that post to remember where it goes.

要做的另一個重要步驟是開始使用存儲庫模式。 我已經在第3部分中討論過它。 從那篇文章中查看我們的MVVM體系結構架構,以記住它的去向。

As you see in the picture, Repository is a separate layer for the data. It’s our single source of contact for getting or sending data. When we use Repository, we are following the separation of concerns principle. We can have different data sources (like in our case persistent data from an SQLite database and data from web services), but Repository is always going to be single source of truth for all app data.

如您在圖片中看到的,存儲庫是數據的單獨層。 這是我們獲取或發送數據的唯一聯系方式。 當使用存儲庫時,我們遵循關注點分離原則。 我們可以有不同的數據源(例如我們SQLite數據庫中的持久性數據和Web服務中的數據),但是存儲庫始終將是所有應用程序數據的唯一真實來源。

Instead of communicating with our Retrofit implementation directly, we are going to use Repository for that. For each kind of entity, we are going to have a separate Repository.

與其直接與我們的Retrofit實現進行通信,不如使用存儲庫。 對于每種實體,我們將有一個單獨的存儲庫。

/*** 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>}}

As you notice in the CryptocurrencyRepository class code, I am using the NetworkBoundResource abstract class. What is it and why do we need it?

正如您在CryptocurrencyRepository類代碼中注意到的那樣,我正在使用NetworkBoundResource抽象類。 這是什么,為什么我們需要它?

NetworkBoundResource is a small but very important helper class that will allow us to maintain a synchronization between the local database and the web service. Our goal is to build a modern application that will work smoothly even when our device is offline. Also with the help of this class we will be able to present different network states like errors or loading for the user visually.

NetworkBoundResource是一個很小但非常重要的幫助程序類,它將使我們能夠維護本地數據庫和Web服務之間的同步。 我們的目標是構建一個即使在設備離線時也能平穩運行的現代應用程序。 同樣,在此類的幫助下,我們將能夠直觀地呈現不同的網絡狀態,例如錯誤或負載。

NetworkBoundResource starts by observing the database for the resource. When the entry is loaded from the database for the first time, it checks whether the result is good enough to be dispatched or if it should be re-fetched from the network. Note that both of these situations can happen at the same time, given that you probably want to show cached data while updating it from the network.

NetworkBoundResource從觀察數據庫中的資源開始。 首次從數據庫中加載條目時,它將檢查結果是否足夠好以進行分派,或者是否應從網絡中重新獲取。 請注意,這兩種情況可能同時發生,因為您可能想在從網絡更新數據時顯示緩存的數據。

If the network call completes successfully, it saves the response into the database and re-initializes the stream. If the network request fails, the NetworkBoundResource dispatches a failure directly.

如果網絡調用成功完成,它將響應保存到數據庫中并重新初始化流。 如果網絡請求失敗,則NetworkBoundResource直接調度失敗。

/*** 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))}}}}@MainThreadprivate 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 hadresult.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>>@WorkerThreadprotected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body// Called to save the result of the API response into the database.@WorkerThreadprotected abstract fun saveCallResult(item: RequestType)// Called with the data in the database to decide whether to fetch// potentially updated data from the network.@MainThreadprotected 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.@MainThreadprotected abstract fun loadFromDb(): LiveData<ResultType>// Called to create the API call.@MainThreadprotected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

Under the hood, the NetworkBoundResource class is made by using MediatorLiveData and its ability to observe multiple LiveData sources at once. Here we have two LiveData sources: the database and the network call response. Both of those LiveData are wrapped into one MediatorLiveData which is exposed by NetworkBoundResource.

在幕后, NetworkBoundResource類是通過使用MediatorLiveData及其一次觀察多個LiveData源的功能而制成的。 在這里,我們有兩個LiveData源:數據庫和網絡呼叫響應。 這兩個LiveData都包裝到一個MediatorLiveData中,該MediatorLiveData由NetworkBoundResource公開。

Let’s take a closer look how the NetworkBoundResource will work in our app. Imagine the user will launch the app and click on a floating action button on the bottom right corner. The app will launch the add crypto coins screen. Now we can analyze NetworkBoundResource's usage inside it.

讓我們仔細看看NetworkBoundResource將如何在我們的應用程序中工作。 假設用戶將啟動該應用程序,然后單擊右下角的浮動操作按鈕。 該應用程序將啟動添加加密硬幣屏幕。 現在我們可以分析NetworkBoundResource在其中的用法。

If the app is freshly installed and it is its first launch, then there will not be any data stored inside the local database. Because there is no data to show, a loading progress bar UI will be shown. Meanwhile the app is going to make a request call to the server via a web service to get all the cryptocurrencies list.

如果該應用是全新安裝的并且是首次啟動,則本地數據庫內部將不會存儲任何數據。 因為沒有要顯示的數據,所以將顯示加載進度欄UI。 同時,該應用將通過網絡服務向服務器發出請求調用,以獲取所有加密貨幣列表。

If the response is unsuccessful then the error message UI will be shown with the ability to retry a call by pressing a button. When a request call is successful at last, then the response data will be saved to a local SQLite database.

如果響應不成功,則會顯示錯誤消息UI,并具有通過按按鈕重試呼叫的功能。 最后一次請求調用成功后,響應數據將保存到本地SQLite數據庫中。

If we come back to the same screen the next time, the app will load data from the database instead of making a call to the internet again. But the user can ask for a new data update by implementing pull-to-refresh functionality. Old data information will be shown whilst the network call is happening. All this is done with the help of NetworkBoundResource.

如果我們下次再次返回同一屏幕,則該應用程序將從數據庫中加載數據,而不是再次撥打互聯網。 但是用戶可以通過實現“按需刷新”功能來請求新的數據更新。 在進行網絡呼叫時,將顯示舊的數據信息。 所有這些都是在NetworkBoundResource的幫助下完成的。

Another class used in our Repository and LiveDataCallAdapter where all the "magic" happens is ApiResponse. Actually ApiResponse is just a simple common wrapper around the Retrofit2.Response class that converts each response to an instance of LiveData.

在我們的信息庫和使用另一類LiveDataCallAdapter ,所有的“神奇”的情況是ApiResponse 。 實際上, ApiResponse只是Retrofit2.Response類的簡單通用包裝,它將每個響應轉換為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>>() {}.typeval errorResponse: CoinMarketCap<CoinMarketCapType> = gson.fromJson(response.errorBody()!!.charStream(), type)val msg = errorResponse.status?.errorMessage ?: errorResponse.messageval 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>()

Inside this wrapper class, if our response has an error, we use the Gson library to convert the error to a JSON object. However, if the response was successful, then the Gson converter for JSON to POJO object mapping is used. We already added it when creating the retrofit builder instance with GsonConverterFactory inside the Dagger AppModule function provideApiService.

在此包裝器類內部,如果響應中有錯誤,則使用Gson庫將錯誤轉換為JSON對象。 但是,如果響應成功,則使用Gson轉換器將JSON轉換為POJO對象。 在Dagger AppModule函數provideApiService使用GsonConverterFactory創建改造生成器實例時,我們已經添加了它。

滑動以加載圖像 (Glide for image loading)

What is Glide? From the docs:

什么是Glide ? 從文檔:

Glide is a fast and efficient open source media management and image loading framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy to use interface.
Glide是適用于Android的快速高效的開源媒體管理和圖像加載框架,它將媒體解碼,內存和磁盤緩存以及資源池包裝到一個簡單易用的界面中。
Glide’s primary focus is on making scrolling any kind of a list of images as smooth and fast as possible, but it is also effective for almost any case where you need to fetch, resize, and display a remote image.
Glide的主要重點是使盡可能平滑和快速地滾動任何種類的圖像列表,但是對于幾乎所有需要獲取,調整大小和顯示遠程圖像的情況,它也都有效。

Sounds like a complicated library which offers many useful features that you would not want to develop all by yourself. In My Crypto Coins app, we have several list screens where we need to show multiple cryptocurrency logos — pictures taken from the internet all at once — and still ensure a smooth scrolling experience for the user. So this library fits our needs perfectly. Also this library is very popular among Android developers.

聽起來像一個復雜的庫,其中提供了許多您不想自己開發的有用功能。 在“我的加密貨幣”應用程序中,我們有幾個列表屏幕,在這些屏幕中,我們需要顯示多個加密貨幣徽標(一次從互聯網上拍攝的圖片),并且仍然可以確保用戶流暢的滾動體驗。 因此,該庫完全符合我們的需求。 同樣,該庫在Android開發人員中非常受歡迎。

Steps to setup Glide on My Crypto Coins app project:

在“我的加密貨幣”應用程序項目上設置Glide的步驟:

聲明依賴 (Declare dependencies)

Get the latest Glide version. Again versions is a separate file versions.gradle for the project.

獲取最新的Glide版本 。 同樣,版本是項目的單獨文件versions.gradle

// 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"

Because we want to use the networking library OkHttp in our project for all network operations, we need to include the specific Glide integration for it instead of the default one. Also since Glide is going to perform a network request to load images via the internet, we need to include the permission INTERNET in our AndroidManifest.xml file — but we already did that with the Retrofit setup.

因為我們要在項目中使用網絡庫OkHttp進行所有網絡操作,所以我們需要為其包含特定的Glide集成,而不是默認的集成。 同樣,由于Glide將執行網絡請求以通過Internet加載圖像,因此我們需要在我們的AndroidManifest.xml文件中包括INTERNET權限-但是我們已經在Retrofit設置中做到了這一點。

創建AppGlideModule (Create AppGlideModule)

Glide v4, which we will be using, offers a generated API for Applications. It will use an annotation processor to generate an API that allows applications to extend Glide’s API and include components provided by integration libraries. For any app to access the generated Glide API we need to include an appropriately annotated AppGlideModule implementation. There can be only a single implementation of the generated API and only one AppGlideModule per application.

我們將使用的Glide v4為應用程序提供了生成的API。 它將使用注釋處理器生成一個API,該API允許應用程序擴展Glide的API并包括集成庫提供的組件。 對于任何要訪問生成的Glide API的應用程序,我們都需要包含一個帶注釋的AppGlideModule實現。 生成的API只能有一個實現,每個應用程序只能有一個AppGlideModule

Let’s create a class extending AppGlideModule somewhere in your app project:

讓我們在您的應用程序項目中的某個地方創建一個擴展AppGlideModule的類:

/*** 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()

Even if our application is not changing any additional settings or implementing any methods in AppGlideModule, we still need to have its implementation to use Glide. You're not required to implement any of the methods in AppGlideModule for the API to be generated. You can leave the class blank as long as it extends AppGlideModule and is annotated with @GlideModule.

即使我們的應用程序沒有更改任何其他設置或在AppGlideModule實現任何方法,我們仍然需要使其實現才能使用Glide。 您無需為要生成的API實施AppGlideModule中的任何方法。 您可以將類保留為空白,只要它擴展了AppGlideModule并使用@GlideModule注釋@GlideModule

使用Glide生成的API (Use Glide-generated API)

When using AppGlideModule, applications can use the API by starting all loads with GlideApp.with(). This is the code that shows how I have used Glide to load and show cryptocurrency logos in the add crypto coins screen all cryptocurrencies list.

使用AppGlideModule ,應用程序可以通過從GlideApp.with()開始所有加載來使用API??。 這是顯示我如何使用Glide在添加加密硬幣屏幕的所有加密貨幣列表中加載和顯示加密貨幣徽標的代碼。

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

As you see, you can start using Glide with just few lines of code and let it do all the hard work for you. It is pretty straightforward.

如您所見,您只需幾行代碼就可以開始使用Glide,并讓它為您完成所有艱苦的工作。 這很簡單。

Kotlin協程 (Kotlin Coroutines)

While building this app, we are going to face situations when we will run time consuming tasks such as writing data to a database or reading from it, fetching data from the network and other. All these common tasks take longer to complete than allowed by the Android framework’s main thread.

在構建此應用程序時,我們將面臨一些情況,例如,運行耗時的任務,例如將數據寫入數據庫或從數據庫中讀取數據,從網絡中獲取數據等。 完成所有這些常見任務所需的時間要比Android框架主線程所允許的時間長。

The main thread is a single thread that handles all updates to the UI. Developers are required not to block it to avoid the app freezing or even crashing with an Application Not Responding dialog. Kotlin coroutines is going to solve this problem for us by introducing main thread safety. It is the last missing piece that we want to add for My Crypto Coins app.

主線程是處理UI的所有更新的單個線程。 要求開發人員不要阻止它,以避免應用程序凍結甚至因“應用程序無響應”對話框而崩潰。 Kotlin協程將通過引入主線程安全性為我們解決此問題。 這是我們要為“我的加密貨幣”應用添加的最后丟失的部分。

Coroutines are a Kotlin feature that convert async callbacks for long-running tasks, such as database or network access, into sequential code. With coroutines, you can write asynchronous code, which was traditionally written using the Callback pattern, using a synchronous style. The return value of a function will provide the result of the asynchronous call. Code written sequentially is typically easier to read, and can even use language features such as exceptions.

協程是Kotlin的一項功能,可將長時間運行的任務(如數據庫或網絡訪問)的異步回調轉換為順序代碼。 使用協程,您可以使用異步樣式編寫異步代碼,該代碼通常是使用Callback模式編寫的。 函數的返回值將提供異步調用的結果。 順序編寫的代碼通常更易于閱讀,甚至可以使用諸如異常之類的語言功能。

So we are going to use coroutines everywhere in this app where we need to wait until a result is available from a long-running task and than continue execution. Let’s see one exact implementation for our ViewModel where we will retry getting the latest data from the server for our cryptocurrencies presented on the main screen.

因此,我們將在此應用中的所有地方使用協程,我們需要等到長時間運行的任務獲得結果并繼續執行。 讓我們看一下ViewModel的一個確切實現,在該模型中,我們將嘗試從服務器獲取主屏幕上顯示的加密貨幣的最新數據。

First add coroutines to the project:

首先將協程添加到項目中:

// 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"

Then we will create abstract class which will become the base class to be used for any ViewModel that needs to have common functionality like coroutines in our case:

然后,我們將創建抽象類,該抽象類將成為所有需要具有通用功能(例如協程)的ViewModel使用的基類:

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()}
}

Here we create specific coroutine scope, which will control the lifetime of coroutines through its job. As you see, scope allows you to specify a default dispatcher that controls which thread runs a coroutine. When the ViewModel is no longer used, we cancel viewModelJob and with that every coroutine started by uiScope will be cancelled as well.

在這里,我們創建了特定的協程范圍,它將通過其工作來控制協程的壽命。 如您所見,作用域使您可以指定一個默認調度程序,該調度程序控制哪個線程運行協程。 當不再使用ViewModel時,我們將取消viewModelJob并且uiScope啟動的每個協程uiScope將被取消。

Finally, implement the retry functionality:

最后,實現重試功能:

/*** 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 = falseinit {...// 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 = liveDatamediatorLiveDataMyCryptocurrencyResourceList.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))}...
}

Here we call a function marked with a special Kotlin keyword suspend for coroutines. This means that the function suspends execution until the result is ready, then it resumes where it left off with the result. While it is suspended waiting for a result, it unblocks the thread that it is running on.

在這里,我們調用標有特殊Kotlin關鍵字的函數來suspend協程。 這意味著該函數將暫停執行直到結果準備就緒,然后再從結果中停止執行。 在掛起等待結果時,它會解除阻塞正在運行的線程。

Also, in one suspend function we can call another suspend function. As you see we do that by calling new suspend function marked withContext that is executed on different thread.

同樣,在一個暫停函數中,我們可以調用另一個暫停函數。 如您所見,我們通過調用標記為withContext新的掛起函數來執行withContext ,該函數在不同的線程上執行。

The idea of all this code is that we can combine multiple calls to form nice-looking sequential code. First we request to get the ids of the cryptocurrencies we own from the local database and wait for the response. Only after we get it do we use the response ids to make a new call with Retrofit to get those updated cryptocurrency values. That is our retry functionality.

所有這些代碼的想法是,我們可以組合多個調用以形成美觀的順序代碼。 首先,我們要求從本地數據庫獲取我們擁有的加密貨幣的ID,然后等待響應。 只有在得到它之后,我們才使用響應ID使用Retrofit進行新的調用以獲取那些更新的加密貨幣值。 那就是我們的重試功能。

我們做到了! 最終想法,存儲庫,應用程序和演示文稿 (We made it! Final thoughts, repository, app & presentation)

Congratulations, I am happy if you managed to reach to the end. All the most significant points for creating this app have been covered. There was plenty of new stuff done in this part and a lot of that is not covered by this article, but I commented my code everywhere very well so you should not get lost in it. Check out final code for this part 5 here on GitHub:

恭喜,如果您設法做到最后,我很高興。 涵蓋了創建此應用程序的所有最重要的要點。 在這一部分中有很多新的東西要做,但是本文沒有涉及很多,但是我在所有地方都很好地評論了我的代碼,因此您不要迷路。 在GitHub上查看第5部分的最終代碼:

View Source On GitHub.

在GitHub上查看源代碼 。

The biggest challenge for me personally was not to learn new technologies, not to develop the app, but to write all these articles. Actually I am very happy with myself that I completed this challenge. Learning and developing is easy compared to teaching others, but that is where you can understand the topic even better. My advice if you are looking for the best way to learn new things is to start creating something yourself immediately. I promise you will learn a lot and quickly.

我個人面臨的最大挑戰不是學習新技術,開發應用程序,而是寫所有這些文章。 實際上,我對完成這項挑戰感到非常滿意。 與教別人相比,學習和發展很容易,但是在這里您可以更好地理解該主題。 如果您正在尋找學習新事物的最佳方法,我的建議是立即開始自己創建一些東西。 我保證您會學到很多東西并且很快。

All these articles are based on version 1.0.0 of “Kriptofolio” (previously “My Crypto Coins”) app which you can download as a separate APK file here. But I will be very happy if you install and rate the latest app version from the store directly:

所有這些文章均基于“ Kriptofolio”(以前稱為“我的加密貨幣”)應用1.0.0版,您可以在此處將其下載為單獨的APK文件。 但是,如果您直接從商店中安裝最新應用程序版本并對其進行評分,我將非常高興:

在Google Play上獲取 (Get It On Google Play)

Also please feel free to visit this simple presentation website that I made for this project:

另外,請隨時訪問我為此項目制作的這個簡單的演示網站:

Kriptofolio.app (Kriptofolio.app)



A?iū! Thanks for reading! I originally published this post for my personal blog www.baruckis.com on May 11, 2019.

阿奇! 謝謝閱讀! 我最初于2019年5月11日在我的個人博客www.baruckis.com上發布了這篇文章。

翻譯自: https://www.freecodecamp.org/news/kriptofolio-app-series-part-5/

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/391524.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/391524.shtml
英文地址,請注明出處:http://en.pswp.cn/news/391524.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode 90. 子集 II(回溯算法)

給你一個整數數組 nums &#xff0c;其中可能包含重復元素&#xff0c;請你返回該數組所有可能的子集&#xff08;冪集&#xff09;。 解集 不能 包含重復的子集。返回的解集中&#xff0c;子集可以按 任意順序 排列。 示例 1&#xff1a; 輸入&#xff1a;nums [1,2,2] 輸…

robot:linux下安裝robot環境

https://www.cnblogs.com/lgqboke/p/8252488.html 轉載于:https://www.cnblogs.com/gcgc/p/11425588.html

感知器 機器學習_機器學習感知器實現

感知器 機器學習In this post, we are going to have a look at a program written in Python3 using numpy. We will discuss the basics of what a perceptron is, what is the delta rule and how to use it to converge the learning of the perceptron.在本文中&#xff0…

JS解析格式化Json插件,Json和XML互相轉換插件

Json對象轉換為XML字符串插件 http://www.jsons.cn/Down/jquery.json2xml.js var xml_content $.json2xml(json_object);XML字符串轉換為Json對象插件 http://www.jsons.cn/Down/jquery.xml2json.js var json_obj $.xml2json(xml_content);json序列化和反序列化方法插件 …

Python之集合、解析式,生成器,函數

一 集合 1 集合定義&#xff1a; 1 如果花括號為空&#xff0c;則是字典類型2 定義一個空集合&#xff0c;使用set 加小括號使用B方式定義集合時&#xff0c;集合內部的數必須是可迭代對象&#xff0c;數值類型的不可以 其中的值必須是可迭代對象&#xff0c;其中的元素必須是可…

深度神經網絡課程總結_了解深度神經網絡如何工作(完整課程)

深度神經網絡課程總結Even if you are completely new to neural networks, this course from Brandon Rohrer will get you comfortable with the concepts and math behind them.即使您是神經網絡的新手&#xff0c;Brandon Rohrer的本課程也會使您熟悉其背后的概念和數學。 …

leetcode 1006. 笨階乘

通常&#xff0c;正整數 n 的階乘是所有小于或等于 n 的正整數的乘積。例如&#xff0c;factorial(10) 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1。 相反&#xff0c;我們設計了一個笨階乘 clumsy&#xff1a;在整數的遞減序列中&#xff0c;我們以一個固定順序的操作符序列來…

python:如何傳遞一個列表參數

轉載于:https://www.cnblogs.com/gcgc/p/11426356.html

curl的安裝與簡單使用

2019獨角獸企業重金招聘Python工程師標準>>> windows 篇&#xff1a; 安裝篇&#xff1a; 我的電腦版本是windows7,64位&#xff0c;對應的curl下載地址如下&#xff1a; https://curl.haxx.se/download.html 直接找到下面的這個版本&#xff1a; curl-7.57.0.tar.g…

gcc 編譯過程

gcc 編譯過程從 hello.c 到 hello(或 a.out)文件&#xff0c; 必須歷經 hello.i、 hello.s、 hello.o&#xff0c;最后才得到 hello(或a.out)文件&#xff0c;分別對應著預處理、編譯、匯編和鏈接 4 個步驟&#xff0c;整個過程如圖 10.5 所示。 這 4 步大致的工作內容如下&am…

虎牙直播電影一天收入_電影收入

虎牙直播電影一天收入“美國電影協會(MPAA)的首席執行官J. Valenti提到&#xff1a;“沒有人能告訴您電影在市場上的表現。 直到電影在黑暗的劇院里放映并且銀幕和觀眾之間都散發出火花。 (“The CEO of Motion Picture Association of America (MPAA) J. Valenti mentioned th…

郵箱如何秘密發送多個人郵件_如何發送秘密消息

郵箱如何秘密發送多個人郵件Cryptography is the science of using codes and ciphers to protect messages, at its most basic level. Encryption is encoding messages with the intent of only allowing the intended recipient to understand the meaning of the message.…

leetcode 面試題 17.21. 直方圖的水量(單調棧)

給定一個直方圖(也稱柱狀圖)&#xff0c;假設有人從上面源源不斷地倒水&#xff0c;最后直方圖能存多少水量?直方圖的寬度為 1。 上面是由數組 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直方圖&#xff0c;在這種情況下&#xff0c;可以接 6 個單位的水&#xff08;藍色部分表示水&a…

python:動態參數*args

動態參數 顧名思義&#xff0c;動態參數就是傳入的參數的個數是動態的&#xff0c;可以是1個、2個到任意個&#xff0c;還可以是0個。在不需要的時候&#xff0c;你完全可以忽略動態函數&#xff0c;不用給它傳遞任何值。 Python的動態參數有兩種&#xff0c;分別是*args和**kw…

3.5. Ticket

過程 3.4. Ticket 使用方法 New Ticket 新建Ticket, Ticket 可以理解為任務。 將Ticket 分配給團隊成員 受到Ticket后&#xff0c;一定要更改Ticket 為 accept &#xff0c; 這時在View Tickets 中將會看到該Ticket已經分配&#xff0c; 編碼過程 這里有一個特別的規定&…

Python操作Mysql實例代碼教程在線版(查詢手冊)_python

實例1、取得MYSQL的版本在windows環境下安裝mysql模塊用于python開發MySQL-python Windows下EXE安裝文件下載 復制代碼 代碼如下:# -*- coding: UTF-8 -*- #安裝MYSQL DB for pythonimport MySQLdb as mdb con None try: #連接mysql的方法&#xff1a;connect(ip,user,pass…

批判性思維_為什么批判性思維技能對數據科學家至關重要

批判性思維As Alexander Pope said, to err is human. By that metric, who is more human than us data scientists? We devise wrong hypotheses constantly and then spend time working on them just to find out how wrong we were.正如亞歷山大波普(Alexander Pope)所說…

leetcode 1143. 最長公共子序列(dp)

給定兩個字符串 text1 和 text2&#xff0c;返回這兩個字符串的最長 公共子序列 的長度。如果不存在 公共子序列 &#xff0c;返回 0 。 一個字符串的 子序列 是指這樣一個新的字符串&#xff1a;它是由原字符串在不改變字符的相對順序的情況下刪除某些字符&#xff08;也可以…

【Spark】SparkStreaming-Kafka-Redis-集成-基礎參考資料

SparkStreaming-Kafka-Redis-集成-基礎參考資料 Overview - Spark 2.2.0 DocumentationSpark Streaming Kafka Integration Guide - Spark 2.2.0 DocumentationSpark Streaming Kafka Integration Guide (Kafka broker version 0.8.2.1 or higher) - Spark 2.2.0 Documentat…

Manjaro 17 搭建 redis 4.0.1 集群服務

安裝Redis在Linux環境中 這里我們用的是manjaro一個小眾一些的發行版 我選用的是manjaro 17 KDE 如果你已經安裝好了manjaro 那么你需要準備一個redis.tar.gz包 這里我選用的是截至目前最新的redis 4.0.1版本 我們可以在官網進行下載 https://redis.io/download選擇Stable &…