如何使用Retrofit、OkHttp、Gson、Glide和Coroutines处理RESTful网络服务

170 阅读16分钟

Kriptofolio应用程序系列 - 第五部分

如今,几乎每个安卓应用都会连接到互联网以获取/发送数据。你肯定应该学习如何处理RESTful网络服务,因为它们的正确实现是创建现代应用程序的核心知识。

这一部分将会很复杂。我们将同时结合多个库来获得一个工作结果。我不打算谈论本地Android处理互联网请求的方式,因为在现实世界中没有人使用它。每个好的应用程序都不会试图重新发明轮子,而是使用最流行的第三方库来解决常见的问题。如果要重新创造这些精心制作的库所提供的功能,那就太复杂了。

系列内容

什么是Retrofit、OkHttp和Gson?

Retrofit是一个用于Java和Android的REST客户端。在我看来,这个库是最需要学习的,因为它将做主要的工作。它使得通过基于REST的网络服务检索和上传JSON(或其他结构化数据)变得相对容易。

在Retrofit中,你要配置用于数据序列化的转换器。通常,为了将对象序列化和反序列化为JSON,你使用一个开源的Java库--Gson。此外,如果你需要,你可以向Retrofit添加自定义转换器来处理XML或其他协议。

为了发出HTTP请求,Retrofit使用OkHttp库。OkHttp是一个纯HTTP/SPDY客户端,负责任何低级别的网络操作、缓存、请求和响应操作。相比之下,Retrofit是一个建立在OkHttp之上的高级REST抽象概念。Retrofit与OkHttp强耦合,并大量使用它。

现在你知道所有的东西都是密切相关的,我们将同时使用所有这3个库。我们的第一个目标是使用Retrofit从互联网上获取所有的加密货币列表。在调用服务器时,我们将使用一个特殊的OkHttp拦截器类,用于CoinMarketCap API认证。我们将得到一个JSON数据结果,然后使用Gson库进行转换。

快速设置Retrofit 2只是为了先试试

当学习新东西时,我喜欢尽快在实践中尝试。我们将对Retrofit 2采用类似的方法,让你更快地了解它。现在不要担心代码质量或任何编程原则或优化--我们只是写一些代码,使Retrofit 2在我们的项目中工作,并讨论它的作用。

按照以下步骤在我的加密货币应用程序项目中设置Retrofit 2。

首先,为应用程序赋予INTERNET权限

我们将在一个可通过互联网访问的服务器上执行HTTP请求。在你的Manifest文件中加入这几行,给予这个权限。

medium.com/media/20915…

然后,你应该添加库的依赖性

找到最新的Retrofit版本。此外,你应该知道Retrofit并没有集成JSON转换器。由于我们将获得JSON格式的响应,所以我们也需要在依赖项中手动包含转换器。我们将使用谷歌最新的JSON转换器Gson版本。让我们在你的gradle文件中加入这些行。

medium.com/media/d2824…

正如你从我的评论中注意到的,OkHttp依赖已经和Retrofit 2依赖一起被运出。版本只是一个单独的gradle文件,以方便使用。

medium.com/media/54e06…

接下来设置Retrofit接口

这是一个声明我们的请求和它们的类型的接口。这里我们定义了客户端的API。

medium.com/media/04440…

并设置数据类

数据类是POJO(Plain Old Java Objects),代表我们要进行的API调用的响应。

medium.com/media/52678…

创建一个特殊的拦截器类,以便在调用服务器时进行认证

对于任何需要认证才能获得成功响应的API来说,这种情况是特别的。拦截器是定制你的请求的一个强有力的方法。我们将拦截实际请求,并添加单独的请求头,这将验证由CoinMarketCap专业API开发者门户网站提供的API密钥的调用。要获得你的,你需要在那里注册。

medium.com/media/d8cfa…

最后,将这段代码添加到我们的活动中,以查看Retrofit的工作情况

我想尽快让你的手变脏,所以我把所有东西都放在一个地方。这不是正确的方法,但它是最快的,而只是为了快速看到一个可视化的结果。

medium.com/media/4af70…

你可以在这里探索代码。记住,这只是一个初步的简化实施版本,让你更好地了解这个想法。

使用OkHttp 3和Gson的Retrofit 2的最终正确设置

好了,在快速的实验之后,是时候把这个Retrofit的实现带到下一个层次了。我们已经成功地得到了数据,但并不正确。我们缺少像加载、错误和成功这样的状态。我们的代码是混合的,没有关注点的分离。把所有的代码写在一个活动或一个片段中是一个常见的错误。我们的活动类是基于UI的,应该只包含处理UI和操作系统交互的逻辑。

实际上,在这个快速设置之后,我做了很多工作,做了很多修改。把所有改动的代码放在文章中是没有意义的。你最好在这里浏览一下第五部分的最终代码库。我对所有的东西都做了很好的注释,我的代码对你来说应该是很清楚的。但我要谈的是我所做的最重要的事情以及我为什么要这么做。

改进的第一步是开始使用依赖性注入。记得在上一部分中,我们已经在项目中正确实现了Dagger 2。所以我把它用于Retrofit的设置。

