灵动的适配器模式 | Flutter 设计模式

2023-11-05 00:10

本文主要是介绍灵动的适配器模式 | Flutter 设计模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

设计模式系列的前两篇,分别向大家介绍了一种 创建性型模式 (单例模式) 和一种 行为型设计模式 (观察者模式),今天我们再来介绍一种结构型设计模式 —— 适配器模式。

适配器模式 (Adapter Design Pattern),顾名思义,这个模式就是用来做适配的,像一个「粘合剂」一样。

适配器模式可以将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类黏合在一起,最终使他们可以一起工作。

和 观察者模式 中的观察者与被观察者类似,适配器模式中担任主要角色是 适配器 (Adapter)被适配者 (Adaptee)。一个比较典型的例子是,插座转接头可以被认为是一种适配器,可以把本身不兼容的接口,通过转接变得可以一起工作。

适配器模式示意图,图源网络

在代码世界中,也有很多接口不适配的场景,如我们引入了一个第三方库后,发现它其中的类实现与我们现有代码并不兼容,需要一个 Adapter 类做一层转换才行。另外,相较于直接接触原始的代码实现,这种模式下,客户端仅仅依赖适配器类,对于代码复用和维护性也多了一层保障。

类适配器与对象适配器

适配器模式 UML 图

适配器模式有两种实现方式:类适配器对象适配器。其中,类适配器使用继承关系来实现,而对象适配器使用组合关系来实现。具体的代码实现如下所示。

/// 被适配者
class Adaptee {String concreteOperator() {return 'Adaptee';}
}abstract class ITarget {String operator();
}/// 对象适配器
class ObjectAdapter implements ITarget {var adaptee = Adaptee();String operator() {return adaptee.concreteOperator();}
}/// 类适配器
class ClassAdapter extends Adaptee {String operator() {return super.concreteOperator();}
}

ITarget 表示要转化成的接口,是一个规范化的接口定义。Dart 本身不支持关键词 interface,因此我们可以创建一个没有默认实现的抽象类代替。

需要被适配的 Adaptee 表示一组不兼容 ITarget 接口定义的类或接口,ObjectAdapterClassAdapter 两种适配器分别用不同的方式将 Adaptee 转化成了符合 ITarget 接口定义的接口。而在客户端使用时只需要依赖 ITarget 即可完成对 Adaptee 的适配。

class Client {Client(this.adapter);final ITarget adapter;operator() {var result = adapter.operator();assert(result == 'Adaptee');}
}

关于类适配器与对象适配器:

  • 如果希望你一个适配器可以同时适配多个不同的类,则单继承机制的 Dart 语言无法使用 类适配器 实现这种一对多的适配器。

  • 如果 Adaptee 接口很多,而且 AdapteeITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 可以充分将继承的代码复用作用利用起来。

  • 大部分场景下,我们推荐使用 对象适配器 的方式实现适配器模式,因为 继承 在很多情况下容易被 滥用 并造成 层级过多 的现象,而 组合 更加灵活。

实现

在代码应用中,适配器模式典型的例子是 Android 中的 ListView,在 Android 中,ListView 作为一个展示列表的 UI 组件,它的主要作用是将用户交给它的 Item View 以列表形式展示出来,然而描述 View 的形式却多种多样,可以是 Android 中 XML 布局,也可以是以 Java 代码中自定义 View 的形式提供,甚至可以是自定义的一套规则,实现自己的 UI 描述语言。

本身,XML 或者其他描述语言对于 ListView 是不可知的,所以,在 ListView 和它们之前介入一个适配器就可以有效的解决这个问题,适配器的作用就是将这些形式转换成 Item View 以适应 Listview。

在 Flutter 中,这种形式的模式很容易实现,例如,我们想自定义一个可以展示蔬果列表的组件 VeggieList

class VeggieList extends StatefulWidget {@override_VeggieListState createState() => _VeggieListState();
}class _VeggieListState extends State<VeggieList> {final List<Veggie> veggies = [];@overrideWidget build(BuildContext context) {return veggies.isEmpty? Text('无水果'): Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[for (var veggie in veggies)ListTile(title: Text('${veggie.name}')))],);}
}

