列表类产品现在非常多,可以说是10个APP中9个是有列表功能的,今天要说的是视频、直播类切换类型的负责业务解耦。具体业务场景可以用抖音短视频为例,只讨论其实现方式。
这种类型的产品一般实现方式有两种。
一、常见此种逻辑的代码实现
1.1 使用ViewPager2 + Fragment
优点:
- 模块化: 每个功能都在独立的 Fragment 中完成,使得代码更易于维护和管理。
- 复用性: 可以轻松地在不同的页面中重复使用 Fragment,避免了代码的重复编写。
- 灵活性: Fragment 提供了更多的生命周期方法和回调,可以更精细地控制页面的行为和状态。
- 易于管理状态: 每个 Fragment 都有自己的生命周期,可以方便地管理页面状态和数据加载。
缺点:
- 内存消耗: 每个 Fragment 都有自己的视图层次结构和生命周期,可能会占用较多的内存,尤其是在包含大量视频或图片的页面。
- 性能问题: 在 ViewPager2 中使用 Fragment 可能会存在性能问题,特别是在加载大量页面时,会影响滑动的流畅性。
1.2 使用ViewPager2 + 自定义View
优点:
- 轻量级: 自定义 View 可以更轻量地处理页面内容,避免了 Fragment 的复杂性。
- 性能优化: 自定义 View 可以更灵活地优化绘制和布局,提高了页面的渲染效率。
- 更小的内存消耗: 自定义 View 可以更精简地管理页面状态,减少了内存占用。
- 更灵活的布局: 自定义 View 提供了更灵活的布局方式,可以更容易地实现各种复杂的界面效果。
缺点:
- 复杂性增加: 自定义 View 的开发相对于 Fragment 更复杂,需要更多的自定义绘制和布局代码。
- 可维护性降低: 自定义 View 可能会导致代码结构不够清晰,降低了代码的可读性和可维护性。
- 复用性降低: 自定义 View 不像 Fragment 那样容易进行模块化和重用,可能会导致代码冗余和重复编写。
曾经我在直播类产品中使用过第一种方式实现直播间的切换,效果还行,但是也确实遇到了很多坑,不仅是上述的几个缺点,总之太多笨重,如果有实现这种需求的用第一种方式要三思啊。
二、今天要讨论的是这样一个问题
不管使用那种方式,面对如此庞大的业务,每一个Item 的代码应该怎么写? RecyclerView 这个最常用的组件,作者将其写到了一个文件中,有人说聚合性高,写法很牛逼,但是换个人,如果是我写的,他们还会这么说吗?毕竟代码还需要给别人看。比如常规的写法我们应该是这样的
- Activity/Fragment -> 数据填充、属性、触发渲染
- ViewPager2 + CustomView -> 常规列表功能
- Custom View中 播放视频、展示图片、用户信息、视频信息、手势处理等等
呈现出来的应该是这样:

当然这是是我们看见的,还有看不见的快进/退、放大、缩小、图文类型、直播类型等等
那这种写法有什么问题呢?
- 代码复杂度增加:一个类中包含大量的业务逻辑和功能会使得代码变得庞大而复杂,不易于维护和阅读。
- 单一职责原则违反:单一职责原则是面向对象编程中的基本原则之一,一个类应该只负责一种类型的职责。将多个不同类型的功能聚合在一个类中,违反了这个原则,会导致代码结构混乱,难以理解和修改。
- 难以测试和调试:功能过于集中的类会导致测试和调试变得困难,因为它们会有过多的依赖和交互。
- 耦合度增加:各个功能之间可能会产生较大的耦合,导致代码的灵活性和可维护性降低。
- 可扩展性受限:如果后续需要添加新的功能或修改现有功能,可能会涉及到整个类的修改,增加了开发的风险和成本。
三、那我们希望的是什么呢?
当然就是解决上面的这几个问题
3.1 梳理一个独立使用的根布局
想法是这样的,为了方便任意位置的使用,我们直接自定义一个View,这个View的职责:主要管理一级视频功能,即视频列表的根布局
- 列表刷新、加载更多等数据源处理,需要考虑数据的来源
- 列表,即ViewPager2 或者可滑动容器的添加
- 全局的动画等(比如加载数据时的动画)
3.2 对滑动容器进行独立,让上述的独立布局成为一个可以添加任意具备上述功能的容器
为了让这个可滑动的容器,职责单一,我们在此只提供以下功能
- 滑动列表初始化
- 列表适配器创建及绑定
此时我们就具备了两个层级的可滑动的列表
3.3 业务在Adapter中创建,准确来说是添加
这就是一个正常的Adapter,在onCreateViewHolder中进行Item View 的创建,但是如果单纯的创建,不就是上述的问题出现吗,所以此处需要对ItemView 进行解耦
3.4 解耦ItemView 的负责逻辑(重点在这里)