medium.com/media/8c731…

现在,正如你所看到的,Retrofit与活动类分开了,这是它应该做的。它将只被初始化一次并在整个应用程序中使用。

你可能已经注意到在创建Retrofit builder实例时,我们使用addCallAdapterFactory添加了一个特殊的Retrofit调用适配器。默认情况下,Retrofit返回一个Call,但是对于我们的项目,我们要求它返回一个LiveData类型。为了做到这一点,我们需要通过使用LiveDataCallAdapterFactory来添加LiveDataCallAdapter。

medium.com/media/551a6…medium.com/media/68565…

现在我们将从ApiService接口中定义的Retrofit服务方法中获得LiveData而不是Call作为返回类型。

另一个重要步骤是开始使用Repository模式。我在第3部分谈到了它。查看那篇文章中我们的MVVM架构模式,以记住它的位置。

正如你在图片中看到的,Repository是一个独立的数据层。它是我们获取或发送数据的单一来源。当我们使用Repository时,我们遵循关注点分离的原则。我们可以有不同的数据源(比如在我们的案例中,来自SQLite数据库的持久化数据和来自Web服务的数据),但Repository始终是所有应用程序数据的单一真实来源。

我们将使用Repository来代替直接与Retrofit的实现进行通信。对于每一种实体,我们都会有一个单独的Repository。

medium.com/media/810d4…

你注意到在CryptocurrencyRepository类的代码中,我正在使用NetworkBoundResource抽象类。它是什么,为什么我们需要它?

NetworkBoundResource是一个小但非常重要的辅助类,它将允许我们保持本地数据库和网络服务之间的同步。我们的目标是建立一个现代的应用程序,即使我们的设备处于离线状态也能顺利工作。同时,在这个类的帮助下,我们将能够为用户直观地展示不同的网络状态,如错误或加载。

NetworkBoundResource首先观察数据库中的资源。当条目第一次从数据库加载时,它检查结果是否足够好,是否应该从网络中重新获取。注意这两种情况可以同时发生,因为你可能想在从网络上更新数据的同时显示缓存的数据。

如果网络调用成功完成,它会将响应保存到数据库中,并重新初始化流。如果网络请求失败,NetworkBoundResource会直接派发一个失败。

medium.com/media/adafe…

在引擎盖下,NetworkBoundResource类是通过使用MediatorLiveData和它同时观察多个LiveData源的能力来实现的。这里我们有两个LiveData源:数据库和网络调用响应。这两个LiveData都被包装成一个MediatorLiveData,由NetworkBoundResource公开。

让我们仔细看看NetworkBoundResource将如何在我们的应用程序中工作。想象一下,用户将启动应用程序并点击右下角的浮动操作按钮。该应用程序将启动添加加密货币的屏幕。现在我们可以分析NetworkBoundResource在其中的使用情况。

如果应用程序是新安装的,并且是第一次启动,那么本地数据库内将不会有任何数据存储。因为没有数据可以显示,所以会显示一个加载进度条的用户界面。同时,应用程序将通过网络服务向服务器发出请求调用,以获得所有加密货币列表。

如果响应不成功,那么将显示错误信息UI,并可以通过按下按钮重试调用。当请求调用最后成功时,那么响应数据将被保存到本地SQLite数据库中。

如果我们下次再回到同一个屏幕,应用程序将从数据库中加载数据,而不是再次调用互联网。但用户可以通过实现pull-to-refresh功能来要求新的数据更新。旧的数据信息将在网络调用发生时显示。所有这些都是在NetworkBoundResource的帮助下完成的。

在我们的Repository和LiveDataCallAdapter中使用的另一个类是ApiResponse,所有的 "魔法 "都在这里发生。实际上ApiResponse只是Retrofit2.Response类的一个简单的普通包装,它将每个响应转换为LiveData的一个实例。

在这个包装类里面,如果我们的响应有错误,我们会使用Gson库将错误转换为JSON对象。然而,如果响应是成功的,那么就会使用Gson转换器来实现JSON到POJO对象的映射。我们已经在Dagger AppModule函数provideApiService中用GsonConverterFactory创建retrofit builder实例时添加了它。

图像加载的Glide

什么是Glide?来自文档。

Glide是一个快速高效的开源媒体管理和图片加载框架,它将媒体解码、内存和磁盘缓存以及资源池包装成一个简单易用的界面。

Glide的主要重点是使滚动任何种类的图片列表尽可能的顺畅和快速,但它也对几乎所有需要获取、调整大小和显示远程图片的情况有效。

听起来像是一个复杂的库,它提供了许多有用的功能,你不会想自己全部开发。在My Crypto Coins应用程序中,我们有几个列表屏幕,我们需要显示多个加密货币的标志--从互联网上一次性获取的图片--并仍然确保用户的流畅滚动体验。所以这个库非常符合我们的需求。同时这个库在安卓开发者中非常流行。

在My Crypto Coins应用项目中设置Glide的步骤。

声明依赖性

获取最新的Glide版本。同样,版本是项目的一个单独的文件version.gradle。

