Flutter实践二:repository模式

1.repository

几乎所有的APP,从简单的到最复杂的,在它们的架构里几乎都包括状态管理和数据源这两部分。状态管理常见的有Bloc、Cubit、Provider、ViewModel等,数据源则是一些直接和数据库或者网络客户端进行交互,取得相应的数据,并将其解析为模型的类。

一般地,状态管理器直接和数据源通信。当只有一个数据源的时候,事情比较简单。但是当有多个数据源,譬如说APP需要缓存数据的时候,事情就变得复杂起来了。

缓存也就是将你的API请求结果备份在本地数据库中。这允许你后面可以在网络异常的时候,仍然能获得该数据。这也能帮助你在下次打开这个页面的时候,可以更快地进行响应和节省带宽。

当你为特定页面的数据缓存数据的时候,状态管理器负责直接和数据源进行交互,协调数据库和网络数据源。

repository模式中repository类位于状态管理器和数据源之间,接管原本由状态管理器负责的数据协调工作,也就意味着你的状态管理器不需要关心数据的来源。

repository允许你在不同状态管理器之间共享数据协调的逻辑。repository本身很简单,但是对代码库具有非常深远的价值。

2.class dependency

类依赖指的是一个类依赖另一个类来实现它的工作。例如QuoteRepository依赖FavQsApi来获取数据,这样就使得FavQsApi成为了QuoteRepository的类依赖。有两种方式来获取一个类的类依赖实例:

1.自己实例化:你可以在构造函数、属性声明等地方实例化依赖的类,例如:

好处是你不必暴露内部的依赖给类的用户。坏处是如果其他repository也依赖同一个类,你不能在不同repository之间共享同一个依赖类实例,并且你要在所有地方重复这个实例化逻辑。

2.构造函数要求传入一个实例:例如:

这种方式的优劣点与前面一种刚好相反。哪种更好的呢?这个视情况而定。

3.处理类依赖

在实践一中,我们为每个repository创建了各自的包。因为一个repository经常被多个功能所使用。这使得将它们放在某个功能包里变得不可行,因为功能之间不能相互依赖。因此,你不能在多个功能里使用同一个repository。

一种选择是创建单个包来存放所有的repository,让所有的功能都能访问。但是包被认为是经常会一起使用的东西,单个功能不大可能需要用到全部的repository。那么最终只剩下一种选择:为每个repository创建各自的包。

在packages文件夹下创建一个quote_repository包。在quote_repository.dart中

加入下列代码:

1.remoteApi是FavQsApi,用于向远程API发送和请求数据。FavQsApi来自另一个fav_qs_api包:

2.QuoteLocalStorage用于从设备本地存储获取和保存名言。QuoteLocalStorage来自当前的包:

因为QuoteLocalStorage只和名言打交道,出了quote_repository包就没啥用途。而FavQsApi更通用,因为它同时处理名言和身份验证调用。这使得它也适用于user_repository包。如您所知,当您需要在两个包之间共享代码时,在本例中为两个存储库 — 您必须创建第三个存储库。

QuoteLocalStorage依赖KeyValueStorage,它来自单独的key_value_storage包:

将 KeyValueStorage 视为 WonderWords 的本地数据库。它是流行的 Hive包的包装器。它必须成为内部包,才能将所有 Hive 配置集中在一个地方。

回到QuoteRepository构造函数,你是需要在构造函数里要求类依赖,还是自己在内部实例化依赖的类?

其文件位于您正在处理的同一包中的类依赖项应在构造函数中实例化。QuoteLocalStorage 就是这种情况。

来自其他内部包(如 KeyValueStorage 和 FavQsApi)的类依赖项必须在构造函数中接收。

请注意,即使 QuoteLocalStorage 在 QuoteRepository 的构造函数中实例化,您仍然允许通过可选参数在构造函数中接收它。此可选参数背后的意图不是向 QuoteRepository 的用户公开类依赖项。相反,它的存在只是为了允许您在自动化测试中提供模拟实例,这就是您使用 @visibleForTesting 对其进行注释的原因。

4.创建桶文件

QuoteRepository代码都在src目录下,那么状态管理器无法导入QuoteRepository,因为它们被视作是私有的。dart包布局约定中建议将所有代码放在src目录里,通过从直接放置在 lib 下的“导出器”文件中导出它们,有意识地公开要公开的文件。这个导出文件也被称作桶文件。约定的一部分是为 barrel 文件指定与包相同的名称。

