在构建domain层的文章之后,我们接下来说说构建presentation层。我之所以写这篇文章是因为看到有大量的工程从已有代码迁移到MVP结构时,对于什么该属于presentation层,什么该属于UI层的划分不清晰。我在之前的项目中看到一个评论:“我不能修改业务逻辑” 。如果你不能区分什么是属于domain层的,那么在分离责任时,你就会犯错,这是很糟糕的事情。你为什么要在不清楚最基本逻辑的情况下去试图创建一个清晰的结构。
下面我会给出一些案列以及我是如何解决的。这篇文章主要是理清概念,你可以用多种方式去实现。
两个界面间的切换可以是两个fragment之间,两个activity之间,打开一个dialog,打开一个activity等等。这是如何做到的属于实现的细节,是传递机制的责任。但是导致界面切换的行为呢?这个行为就是我们presentation层的责任。我们的presenter应该知道要做什么和我们的实现如何去做。这个情境中,要如何做呢?如何导航到另一个界面呢?打开一个新的activity,然后,问题来了,我之前说过我的presentation层是纯java的,所以我不能使用任何Android相关的东西,这如何实现呢?使用抽象。你可以创建一个只有navigate方法的NacigationCommand接口,在presenter需要时调用它,在视图层中实现它。假设我们有一个Activity A要在按钮点击后跳转Activity B,下面是序列图。
代码是这样的:
Android module (View layer)
public class ActivityA extends Activity {
@OnClick(R.id.someButton)
public void onSomeButtonClicked() {
presenter.onSomeButtonClicked();
}
}
public class ToActivityB implements NavigationCommand {
private final Activity currentActivity;
public ToActivityB(Activity activity) {
currentActivity = activity;
}
@Override
public void navigate() {
currentActivity.startActivity();
}
}
Java module (Presentation layer)
public interface NavigationCommand {
public void navigate();
}
public class PresenterA {
private final NavigationCommand toBNavigation;
public PresenterA(NavigationCommand toBNavigation) {
this.toBNavigation = toBNavigation;
}
public void onSomeButtonClicked() {
toBNavigation.navigate();
}
}
通过这种处理,我们解耦了不同层之间的责任。我们把导航到Activity B的操作提取成了一个可以在项目中复用的类。我们可以测试我们的presenter来调用导航命令,并且如果我们更改了界面的展现形式(例如从Activities转换为Fragments),我们的presentation层不需要改变。开闭原则万岁!
另一个问题是如果你在一个presenter中有多个导航命令,那么构造函数的参数会变得很奇怪
public class PresenterA {
private final NavigationCommand toBNavigation;
private final NavigationCommand toCNavigation;
public PresenterA(NavigationCommand toBNavigation, NavigationCommand toCNavigation) {
this.toBNavigation = toBNavigation;
this.toCNavigation = toCNavigation;
}
}
实例化这个presenter时很难知道导航命令参数的顺序,你只能依靠命名来区分。但是我们可以定义一个继承自NavigationCommand接口的接口来表示导航的子类型。另一个解决方法是,如果你在使用依赖注入,那能不能实现一个Qualifier来指定你需要传递那种类型的参数。
某些场景下你可能需要导航到界面A或者界面B,那么我们可以修改NavigationCommand接口。
public interface ToScreenBNavigationCommand extends NavigationCommand {
void setMyParameterToNavigate(String parameter);
}
如果我们这么做,那么我们就可以在调用navigate方法前先设定好我们的目的地。
这个想法来自于Pedro,他在自己的项目EffetiveAndroidUI中已经实现了.
有很多Android组件可作为视图元素,这都不是presenter要关心的。记住View接口就像是presenter使用的契约。我们的一个界面中可以有多个View接口吗?当然可以!我如何在一个Activity中拥有多个View接口/presenter呢?让我们来看看Spotify的浏览界面:
在我看来,这个界面有一个水平滚动的播放列表,然后是可选操作的菜单,底部是当前播放的歌曲。在这个界面中,我们可以清晰的有多个抽象,用我的方式来看这个界面,各个抽象之间是没有关联的。所以让我们来考虑一下为每个概念实现一个View接口/presenter。
那么为什么创建那样的结构,这和创建一个Presenter包含所有的行为和自定义视图有什么区别?那么考虑一下谁负责填充视图,如果你打算重用这个组件你如何才能做到。RecommendedPlayLists可能需要访问网络来为当前用户获取推荐播放列表,那显然有必要为presenter创建一个自定义视图来实现它。这样就可以复用了。那么浏览菜单呢?这是另一个抽象了,他只是负责当你点击时导航到另一个界面(使用前面提到的navigation command!)最后,当前播放的歌曲也是一定会在其他地方复用的。
如你所见,一个界面可以包含多个View接口/Presenter,因为界面是一个汇总,而且会有多个责任,这完全依赖于设计。(记住一个责任就是一个变化的原因,这里的视图可能因为多种原因而变化。)
当然!一个view接口可以有多种实现。比如在Spotify中,我展示给你的那个界面可以控制当前的播放和展示歌曲信息,但是我们点击这个区域的话,就会跳转:
这两者不就是使用了不同的展示方式吗。所以也许我们可以复用presenter,使用不同的Android组件来实现view接口。但是,这个界面似乎有些额外的新功能,那应不应该也添加到这个presenter中呢?根据你的实际使用情况有不同的做法:你可以用不同的行为组合成一个presenter;使用两个不同的presenter,一个用来对应行为,一个对应presentation;或者直接使用一个presenter处理所有。记住,没有完美的解决方案,软件就是要取舍。
但是,这确实不是常见情况,通常一个Presenter只有一个view接口实现。
总结下目前我们都经历了什么
让我们再来看看其他的关键点
下面是一张来自软件Citymapper的截图,当你点击“Get me somewhere”按钮后,会展示一个可以选择起点和终点的界面:
你如何分解这个界面?我首先想到的是,开始位置在没有结束位置时是否还有意义?我认为没有。我会创建一个“PickLocation” presenter,它可以获知何时开始和结束位置被填写了。一个Activity内部包含一个有两个fragment的view pager可以满足视图层的实现。两个fragment都可以接触到同一个presenter并且调用“presenter.startLocationChanged()”和“presenter.endLocationChanged()”。
如果设计改变了怎么办?我们把两个tab换成了更适合的两步表单。然后我们就需要在开始位置fragment和结束位置fragment间切换。我们的视图层变了,但是presenter仍然是同一个,另外,我们也可以考虑在一个界面上展示两个地图,顶部展示开始位置,底部则是结束位置,这仍然是和之前相同的,变化的是视图层的实现,而不是我们的presenter。
那么我的presenter的生命周期是什么样的?这依赖于我们实现视图层使用的组件。
要解释生命周期,让我们来看一下Selltag app,一个二手交易应用。如图是我们如何创建一个商品:
如你所见是一个3步表单。忽视西班牙语。我想还是挺清晰的,“Siguiente”表示下一步,“Publicar”表示发布。
第一步,你为商品拍照,第二部,你填写名称,描述和价格,最后你发布它。
我的实现思路依然是使用一个presenter-“PublishProductPresenter”。这个presenter代表着完整的发布商品流程。在平板上又会是什么样的?也许这三步会整合到一起因为你的屏幕足够大。那如果是一个web应用呢?会不会因为你看到了不同的设计就认为是不同的实现方式呢?其实我们只需要改变视图层,presentation层则是同一个,因为它只对用户事件进行响应然后调用dmain层查询。
但是!我可以把他拆成3个presenter来使用,和你使用一个presenter是同样的方式。好吧,也许你是对的,但是你如何从把信息从第一步传递到发布商品的最后一步?你要在一个presenter中持有另一个presenter的引用?也许你会创建一个共享的对象来记录每一步更改的属性?这些听起来很危险并且难以debug在你出错的时候。
你可以创建一个拥有3个fragment的activity或者3个activity来实现视图层。
如果屏幕方向发生变化了怎么办?你的activity会被销毁,presenter同样。问题大部分是关于presenter是否该有状态或者是因为它不应该属于domain层,下面的例子我会介绍:
这是F-Droid Android版,一个只包含开源应用的开源应用市场。展示的数据通过网络请求来刷新。想象一下,如果我们旋转屏幕,我们的presenter是无状态的那么列表会重新加载。你如何解决?其实很简单,创建一个内存缓存或者磁盘缓存来存储之前获得的数据,并且设定好失效时间。但是永远不要把获取的结果存在presenter中,因为当你需要重新创建presenter的时候,你需要保持对那些数据的引用来避免再次请求数据。总之,我总是让presenter无状态。
回调地狱是人们谈论presentation层时经常讨论的话题。我看到大部分关于回调地狱的讨论都是因为我们让presenter做了太多的事情。记住协调domain的行为不是presentation的责任,我们的presentation层应该只是调用domain的行为,这些行为应该是异步处理的,让我们的presentation层尽可能简单。你可以查看我之前的文章modeling my domain layer.在你添加例如RxJava和Jdeferred这样的库之前,想想是你需要那些工具还是只有使用那些工具才能修补你设计上的错误。
为了说明这点我做了一个列子。想象一下如果从服务器获取的某个表示为true时,你需要展示一个列表。第一种处理主要有两个错误,第一是你的presenter不需要知道这个和domain层有关的标识。第二是,糟糕的组织协调导致我们需要些更多的代码来处理异步:
与其在presenter中处理,不如这样做:
如你所见,现在我们没有任何问题了,行为调用也很清晰。另外,如果这个标识不需要了,我只需要更改我的行为。
构建presentation层是很简单的,但是你需要确定什么属于这一层,哪些责任是domain层的。当你有一个庞大的presenter时,问问自己这个界面是不是有如此多的用户操作需要处理或者你在presenter中做了domain层的行为,等等......
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。