这个组件主要关心的是 veggies,一个提供一组 Veggie 对象的数组。

veggies 数据源并不统一,可能来自多个不同的接口,可能在云端,也可能是本地的假数据,并且不同的接口提供的数据格式也可能不相同,可能是 xml 或者是 json。

/// 返回 json 数据格式的接口
class JsonVeggiesApi {final String _veggiesJson = '''{"veggies": [{"name": "apple (JSON)",},{"name": "banana (JSON)",},]}''';String getVeggiesJson() {return _veggiesJson;}
}/// 返回 xml 数据格式的接口
class XmlVeggiesApi {final String _contactsXml = '''<?xml version="1.0"?><veggies><veggie><name>apple (XML)</name></veggie><veggie><name>banana (XML)</name></veggie></veggies>''';String getVeggiesXml() {return _contactsXml;}
}

这些接口显然不能直接应用在 VeggieList 中展示,因此,需要做一些适配工作,适配的目的就是将这些数据转换成 Veggie 对象的数组,因此我们可以定义如下这个接口:

abstract class IVeggiesAdapter {List<Veggie> getVeggies();
}

其中的 getVeggies 方法返回的就是 VeggieList 组件需要的 Veggie 对象数组。

创建适配器时,只需要实现这个接口,然后组合目标需要被适配的类做接口转换即可,例如下面的 JsonnVeggiesAdapter,专门负责将 JsonVeggiesApi 转换为兼容 VeggieList 的适配器:

class JsonnVeggiesAdapter implements IVeggiesAdapter {final JsonVeggiesApi _api = JsonVeggiesApi();@overrideList<Veggie> getVeggies() {final veggiesJson = _api.getVeggiesJson();final veggiesList = _parseContactsJson(veggiesJson);return veggiesList;}List<Veggie> _parseContactsJson(String contactsJson) {final contactsMap = json.decode(contactsJson) as Map<String, dynamic>;final contactsJsonList = contactsMap['contacts'] as List;final contactsList = contactsJsonList.map((json) {final contactJson = json as Map<String, dynamic>;return Veggie(name: contactJson['name'] as String,);}).toList();return contactsList;}
}

最终,在使用到 VeggieList 时,注入 JsonnVeggiesAdapter 这个适配器就可以将原本不兼容的 JsonVeggiesApi 中的数据展示出来了:

class AdapterExample extends StatelessWidget {const AdapterExample();@overrideWidget build(BuildContext context) {return ScrollConfiguration(behavior: const ScrollBehavior(),child: SingleChildScrollView(child: VeggieList(adapter: JsonnVeggiesAdapter(),),),);}
}/// 最终的 VeggieList
class VeggieList extends StatefulWidget {final IVeggiesAdapter adapter;const VeggieList({@required this.adapter,});@override_VeggieListState createState() => _VeggieListState();
}class _VeggieListState extends State<VeggieList> {final List<Veggie> veggies = [];void _getVeggies() {setState(() {veggies.addAll(widget.adapter.getVeggies());});}@overrideWidget build(BuildContext context) {return veggies.isEmpty? Text('无水果',): Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[for (var veggie in veggies)ListTile(title: Text('${veggie.name}')))],);}
}

同理,不同接口来源的数据都可以通过适配器实现 IVeggiesAdapter 接口与 VeggieList 做兼容。

Flutter

在应用中,我们经常会使用到 CustomScrollView 创建拥有自定义滚动效果的组件,而 CustomScrollView 只允许包含 sliver 系列组件 (SliverAppBarSliverListSliverPersistentHeader 等) ,如果想包含普通的组件,必然需要使用 SliverToBoxAdapter


return MaterialApp(home: CustomScrollView(controller: scrollController,slivers: <Widget>[SliverAppBar(),SliverToBoxAdapter(child: Container(height: 100.0,),),],),
);