在桶文件quote_repository.dart中插入如下代码:

export 'src/quote_repository.dart'

5.分页

分页是将一个API的结果分割成多个批次,每个批次叫做分页。这能让用户无需等待太久,就能取得数据,与APP进行交互,同时也减少了蜂窝数据的消耗。用户可以按需渐进式地加载更多分页的数据。

6.Stream

Dart中有两种异步编程类型:Future和Stream。Future代表你不能立刻获得的值,例如getQuote()返回Future<Quote>,而不是Quote。因为需要花费一些网络请求的时间来获得Quote。getQuote函数立即返回一个通道--Channel给调用者。随后当请求成功时,会通过这个通道发送真实数据。

Stream是Future的复杂形式。Future每次发送一个数据,而Stream可以发送多个数据。getQuoteListPage()返回一个Stream,而不是Future。这和下面的数据获取策略有关。

7.数据获取策略

当你决定缓存网络调用的结果时,你需要考虑后面采取哪种策略来传递这些数据。

是否总是返回缓存的数据?万一它们过期了呢?

接着你是否需要每次从server获取数据,而只是将缓存的数据作为网络请求失败时的回退?是的话,频繁的加载时间是否会让用户感到不安?假设数据不经常更改,进行不必要的网络调用会浪费蜂窝流量吗?

这些问题没有明确的答案。你必须考虑每种情况。数据过期的频率如何?在这种情况下,你是否应该优先考虑速度或准确性?
因此,是时候更具体地决定 WonderWords 主页面的最佳策略是什么了。
当用户打开 WonderWords 时,他们可能希望每次都能看到新的名言。如果他们非常喜欢一句话,想再看一遍,他们总是可以收藏这句话。
到目前为止,可以肯定的是,最好的策略是每次都从服务器获取名言,而不用担心缓存。但是,如果网络调用失败怎么办?在这种情况下,最好将显示缓存的数据作为回退。
好了,你现在有了一个政策。你将继续每次从服务器获取名言,但随后缓存这些名言,以便将来在网络调用失败时可以使用它们。

你的新策略非常可靠,但仍然有一个巨大的缺陷:每次都从 API 获取项目意味着用户的加载时间频繁且漫长。当用户打开应用时,他们希望尽快开始与应用交互。

你无法让服务器更快地给你返回数据。但是,既然你无论何时都会缓存名言,那么你可以进行一个主要操作:你可以显示缓存的名言,而不是在用户每次打开应用程序时都显示加载页面,同时在后台获取新的名言。

注意:使用此新策略时,从存储库返回 Future 已不满足需求。当状态管理器要求第一个页数据时,你将首先发送缓存的数据(如果有的话),然后发送来自API的数据。处理多次发送数据时,你需要使用 Stream。

你现在拥有了为 WonderWords 主页面量身定制的策略。坏消息是,即使仅考虑了主屏幕,这个设计的策略并不适合所有的情况。
 

8.考虑额外的情况

考虑这些边界情况:

如果用户想要通过下拉列表来有目的地刷新列表,该怎么办?在这种情况下,您不能先返回“旧”数据。此外,用户不介意看到加载页面;毕竟,他们清楚自己刚才请求了新数据。
如果用户搜索特定名言,但随后清除了搜索框,以便他们可以返回到之前看到的名言,该怎么办?在这种情况下,最好只显示缓存的数据。后面你不需要展示新数据,因为用户只想返回到以前的状态。

这意味着根据页面的复杂程度,单个数据获取策略可能还不够。在这种情况下,你能做的就是让状态管理器为用户体验旅程的每一步决定最佳策略。这就是getQuoteListPage() 具有fetchPolicy 参数的原因。

fetchPolicy 的类型为 QuoteListPageFetchPolicy ,这是你正在处理的文件末尾的枚举。以下是枚举的值:

cacheAndNetwork:如果 HTTP 调用成功,则首先发出缓存的名言(如果有),然后从服务器发出名言。在用户首次打开应用时很有用。

networkOnly:在任何情况下都不要使用缓存。如果服务器请求失败,请告知用户。当用户有意识地刷新列表时很有用。

networkPreferably:首选使用服务器。如果请求失败,请尝试使用缓存。如果缓存中没有任何内容,则让用户知道发生了错误。当用户请求后续页面时很有用。

cachePreferably:首选使用缓存。如果缓存中没有任何内容,请尝试使用服务器。当用户清除标签或搜索框时很有用。

