作者:麦客奥德彪
链接:
https://juejin.cn/post/7348464590159773748
本文由作者授权发布。
列表类产品现在非常多,可以说是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 抽象工厂模式配置化图层的创建与管理
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 在目标页面进行视图绑定
private fun testArchitecture() {
val videoItems = mutableListOf()
testData(videoItems)
// 使用前根据配置指定图层
// VideoViewFactory.setVideoViewFactory(MyVideoViewFactory())
val videoSceneView = VideoSceneView(this)
.apply {
videoPageView.setLifeCycle(lifecycle)
videoPageView.setItems(videoItems)
}
setContentView(videoSceneView)
}
这种设计方案的优势在于提供了更灵活、可扩展的方式来管理和组织视频播放页面的各个组件,从而降低了代码的复杂度,提高了代码的可维护性和可读性。以下是对该设计方案的总结:- 模块化和组件化: 通过将视频播放页面的功能拆分成不同的组件,实现了模块化和组件化。每个组件都具有清晰的职责和功能,便于单独开发、测试和维护。
- 解耦和灵活性: 使用图层和宿主的设计模式,实现了各个组件之间的解耦,使得它们可以独立存在并且相互影响。同时,通过抽象工厂模式配置化图层的创建与管理,进一步提高了灵活性,使得可以根据需要动态的配置和切换不同的图层。
- 单一职责原则: 每个组件都遵循单一职责原则,只负责特定的功能和逻辑,使得代码结构清晰,易于理解和修改。
- 可扩展性: 通过定义清晰的接口和抽象类,使得可以很容易地扩展和添加新的功能和组件,而不会影响到已有的代码逻辑。
- 配置化: 通过抽象工厂模式,将图层的创建和管理配置化,使得可以根据需要动态的配置和切换不同的图层,从而满足不同场景下的需求。
VEVodDemo-android 火山视频UI解耦部分
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!