完整的Android MVP开发之旅
开发背景
最近是在做一个与健身相关的APP,里面有训练器模块基本功能是按照特点动作的演示和描述来引导用户完成训练。在第一个版本时由于没接触过些类项目与功能花了几周的时间大概1500行代码才完成这个功能,
当时虽然我已经尽量让代码表现的清晰,但是可以想像到当一个Activity中包含这么多代码是什么感觉。自己维护起来都难受。
先谈设计
有了前一次设计经验此次开发使用MVP、模块化、面向接口等概念,将整个训练器分为控制器、数据模型、音频、视图、可训练对象五个模块分别用以下接口表示:
- ITrainerController
- ITrainerModel
- IAudioFlow
- ITrainerView
- ITrainable
去掉一些抽象类后接口图如下:
设计以上接口后引入MVP概念使用ITrainerController
做为Presenter,ITrainerModel做Model,ITrainerView做View下面介绍主要模块。
控制器
可以用在MVC和MVP中这取决于用哪种开发模式,在我开发的项目控制器用来控制训练器的运行管理训练器的生命周期如训练、暂停、休息、完成等状态协调ITrainerModel、ITrainerView、IAudioFlow等各个模块。
在使用过程中控制器并不止一个这也是抽象出一个接口的原因,ITrainerController接口继承IPresenter接口使其能做为Presenter使用。
数据模型
数据模型中包含大量的ITrainable对象,对内组织数据对外提供数据支持。对数据的组织方式主要分两种:
- 从本地数据库
- 从网络获取
在训练器中可能是正常的训练或是一次训练测试而训练数据和测试数据又有一些差异但它们的数据都被当做ITrainable,测试数据是不需要保存的只需要从服务器拉取后按要求完成就行而训练是会产生本地记录的。
针对不同数据组织方式提供不同的数据模型这是有必要的。
音频
音频比较多样化像训练过程中包含动作名、时间、单位词、提醒等音频这些音频都是分开的不同的音频文件。Android主要有两种实现方式:
- SoundPool
- MediaPlayer
首先说SoundPool优点自然就是免去了加载、管理音频等过程但是它并不适应我们的训练器,主要原因是缺少准备、完成后的一些回调而在训练器运行过程中这些过程必不可少比如在播放完一段预备开始后音频这时我们才能进行正式的训练。
最后是采用MediaPlayer,但是在使用过程又要考虑到音频的集中管理与资源的释放免不了多封装一次。设计时我将全部音频逻辑放在Android Service中Activity通过bind AudioService来使用音频,将音频逻辑放入AudioService这样可以音频完
全独立起来使其能在后台播放并且也可以提高进程优先级。
在设计中AudioService仅仅播放与管理音频和资源并不具备音频播放的逻辑功能。由于不同的训练方式音频的播放逻辑也有不同之处所以在此设计IAudioFlow接口来负责音频逻辑。
训练视图
Android常用Activity作为视图,通过实现ITrainerView接口来完成训练视图的显示。视图中不包含任何业务逻辑代码。
再谈实现
说到实现其实这并不是最需要关注的内容,因为上面提供了很全面的接口而我们的模块又是使用的接口所以不管如何实现那些功能并不会对各个模块之间产生大的影响除非功能实现与实际要求相差太多。这里我只详细说一下音频模块的实现。
音频实现
音频模块又可分为音频管理与音频业务逻辑。音频管理就是加载、播放、回收资源等功能,音频业务逻辑主要处理在正确的状态下应该播放什么样的音频。将整个音频管理模块放在Android Service中与业务逻辑完全分离。音频模块涉及以下类与接口:
- AudioService: 音频服务器继承Android Service
- IAudioService: 音频抽象接口包含播放、暂停等事件
- MediaPlayerHolder: 持有MediaPlayer管理MediaPlayer生命同期
- IAudioFlow: 为不同的训练内容提供音频逻辑
- AudioServiceImpl: 实现IAudioService
基本使用流程是首先通过绑定AudioService的onBind方法返回IAudioService的实现类供IAudioFlow使用,IAudioFlow持有IAudioService实现后加载训练音频然后供ITrainerController使用。在AudioServiceImpl中会维持一个音频优化级队列,
上面提到因为音频都是不在一个文件中的所有需要在使用时将它们连接起来形成一段音频。通过优先级队列结合MediaPlayer播放完成时回调可以将多个音频组合在一起形成需要的音频。由于音频的播放越来越多MediaPlayer的回收利用特别重
要在AudioServiceImpl同样也具备MediaPlayer的回收与利用功能。这个功能实现是通过MediaPlayerHolder来处理的,通过MediaPlayerHolder的静态get方法获取MediaPlayerHolder如果回收池中有空闲的MediaPlayerHolder则拿来用没有时则
新建一个,同样也在一个音频播放完成后调用MediaPlayerHolder的recycle来进行回收利用。
模块整合
为减少依赖模块之间的整合需提供管理或帮助类,新建TrainerHelper来创建模块实现类其中包含一个Mode枚举来列举训练模式。
public class TrainerHelper {
public enum Mode{TEST, TRAINING, EXAM}
private static Mode mode;
public static void setMode(Mode m){
mode = m;
}
public static ITrainerController createPresenter(ITrainerView view, Bundle createArgs){
return new TrainerPresenter(view,createArgs);
}
public static ITrainerModel createTrainerModel(ITrainerController controller){
return = new DefaultTrainerModel(bundle);;
}
public static IAudioFlow createTrainerAudioFlow(ITrainerController controller){
return new DefaultAudioFlow(controller);
}
}
总结
成功的设计与架构能减少大量的工作时间,利用接口可让开发人员更加注重功能上的实现同时隔离各个模块之间的依赖。下次产品经理再改需求或再整出个训练模式时咱也能从容应对。由于本人水平有限如有错误烦请指正我会在第一时间做出更改。
《架构文摘》每天一篇架构领域重磅好文,涉及一线互联网公司应用架构(高可用、高性 能、高稳定)、大数据、机器学习等各个热门领域。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端