注意:只有cacheAndNetwork能发送两次数据,其他策略返回类型用Future就足够了。

9.填充缓存

四个受支持的策略中的每一个都可能需要在某个时间点从服务器获取数据;毕竟,没有 cacheOnly 策略。所以第一步是创建一个实用程序函数,用于从服务器获取数据并用它填充缓存。这样,您就可以为所有策略重用 getQuoteListPage()中的该函数。

打开lib/quote_repository/src/quote_repository.dart,添加代码:

1.返回类型为Future。

2.从远端API获取新的分页。

3.不应该缓存过滤的结果。

4.每次获得新的第一页时,都必须从缓存中删除之前存储的所有后续分页。这迫使将来从网络获取后面的分页,因此您不会冒着混合更新和过时分页的风险。不这样做会带来问题,例如,如果曾经位于第二页上的名言移至第一页,如果将缓存页面和新页面混合在一起,则可能会显示该名言两次。

10.模型分离

通过调用remoteApi.getQuoteListPage()从API获取的对象是QuoteListPageRM类型的,RM表示为Remote Model。
而调用_localStorage.upsertQuoteListPage()从缓存获取的对象是QuoteListPageCM类型的,CM表示为cache Model。

两者类型不一致。而repository的getQuoteListPage()返回的是QuoteListPage类型。

当涉及到其模型时,应用的每一层都有自己的规范。例如,您的远程模型复制了 JSON 的结构,并且充满了 JSON 解析注释。另一方面,缓存模型中充满了数据库内容,具体取决于您使用的数据库包。更不用说某些属性类型也可能不同;例如,有些内容在API里是字符串类型,而在数据库里是枚举类型。

最后,由于存储库的数据有时来自数据库,有时来自网络,因此您需要一个中立、公正的模型来返回给repository的用户。这被称为领域模型,在这个例子中是QuoteListPage。

换句话说,领域模型是与它们的来源无关的模型。

WonderWords 在单独的 domain_models 包中定义了领域模型,所有repository的包都依赖于该模型。这样做允许不同的repository共享相同的领域模型。

WonderWords 还遵循了另一个良好的做法:除了域模型之外,它还在同一包中定义了领域异常。就像在一切正常时返回中性/领域模型一样,当出现问题时,也可以抛出中性/领域异常。

你可以看到这发生在你刚刚写的那个 catch 块中。每当你捕获到来自 fav_qs_api 包的 EmptySearchResultFavQsException 时,你就用来自 domain_models 的 EmptySearchResultException 替换它。

拥有这些领域异常似乎没有必要,但这是状态管理器根据发生的异常执行自定义逻辑的唯一方法。例如,由于quote_list功能不依赖于 fav_qs_api 包,QuoteListBloc 无法检查异常是否是
EmptySearchResultFavQsException ,仅仅是因为它不知道该类型。但是,由于 quote_list 包确实依赖于 domain_models,因此 QuoteListBloc 可以毫无问题地验证异常是否为 EmptySearchResultException,并使用它来向用户显示自定义消息。

11.Mappers

现在你明白了为什么每个数据源需要不同的模型,并且需要一个中立的模型才能最终从repository返回。但是,如何从一种模型类型转到另一种模型类型?您可能已经猜到您需要某种转换器。这些转换器称为Mappers。

映射器只是从一个模型中获取对象,并返回另一个模型的对象的函数。任何必要的转换逻辑都发生在中间。例如:

您所要做的就是使用收到的 QuoteCM 对象中的值实例化一个新的 Quote 对象。

然后,要使用此映射器函数,您只需执行以下操作:

你也可以使用dart的扩展函数来实现mapper:

现在,您不必再接收QuoteCM对象。使用 Dart 扩展函数可以创建一个函数,该函数的工作方式就像你在 QuoteCM 中声明它一样。请注意,您只需键入 id 或 body ,就能访问 QuoteCM 中的属性。

调用mapper就变成下面这样:

12.支持不同的数据策略

现在你终于理解了 _getQuoteListPageFromNetwork() 中发生的一切。来到上面getQuoteListPage()中,加入以下实现:

1.有三种情况,其中你想跳过缓存查找并直接从网络返回数据:如果用户选择了标签,如果他们正在搜索,或者函数的调用方显式指定了networkOnly 策略。

2.早先创建的函数

3.在 Dart 函数中生成 Stream 的最简单方法是将 async* 添加到函数的头部,然后在想要发出新项时使用 yield 关键字。