因为我们想在项目中使用网络库OkHttp进行所有的网络操作,所以我们需要为它包含特定的Glide集成,而不是默认的。另外,由于Glide要执行网络请求,通过互联网加载图片,我们需要在AndroidManifest.xml文件中加入权限INTERNET--但我们已经在Retrofit设置中做了这个。

创建AppGlideModule

我们将使用的Glide v4,为应用程序提供了一个生成的API。它将使用一个注释处理器来生成一个API,允许应用程序扩展Glide的API并包括由集成库提供的组件。对于任何应用程序访问生成的Glide API,我们需要包括一个适当的注释的AppGlideModule实现。生成的API只能有一个实现,每个应用程序只能有一个AppGlideModule。

让我们在你的应用项目的某个地方创建一个扩展AppGlideModule的类。

medium.com/media/11d72…

即使我们的应用程序没有改变任何额外的设置或实现AppGlideModule的任何方法,我们仍然需要有它的实现来使用Glide。你不需要实现AppGlideModule中的任何方法来生成API。只要它扩展了AppGlideModule并且用@GlideModule来注解,你可以让这个类保持空白。

使用Glide-生成的API

当使用AppGlideModule时,应用程序可以通过用GlideApp.with()开始所有的加载来使用API。这段代码显示了我是如何使用Glide来加载并在添加加密货币屏幕的所有加密货币列表中显示加密货币标识的。

medium.com/media/2935c…

正如你所看到的,你可以只用几行代码就开始使用Glide,让它为你做所有的艰苦工作。这是很直接的。

Kotlin Coroutines

在构建这个应用程序时,我们将面临运行耗时任务的情况,例如将数据写入数据库或从数据库中读取,从网络中获取数据以及其他。所有这些常见的任务完成的时间都超过了Android框架的主线程所允许的时间。

主线程是一个单一的线程,它处理所有对用户界面的更新。开发人员需要避免阻塞它,以避免应用程序冻结甚至崩溃,出现应用程序无响应的对话框。Kotlin coroutines将通过引入主线程安全为我们解决这个问题。这是我们想为 "我的加密货币 "应用程序添加的最后一块缺失。

Coroutines是Kotlin的一项功能,它将长期运行任务的异步回调,如数据库或网络访问,转换成顺序代码。有了Coroutines,你可以使用同步风格来编写异步代码,这些代码传统上是用回调模式来编写的。一个函数的返回值将提供异步调用的结果。按顺序编写的代码通常更容易阅读,甚至可以使用异常等语言特性。

因此,在这个应用程序中,当我们需要等待一个长期运行的任务的结果,然后继续执行时,我们将在任何地方使用coroutines。让我们看看ViewModel的一个具体实现,我们将重新尝试从服务器上获取主屏幕上的加密货币的最新数据。

首先在项目中加入冠词:

medium.com/media/e7446…

然后,我们将创建一个抽象类,它将成为任何需要有共同功能的ViewModel的基类,比如我们案例中的coroutines:

medium.com/media/0f20a…

在这里,我们创建特定的coroutine范围,它将通过其工作控制coroutines的生命周期。正如你所看到的,作用域允许你指定一个默认的调度器,控制哪个线程运行一个coroutine。当ViewModel不再被使用时,我们取消viewModelJob,随之,uiScope启动的每个coroutine也将被取消。

最后,实现重试功能:

medium.com/media/85f86…

在这里,我们调用了一个标有特殊的Kotlin关键字suspend的函数,用于coroutine。这意味着该函数会暂停执行,直到结果准备好,然后它就会带着结果重新开始。在它暂停等待结果的时候,它解除了它所运行的线程的阻塞。

另外,在一个暂停函数中,我们可以调用另一个暂停函数。正如你所看到的,我们通过调用在不同线程上执行的标有withContext的新暂停函数来做到这一点。

所有这些代码的想法是,我们可以将多个调用结合起来,形成看起来不错的顺序代码。首先,我们请求从本地数据库中获取我们拥有的加密货币的ID,并等待响应。只有在我们得到它之后,我们才使用响应的ID与Retrofit进行新的调用,以获得这些更新的加密货币值。这就是我们的重试功能。

我们成功了!最后的想法,存储库,应用程序和演示

恭喜你,如果你成功地到达了终点,我很高兴。创建这个应用程序的所有最重要的点都已涵盖。这一部分做了很多新的东西,很多都没有包括在这篇文章中,但是我对我的代码进行了很好的注释,所以你不应该迷失在其中。在GitHub上查看第五部分的最终代码。

在GitHub上查看源代码

对我个人来说,最大的挑战不是学习新技术,不是开发应用程序,而是写所有这些文章。事实上,我对自己完成这一挑战感到非常高兴。与教别人相比,学习和开发很容易,但这时你可以更好地理解这个主题。如果你正在寻找学习新事物的最佳方法,我的建议是立即开始自己创造一些东西。我保证你会学到很多东西,而且很快。

所有这些文章都是基于1.0.0版本的 "Kriptofolio"(以前是 "我的加密货币")应用程序,你可以在这里下载单独的APK文件。但如果你直接从商店安装并评价最新的应用程序版本,我将非常高兴。