前言
最近项目内需要重构DTV的UI,有些同事对DTV的业务不是很熟,所以为了避免把DTV的业务逻辑与UI杂糅在一块,达到快速开发UI的效果,在Uncle Bob大叔的Clean架构的启发下,把DTV业务逻辑重新封装,个人觉得,对于业务比较多场景,Clean架构确实能起到降低耦合,易测试等功效。
在DTV业务场景中,由于涉及到硬件资源,需要对资源进行统一管理,避免占用内存,并且由于业务逻辑比较多,而且相对复杂,对于新手来说,确实入门相对有难度,搜索,播放,时移,录制,EPG,以及节目管理,喜爱管理等,业务既相互独立,又有内置联系。
本文就是针对此场景,把DTV的业务借助Clean架构,隐藏了资源管理和业务逻辑,对外统一接口调用,经过试验,大大提高应用开发速度。下面就主要介绍下结构以及相应的实战示例。
Clean简要介绍
关于Clean架构介绍,网上已经有好多文章值得参考,这里列举下我参考的文档:
参考网站:A detailed guide on developing Android apps using the Clean Architecture pattern
中文翻译:张天雷
按照惯例,还是介绍下经典架构图:
- 洋葱的结构,最外层是UI,DB等于Android相关的,所以会调用android相关的api
- 内层presenters等主要是接口适配层,封装了主要的业务接口供外层调用,主要是用于接收外层发过来的指令,并根据下一层的结果来更新UI的功能。
- Use Cases层主要是逻辑层,把presenter层的调用指令进一步分发,同时把Entities层的数据回调给上层,另外这一层还会涉及到线程切换功能。
- Entities层,这层主要是实体逻辑层,封装了最外层DB,Web等数据接口,同样也会直接调用android相关的API
- 数据调用流方向:UI->Presenters->Use Cases->Entities
- 数据回调方向:Entities->Use Cases->Presenters->UI
- 至于右下角的数据流含义,在接下来的代码中会详细解释
DTV+Clean
由于DTV场景比较多,根据Clean结构图,我大致把业务做如下处理
- 统一DTV资源管理部分,主要是监听Android UI的生命周期,无须开发者手动加载和释放资源,这里主要利用谷歌的AndroidX工具包中jetpack神器Lifecycle,具体用法不做过多介绍,代码示例用会有
- 节目搜索,包括DVB-C/DVB-T/DVB-S,根据不同业务做不同的参数区分
- 节目播放:主要是切台,选台等。
- 节目时移:包括启动,退出,快进快退等功能
- 节目管理:节目数量,喜爱,加锁,删除等功能的统一入口
- EPG:显示节目详情,包括7天EPG等
- PVR:录制节目的管理,播放等功能。
- 结合MVP模式,使用最佳
业务区分后,我画了一个流程图,大致就明白内容了。
- 图中黑色线主要是调用逻辑,蓝色是数据回调,红色是资源管理释放
代码实战
看下代码结构图:
- presentation对应洋葱结构的第二层presenter层,对外给UI提供相关接口和消息回调
- domain对应第三层的Use Cases和 Entities层,
- storage主要是外层的DB部分,
- dvb是调用DTV具体资源逻辑部分,与硬件相关了。
DTV中的presenter层
IDVBPlayPresenter.java
package com.nathan.arch.presentation.presenters;
import com.nathan.arch.domain.model.ChannelUnitModel;
import com.nathan.arch.domain.model.DvbPlayerStatus;
import com.nathan.arch.domain.model.EpgInfoDModel;
import com.nathan.arch.domain.model.PlayChannelInfoDModel;
import com.nathan.arch.domain.model.TipMessage;
import com.nathan.arch.domain.model.TunerInfoDModel;
import com.nathan.arch.presentation.presenters.base.IDVBBasePresenter;
import com.nathan.arch.presentation.ui.IDVBBaseCallback;
public interface IDVBPlayPresenter extends IDVBBasePresenter {
void attach(Callback callback);
interface Callback extends IDVBBaseCallback {
void showAllChannels(List<ChannelUnitModel> channelUnitModelList);
}
void playChannelByNum(int num);
void getALLChannels();
}
首先看下播放部分:
import部分只依赖domain层,纯java,易于测试,对UI层一无所知,高内聚
接口IDVBPlayPresenter
定义了两个功能,playChannelByNum
用于播放节目,getALLChannels
用于节目获取,UI可以主动调用这两个方法,另外注意返回值都是void,也就是异步模式,
而Callback用于数据回传,用于通知UI。
现在就能解释架构图中右下角的含义了,IDVBPlayPresenter
用于数据调用,属于控制流,而Callback用于数据回调,属于更新流,简而言之,一个是入口,一个是出口。
DTV中的domain层
如下图中所示:
根据presenter层的接口,进一步分解相应的功能,减轻presenter的压力
可以看到我这里把播放的接口分了好几个interactor,根据不同场景,可以加上线程切换
DTV中的Entities层
PlayRepositoryImpl.java
package com.nathan.arch.storage;
import com.nathan.arch.domain.model.ChannelUnitModel;
import com.nathan.arch.domain.repository.PlayRepository;
import com.nathan.arch.storage.dvb.ICallbackPlayMethod;
import com.nathan.arch.storage.dvb.impl.PlayMethodFromDVB;
import com.nathan.arch.storage.tools.EmptyTool;
import java.util.List;
import timber.log.Timber;
public class PlayRepositoryImpl implements PlayRepository , ICallbackPlayMethod.callback {
/**
* set this class to Singleton
*/
private static PlayRepositoryImpl instance = null;
public static PlayRepositoryImpl getInstance(){
if (EmptyTool.isEmpty(instance)){
instance = new PlayRepositoryImpl();
}
return instance;
}
这一层我把它设置成单例模式,用于在多个activity或者fragment中进行资源和数据共享,
同样,这层既依赖domain层,同时又可以调用DTV具体硬件资源,并且也可以调用Android的相关API。
然后把这层中获取的数据,通过callback出口返回去,通知UI进行处理。
DTV的demo用法
package com.nathan.dtvclean;
import com.nathan.arch.presentation.presenters.IDVBPlayPresenter;
import com.nathan.arch.presentation.presenters.impl.IDVBPlayPresenterImpl;
public class MainActivity extends AppCompatActivity implements IDVBPlayPresenter.EventCallback{//监听
private IDVBPlayPresenter playPresenter;//声明需要使用的接口
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
playPresenter = new IDVBPlayPresenterImpl();//接口初始化
playPresenter.attach(this);//MVP模式用于回调监听
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
playPresenter.playChannelByNum(123);//接口调用
}
});
}
@Override
public void showChannelPlayFinish() {//监听回调
Toast.makeText(this,"Play finish",Toast.LENGTH_LONG).show();
}
}
总结
- 说下clean的优缺点:
- 优点:架构清晰,解耦明显,易于测试
- 缺点:代码量大大提高,大多数用的是面向接口编程,小项目上用的话有些臃肿
- 后续会考虑加入dagger2,针对MVP模式进行进一步封装解耦。
- demo中加入了一些个人积累的API:
- 替换java枚举为静态枚举类,避免内存占用
- 读取USB的反射方法,用于处理PVR
-
java判空的工具类
EmptyTool.java
等 - 本文demo: 传送门