现在,你已经涵盖了不需要缓存查找的所有方案,即当用户具有筛选器或策略为 networkOnly 时。现在,您将处理强制执行缓存查找的方案。

替换上述代码中的“// TODO: Cover other fetch policies.”:

1. 你的本地存储将收藏夹列表保存在单独的存储桶中,因此您必须指定是存储常规列表还是收藏夹列表。
2. fetchPolicy 是 cacheAndNetwork 还是 cachePreferably,都必须发送缓存的分页。这两种策略之间的区别在于,对于cacheAndNetwork ,你稍后还会发送服务器的分页。
3.要返回缓存的页面,即 QuoteListPageCM ,必须调用mapper 函数将其转换为领域模型QuoteListPage 。
4. 如果策略是 cachePreferably,并且您已成功发出缓存的分页,则无需执行其他操作。您可以在此处返回并关闭Stream。

下一步是从 API 获取页面,以完成其余三个场景:

1.当策略为cacheAndNetwork时。你已经介绍了缓存部分,但是 AndNetwork部分还没有。
2.当策略是cachePreferably时,你无法从缓存中获取分页。
3.当策略是networkPreferably。

1.如果策略是 networkPreferably,并且你在尝试从网络获取分页时遇到错误,则尝试通过发出缓存的分页来恢复错误(如果有)。

2.如果策略是 cacheAndNetwork 或 cachePreferably ,则你之前已经发出了缓存的分页,因此你现在唯一的选择是如果网络调用失败,rethrow错误。这样,状态管理器就可以通过向用户显示错误来正确处理它。

在你的设备上使用该应用程序,并注意它如何利用不同的获取策略。例如,当你通过下拉列表来刷新列表时,加载屏幕需要更长的时间;这是正在使用的 networkOnly 策略。当你添加标签然后将其删除时,应用程序会很快恢复到以前的状态;这是由于 cachePreferably 策略。当你关闭应用程序并重新打开它时,数据几乎会立即加载,但随后你可以在几秒钟后看到它是如何换出的;这是 cacheAndNetwork 的实际应用。

参考:

《Real-World Flutter by Tutorials》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/139969.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring的Redis客户端

如何在Spring中操作redis 在创建springboot项目的时候引入redis的依赖. 在配置文件里指定redis主机的地址和端口,此处我们配置了ssh隧道,所以连接的就是本机的8888端口. 创建一个controller类,注入操作redis的对象. 前面使用jedis,是通过jedis对象里的各种方法来操作redis的,此…

python用pychart库,实现将经纬度信息在地图上显示

python使用pyecharts对给到的经纬度数据进行位置标注&#xff0c;下面是批量更新。给入数据&#xff0c;将地图生成。实验数据在下面附件。 from pyecharts import options as opts from pyecharts.charts import Geo import osfolder_path F:\\GPS file_names os.listdir(f…

腾讯小程序音视频 TRTC live-pusher 黑屏等各种问题

微信小程序进行音视频开发, 主要会用到live-player live-pusher,这两个媒体组件. 在开发的过程中,会遇到各种各样的问题,其中最直接的就是黑屏问题, 以下就这个问题进行整理. 文档: https://developers.weixin.qq.com/miniprogram/dev/component/live-player.html https://dev…

【Python】jupyter notebook(学习笔记)

Jupyter Notebook初见 1、Jupyter Notebook介绍 web版的ipython 编程、写文档、记笔记、展示 格式.ipynb 2、为什么使用Jupyter Notebook? 画图方面的优势&#xff1a;图像的生成不会堵塞后面代码的执行数据展示方面的优势&#xff1a;生成的数据可以保存在文件中 3、J…

Springboot细节补充

一、Bean是怎么装配的&#xff1f; 1、bean扫描 在之前的ssm中&#xff0c;spring要么用标签的形式来扫描包&#xff0c;要么使用注解ComponentScan来扫描 但是在Springboot中&#xff0c;启动类上默认有一个注解SpringBootApplication&#xff0c;里面就包含了ComponentScan…

CCF ChinaSoft 2023 论坛巡礼 | 云原生与智能化运维论坛

2023年CCF中国软件大会&#xff08;CCF ChinaSoft 2023&#xff09;由CCF主办&#xff0c;CCF系统软件专委会、形式化方法专委会、软件工程专委会以及复旦大学联合承办&#xff0c;将于2023年12月1-3日在上海国际会议中心举行。 本次大会主题是“智能化软件创新推动数字经济与社…

