如何使用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)

什么是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: 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()

            }

        })

    }

   ...
}

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

    ...
}

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

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

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

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

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

    ...
}

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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值