Android 干净架构的应用:DTV实战

A DTV demo using clean architecture on android

Posted by Nathan on December 13, 2019

前言


最近项目内需要重构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

​ 中文翻译:张天雷

The Clean Code Blog

按照惯例,还是介绍下经典架构图:

CleanArchitecture

  • 洋葱的结构,最外层是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模式,使用最佳

业务区分后,我画了一个流程图,大致就明白内容了。

DTV-flow

  • 图中黑色线主要是调用逻辑,蓝色是数据回调,红色是资源管理释放

代码实战

看下代码结构图:

code-all

  • 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,根据不同场景,可以加上线程切换

code-play

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: 传送门