一只小安卓Clean架构的实践和他的年终总结

前言

从自学安卓到毕业🎓然后工作已经快一年多了吧

这一年平时基本忙这公司的东西

自己想写一些小项目,都是写了一点就废弃了

感觉完整的项目对个人的提升不大,因为大部分时间都在造轮子

总的来说,这一年还是做了一点微小的工作

My contributions this year
My contributions this year

有所成长,但是也知道自己欠缺的东西越来越多

前后待了两个公司,都是小规模的创业公司

至今都是1个人(或者2个人)开发项目,感觉一直都在很低效很低效地工作

感觉android开发真是艰难啊,相比iOS

陆陆续续看过也练手过,MVP,MVVM,MVPVM之类的东西

当时并不能切身地感觉到它们的好处,现在想想真是图样图森破

甚至以前我都不怎么喜欢用Fragment,因为发现Fragment很难管理

现在我开始在项目里面大量的使用Fragment,因为多数情况下它们可以复用,可以节省很多时间

一个人开发嘛,心里的苦只有自己知道😞
其实是所有的锅只能一个人背(哈哈

也开始疯狂封装一些基类来复用

比如一个显示列表的Fragment基本就是传入一个ViewModel和一个Item的布局文件就可以了

即便如此,虽然复用了大量代码,但是限制也越来越多

每次产品经理一开口,我都感觉自己的代码是一摊🐶屎

可能我能通过很好的封装来解决ActivityFragment过重的问题,但是项目还是很难维护

尤其是当需求一直改变,新功能不断迭代,项目的历史负担越来越大

KPI压力倒是没用…感觉我们的用户都是iOS人群👅

每隔一段时间看自己的代码,我都感觉是在屎山里面翻滚...

所以V/P分离貌似成为了一种必需

尽管各种架构前期总有一些额外的工作,但是之后的开发会变得很轻松

在app体积不断增大的时候,让项目细分下来的codebase尽量小

又一个前言

最近了解并尝试了一下clean架构,发现它真的很给力

好后悔自己没有早点熟悉它并投入使用.


Clean架构可以使你的代码有如下特性:

  • 独立于架构
  • 易于测试
  • 独立于UI
  • 独立于数据库
  • 独立于任何外部类库

如果你还不了解Clean架构,肯定要先去看下这个:

Uncle BobThe Clean Architecture

这里只是想讲讲自己关于Clean架构的一点实践,

有问题大家一起探讨下...

开始

试着写了一个demo:Vincent

虽然只写了一点点(然后废弃了= =),但是Clean架构的样子感觉有了(我是这么觉得哈哈

初衷是写一个tubmlr样子的weibo, 写着写着发现渣浪对第三放开发者的限制太多了.

发现授个权还要认证,还要申请一个蓝V的微博…我感觉好难受

首先这张图你肯定见过...

其实更直观的,我们直接就着代码来看3个module之间的依赖:

  • data(数据层
apply plugin: 'java'
def cfg = rootProject.ext;
dependencies {
    // ReactiveX
    compile cfg.dependencies["rxjava"]
    // Square
    compile cfg.dependencies["retrofit"]
    compile cfg.dependencies["converter-gson"]
    compile cfg.dependencies["adapter-rxjava"]
    compile cfg.dependencies["logging-interceptor"]
    compile cfg.dependencies["dagger"]
}
  • usecase(用例层 当然你叫domain也行
apply plugin: 'java'
dependencies {
    compile project(':data')
}

datausecase层都应该是java代码,跟Android Framework无关

  • app(就是我们平时写的那个app
  dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        // skip
        compile project(':usecase')
  }

然后你们感受一下:

  • 用例层从数据层获取数据
  • 在用例里面进行处理业务逻辑(虽然我发现一般的项目根本没有什么业务逻辑可言
  • 视图层从用例层获取数据然后显示在界面上

Standing

然后讲一下各种乱七八糟的东西

data层

entities:

实体类(最原始的数据,不该随着业务逻辑而改变

wrapper:

对实体类的一层封装,在这里你可能要自己加一些东西:比如:

  • 处理timestamp,转换输出本地时间
  • 一些额外的参数,比如数据边界的验证等等

当然这些你也可以放在View层的ViewModel里面
但是感觉放这里比较清晰

remote:

顾名思义嘛,这就是RemoteDataSource,在这里配置和封装了(对于我来说)OkHttpRetrofit的接口

utils:

  • 自定义了一些transformer,比如切换线程和全局的错误处理
  • 网络请求的Interceptor
  • 其他乱七八糟的

当时这样写了我就感觉特别酷炫(说出来别笑我...

因为可以在这里直接用一个java类来测试api,而不是编译整个项目...

不对不对,是API...有一次我们的python过来说你Api这个类应该全大写...

每次叫我们python过来看logcat的时候 = = 他都会在旁边发牢骚: 安卓编译怎么会这么慢....
卧槽我也不想啊

当然你会说用Postman不就好了...

但是这样能有一个配置了OkHttpRetrofit的环境嘛...


usecase层

用例层就比较简单了...因为一般的应用都没有什么业务逻辑

举个🌰,我要从一个从server端获取微博(status)显示到timeline列表上

首先我有一个Usecase的接口要继承(<T>是网络请求返回的数据类型

public abstract class Usecase<T> {
    protected abstract Observable<T> buildObservable();
}

具体代码:

public class GetTimelineUsecase extends Usecase<Timeline> {
    private final RemoteRepository mRepository;
    private final SchedulerTransformer<Timeline> mSchedulerTransformer;
    private int page = 1;
    @Named("uid")
    private long uid;
    @Named("timeline_type")
    private String timeline_type;

    @Inject
    GetTimelineUsecase(RemoteRepository repository,
                       @Named("io_main") SchedulerTransformer schedulerTransformer,
                       @Named("timeline_type") String timeline_type,
                       @Named("uid") long uid) {
        mRepository = repository;
        mSchedulerTransformer = schedulerTransformer;
        this.uid = uid;
        this.timeline_type = timeline_type;
    }
}

在构造函数里面我们注入了:

  • 根据页面逻辑相关的参数(timeline_type,uid)

  • Android Framework相关的东西(比如AndroidSchedulers.mainThread()

    因为这一层是apply plugin: 'java'嘛~

然后我需要有:

  • RemoteRepository(源,返回数据

  • SchedulerTransformer(切换线程,比如IO和UI线程之间

    这里我直接注入一个transformer...当然你也可以注入不同的thread

  • timeline_type,uid(接口的参数

    针对返回相同数据类型的接口 我们可以封装在一个用例里面

我在buildObservable()中根据timeline_typeuid请求不同的接口:

   @Override
    protected Observable<Timeline> buildObservable() {
        switch (timeline_type) {
            case "home_timeline":
                return mRepository.home_timeline(page, feature)
                        .compose(mSchedulerTransformer);
            case "friends_timeline":
                skip;
            default:
                return Observable.error(new Throwable("Timeline type can't be null!"));
        }

然后在GetTimelineUsecase中会有一个excute()方法,我在这里处理业务逻辑:

public Observable<Timeline> excute(final boolean refresh) {
    return buildObservable()
            .doOnSubscribe(new Action0() {
                @Override
                public void call() {
                    if (refresh) {
                        page = 1;
                    }
                }
            })
            .doOnNext(new Action1<Timeline>() {
                @Override
                public void call(Timeline timeline) {
                    if (0 != timeline.getNextCursor()) {
                        ++page;
                    }
                }
            });
}

然后你会问业务逻辑在哪里(黑人问号???

我就说嘛,一般的项目没什么业务逻辑....

这里不是有一点点处理分页的逻辑(就这么多了

所以View层(或者说是你的Presenter)不用再关心分页,不用有什么page或者cursor

只要在那边一直调用excute()就好了,想到一个View层的设计原则

让View尽可能的笨拙和被动

虽然会传过来一个refreshBoolean值哈,但是可以认为这只是用户操作(页面逻辑),和业务逻辑无关.

PS:

在这里还自定义了一个注解

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD})
public @interface Nullable {
}

因为这个module没有support-annotation可以用

Dagger会要求如果你注入的参数是null,就必须用@Nullable注解,不然就会报错.

在一些地方我需要用到它,来传入一些默认值(比如null和0,因为可能这个请求参数用不到

另外,在注入String Long等类型的参数的时候,需要用@Name注解,不然编译器肯定不知道你需要的是什么


View层

View层其实没什么好讲的,就是Databinding+Dagger+MVP

Dagger:

喜欢用Dagger的原因是因为它能迫使我理清所有的依赖关系

至于注入本身感觉也没有那么方便,毕竟多了很多前期工作...

关于Dagger的使用,也是看了很多文章才拎清楚的

推荐一下frogermcs的博客~

MVP:

基本上是照着Google的android-architecture写出来的

有很多分析的不错的文章,我也是看了好久才搞清楚的

比如CameloeAnthony在简书上的那个系列~

这里就不展开了…

EventBus:

项目里的事件总线用的是Eventbus.

不过渐渐发现代码量一上去,各种event就会使逻辑变得很混乱

不知道Eventbus的正确打开方式到底是什么?

现在我反正是尽量少用...

DataBinding

其实用了之后感觉没有一开始那么酷炫啦

双向绑定什么的,实际使用场景很少...

而且Clean结构决定了View和真正的Model只会是单向的绑定

但是如果你还没用过DataBinding,赶紧尝试一下吧

其实Google的文档写的不是很全面,写的最好的感觉还是大帅的博客

ViewModel:

既然是单向绑定,ViewModel扮演的角色就只是

  • 暴露出需要的properties
  • 让View实现它的接口

当然关于ViewModel的写法,讲道理应该是对data层的model的一次映射,

但是感觉这样要用手打好多代码...如果AS有插件(类似GsonFormat之类的)我可能会这样写...

所以一般我们直接extends或者wrap某个model就好了…再调用getter方法获得properties

Tips

其实用ViewModel的一个好处是可以很方便的封装一些方法.

比如图片加载库,你可能会觉得图片加载库还要封装,不是只要一句话:

load(url).into(imageView);

对啊,但是可能你因为Glide的方法数太多,要换成Picasso,这下不就麻烦了

但是当你把所有图片加载的场景(一般的...)都写在@BindingAdapter里面

举个🌰:

    @BindingAdapter("avatar")
    public static void loadAvatar(ImageView view, String url) {
        Glide.with(view.getContext()).load(url).into(view);
    }

    @BindingAdapter("photo")
    public static void loadPhoto(ImageView view, String uri) {
        Glide.with(view.getContext()).load(Uri.parse(uri)).into(view);
    }

这样到时候全局意义上的替换一个库就很方便了

Rxjava

Subscriber来代替Callback,在Presenter里面就全部都是从上往下的Rx的stream

举个🌰,一个列表加载数据:

    public void loadMore() {
        if (dataInTransit || reachBottom) return;
        mSubscriptions.add(mUsecase.execute(false) // boolean: needRefresh
                .doOnSubscribe(() -> dataInTransit = true)
                .doOnTerminate(() -> dataInTransit = false)
                .doOnSubscribe(mView::startLoading)
                .doOnTerminate(mView::stopLoading)
                .doOnError(mView::showError)
                .subscribe(timeline -> {
                    if (timeline.getNextCursor() == 0) {
                        reachBottom = true;
                        mView.showNoMore();
                    } else {
                        mView.setData(timeline.getStatuses(), false);
                    }
                }));
    }

写起来特别舒服~

关于RxJava,响应式编程什么的已经不是一个新鲜玩意儿,在这里就不赘述了

安利一下,感觉Awesome-RxJava列出的一些资料都非常不错~

最后

其他还有一些东西...之后再补上了

总之,Clean架构是个蛮有意思然后很实用的东西

大家有时间可以了解一下~

最后,Everybody 新年快乐🎉🎉🎉 新的一年里都能成为更好的自己~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容