这里,将 Container 放入 SliverToBoxAdapter 中便可以在 CustomScrollView 展示出来了。

我们认为普通的 widget 是不兼容 CustomScrollView 的,SliverToBoxAdapter 在其中就扮演了适配器的角色。它使用 类适配器 的方式,将 SingleChildRenderObjectWidgetcreateRenderObject 接口重写转换成可以包含 RenderBox (对应一般 widget 的 RenderObject) 的 RenderSliver (对应 sliver 系列 widget 的 RenderObject),即这里的 RenderSliverToBoxAdapter

class SliverToBoxAdapter extends SingleChildRenderObjectWidget {/// Creates a sliver that contains a single box widget.const SliverToBoxAdapter({Key? key,Widget? child,}) : super(key: key, child: child);@overrideRenderSliverToBoxAdapter createRenderObject(BuildContext context) => RenderSliverToBoxAdapter();
}

拓展阅读

  • 适配器模式
    https://refactoringguru.cn/design-patterns/adapter

  • 组合优于继承
    https://time.geekbang.org/column/article/169593

  • Flutter Sliver
    https://juejin.cn/post/6844903901720739848

关于本系列文章

Flutter / Dart 设计模式从南到北 (简称 Flutter 设计模式) 系列内容由 CFUG 社区成员、《Flutter 开发之旅从南到北》作者、小米工程师杨加康撰写并发布在 Flutter 社区公众号和 flutter.cn 网站的社区教程栏目。

本系列内容旨在推进 Flutter / Dart 语言特性的普及,帮助开发者更高效地开发出高质量、可维护的 Flutter 应用。如果你对本文还有任何疑问或者文章的建议,欢迎向中文社区官方 GitHub 仓库 (cfug/flutter.cn) 提交 Issue 或者直接与我联系 (yangjiakay@gmail.com)。

这篇关于灵动的适配器模式 | Flutter 设计模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/346120

相关文章

鸿蒙开发搭建flutter适配的开发环境

《鸿蒙开发搭建flutter适配的开发环境》文章详细介绍了在Windows系统上如何创建和运行鸿蒙Flutter项目,包括使用flutterdoctor检测环境、创建项目、编译HAP包以及在真机上运... 目录环境搭建创建运行项目打包项目总结环境搭建1.安装 DevEco Studio NEXT IDE

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

模版方法模式template method

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法

【iOS】MVC模式

MVC模式 MVC模式MVC模式demo MVC模式 MVC模式全称为model(模型)view(视图)controller(控制器),他分为三个不同的层分别负责不同的职责。 View:该层用于存放视图,该层中我们可以对页面及控件进行布局。Model:模型一般都拥有很好的可复用性,在该层中,我们可以统一管理一些数据。Controlller:该层充当一个CPU的功能,即该应用程序

迭代器模式iterator

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/iterator 不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素

《x86汇编语言:从实模式到保护模式》视频来了

《x86汇编语言:从实模式到保护模式》视频来了 很多朋友留言,说我的专栏《x86汇编语言:从实模式到保护模式》写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节。 毕竟我不是作者,只有作者的解读才是最权威的。 当初我学习这本书的时候,只能靠自己摸索,网上搜不到什么好资源。 如果你正在学这本书或者汇编语言,那你有福气了。 本书作者李忠老师,以此书为蓝本,录制了全套视频。 试

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

springboot实战学习(1)(开发模式与环境)

目录 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 (3)前端 二、开发模式 一、实战学习的引言 (1)前后端的大致学习模块 (2)后端 Validation:做参数校验Mybatis:做数据库的操作Redis:做缓存Junit:单元测试项目部署:springboot项目部署相关的知识 (3)前端 Vite:Vue项目的脚手架Router:路由Pina:状态管理Eleme

状态模式state

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/state 在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。 在状态模式中,player.getState()获取的是player的当前状态,通常是一个实现了状态接口的对象。 onPlay()是状态模式中定义的一个方法,不同状态下(例如“正在播放”、“暂停