看图的话是不是很简单,不过这种场景中不好处理的问题在于,多个业务可能有互相使用的场景
- 所有试图都应该知道有没有链接到对应的ItemView 中
- 所有视图业务可能都需要触摸事件 比如双击、缩放等
- 部分试图可能需要感知播放器的状态以做出自己对应的策略
- 所有视图业务都需要知道滑动切换后的数据及动作
- 所有的试图对于手势事件的特定场景应该保持一致动作(比如隐藏除视频播放器外的所有视图)等
如果单纯的使用类文件将这些解耦会出现很多问题,比如上述的问题。 怎么做到上述的这几点呢?怎么优雅的创建这些组件呢,最起码要做到
- 组件添加、移除流程必须清晰
- 组件可以感知播放器的各种状态或者订阅状态
- 各个组件的操作应该是层级间可以互相影响调用的
- 组件间的操作应该可以影响视频播放器器的
- 组件的添加应该是可配置的(比如某些场景下不用展示用户信息)
可以分层设计,为了能满足上述的功能

类图关系大概如下

可以描述为:
VideoLayer
类表示视频图层,它可以通过bindLayerHost()
方法绑定到VideoLayerHost
,通过unbindLayerHost()
方法解除绑定。VideoLayer
可以与VideoView
关联,它的具体功能需要在子类中实现。VideoLayerHost
类表示视频图层的宿主,可以包含多个VideoLayer
。它可以与VideoView
关联,通过attachToVideoView()
方法将自身附加到VideoView
上。同时,它可以添加和移除VideoLayerHostListener
监听器,用于监听宿主与VideoView
的关联状态。VideoView
类表示视频视图,它可以包含一个VideoLayerHost
作为其宿主。可以通过bindLayerHost()
方法将VideoLayerHost
绑定到当前的VideoView
上。
这样一来,可以通过VideoLayerHost来管理VideoLayer 的添加删除,并通过它联系起来这个视图层的运作,在使用过程中,可以用以下流程描述添加一个ItemView 的流程及其原理

3.5 抽象工厂模式配置化图层的创建与管理

在任意使用位置可进行自定义抽象工厂来改变图层
kotlin
class MyVideoViewFactory : VideoViewFactory {
override fun createVideoView(context: Context): VideoView {
val videoView = VideoView(context)
val layerHost = VideoLayerHost(context)
val videoInfoLayer = VideoInfoLayer()
layerHost.addLayer(videoInfoLayer)
layerHost.attachToVideoView(videoView)
videoView.setBackgroundColor(ContextCompat.getColor(context, R.color.default_bg))
return videoView
}
}
四、使用流程 部分代码
4.1 在目标页面进行视图绑定
kotlin
private fun testArchitecture() {
val videoItems = mutableListOf<VideoPageItem>()
testData(videoItems)
// 使用前根据配置指定图层
// VideoViewFactory.setVideoViewFactory(MyVideoViewFactory())
val videoSceneView = VideoSceneView(this)
.apply {
videoPageView.setLifeCycle(lifecycle)
videoPageView.setItems(videoItems)
}
setContentView(videoSceneView)
}
五、总结
这种设计方案的优势在于提供了更灵活、可扩展的方式来管理和组织视频播放页面的各个组件,从而降低了代码的复杂度,提高了代码的可维护性和可读性。以下是对该设计方案的总结:
- 模块化和组件化: 通过将视频播放页面的功能拆分成不同的组件,实现了模块化和组件化。每个组件都具有清晰的职责和功能,便于单独开发、测试和维护。
- 解耦和灵活性: 使用图层和宿主的设计模式,实现了各个组件之间的解耦,使得它们可以独立存在并且相互影响。同时,通过抽象工厂模式配置化图层的创建与管理,进一步提高了灵活性,使得可以根据需要动态地配置和切换不同的图层。
- 单一职责原则: 每个组件都遵循单一职责原则,只负责特定的功能和逻辑,使得代码结构清晰,易于理解和修改。
- 可扩展性: 通过定义清晰的接口和抽象类,使得可以很容易地扩展和添加新的功能和组件,而不会影响到已有的代码逻辑。
- 配置化: 通过抽象工厂模式,将图层的创建和管理配置化,使得可以根据需要动态地配置和切换不同的图层,从而满足不同场景下的需求。
参考: VEVodDemo-android 火山视频UI解耦部分