[原创]仅需小小的改变,B++ Builder 12的代码完成提示即可完美工作.

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XXQQ: 643439947 个人网站: 80x86汇编小站 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi、XCode、Eclipse、C Bui…

隔离在高可用架构中的使用

写作目的 最近看到了河北王校长隔离的视频&#xff0c;结合自己在工作中的应用&#xff0c;分享常见的隔离落地方案。 隔离落地方案 服务环境隔离 因为我们的项目服务于整个国内的多条产品线&#xff0c;也服务于国外。为了低成本所以使用一套代码。在产品线之间隔离&#…

14.求n!和1!+2!+...+20!和2^1+2^2+……++2^20和2^1+2^3+……++2^19

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码 四、举一反三一、题目描述&#xff1a;求1&#xff01;2&#xff01;...20! 二、题目分析 三、解题 程序运行代码1程序运行代码2 一、题目描述&#xff1a;求求2^1^2^2^……2^20^二、解题 程序运行代码 一、题目…

大数据毕业设计选题推荐-消防监控平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

通过百度翻译API完成Java中的中英文翻译

因为要做英文文献索引分词&#xff0c;所以对于索引词汇必须得是英文&#xff0c;将表中的中文都转换成英文 这里用到百度的翻译API 首先需要注册成为百度翻译开发者&#xff1a;百度翻译开放平台 注册成为个人开发者就可以&#xff0c; 这里可以完善相关信息&#xff0c;要记…

每日互动(个推)全新推出AITA智选人群工具,助力品牌营销升级

11月9日&#xff0c;在2023年世界互联网大会“新产品新技术特色场景发布活动”上&#xff0c;数据智能服务商每日互动&#xff08;个推&#xff09;全新打造的AITA智选人群工具首次正式对外发布。作为每日互动在品牌营销领域的大模型应用最新成果&#xff0c;AITA智选人群工具将…

《数据结构、算法与应用C++语言描述》-代码实现键值有序链表跳表

跳表 定义 在一个用有序链表描述的 n 个数对的字典中进行查找&#xff0c;至多需要 n 次关键字比较。如果在链表的中部节点加一个指针&#xff0c;则比较次数可以减少到 n/21。这时&#xff0c;为了查找一个数对&#xff0c;首先与中间的数对比较。如果查找的数对关键字比较小…

【数据仓库】数仓分层方法

文章目录 一. 数仓分层的意义1. 清晰数据结构。2. 减少重复开发3. 方便数据血缘追踪4. 把复杂问题简单化5. 屏蔽原始数据的异常6. 数据仓库的可维护性 二. 如何进行数仓分层&#xff1f;1. ODS层2. DW层2.1. DW层分类2.2. DWD层2.3. DWS 3. ADS层 4、层次调用规范 一. 数仓分层…

全域全自主建设,亚信科技AntDB数据库助力广电5G业务上线运行

自2019年6月&#xff0c;中国广电成功获得5G牌照以来&#xff0c;迅速推进网络建设目标&#xff0c;成为5G网络覆盖广、应用场景多、用户体验出色的第四大运营商。其依托全球独有的700MHz频谱资源&#xff0c;具备覆盖能力强、容量足、速率高的优势。通过不断深化和中国移动的共…

【JavaEE】Servlet(创建Maven、引入依赖、创建目录、编写及打包、部署和验证、smart Tomcat)

一、什么是Servlet&#xff1f; Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app 1.1 Servlet能干什么&#xff1f; &#x1f695;允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类…

自动化测试项目:浅谈分层设计的思路

本文以笔者当前使用的自动化测试项目为例&#xff0c;浅谈分层设计的思路&#xff0c;不涉及到具体的代码细节和某个框架的实现原理&#xff0c;重点关注在分层前后的使用对比&#xff0c;可能会以一些伪代码为例来说明举例。 接口测试三要素&#xff1a; 参数构造发起请求&a…

通配符SSL证书:保护多个子域名的最佳选择

很多网站和应用程序采用了多个子域名来提供不同的服务或内容。为了确保这些子域名的安全性和数据加密&#xff0c;使用通配符SSL证书成为一个理想的解决方案。 首先&#xff0c;通配符SSL证书的最大优势在于其灵活性和经济性。不同于单个SSL证书只能保护一个特定的域名&#xf…

Python实现WOA智能鲸鱼优化算法优化循环神经网络回归模型(LSTM回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

【游戏开发算法每日一记】使用随机prime算法生成错综复杂效果的迷宫(C#和C++)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…