对 Android 开发的一点思考

17 年毕业开始工作到现在已快两个年头,在实际项目开发的过程中,我对 Android 开发有了一些自己的思考。本着碰撞才会有火花、讨论才会进步的理念,我把对 Android 开发的一点思考分享出来,真诚的希望可以有不同的观点,在纠结反驳之中得到最优解,共同进步。

最初的时候,你是否是一个完美主义者,不容忍任何一点 warning 与叹号,if 必有 else,switch 必有 default,即使 else 和 default 中确实什么也不用处理,你也会添加一个 //do nothing 注释,表示这里的逻辑是经过充分考虑的,下次阅读程序时,告诉别人也告诉自己,这里的确什么也不用处理,可以快速跳过。

我想大多数开发者,都是经历过这种心态的,然后在繁忙的版本迭代中、在赶着回家的加班时、在愈来愈发的对自己的薪水不满时、在一次又一次看到团队中别人得过且过的代码时,渐渐的,就可能对“生活”妥协,丢掉了完美主义。

然而如果你有更高的追求,就要勇敢的战胜自己的感性。

使用 IntDef、StringDef

平时特常用的 View.setVisibility() 方法使用 IntDef 来规定参数的可选项,可以试想一下,假如没用 IntDef 会怎么样?对于初学者来说,可能要稍微阅读一下源码或查下资料才能知道 setVisibility 有哪些参数可以设置。你可能会觉得没什么差,因为你很清楚 setVisibility 方法有哪些参数可以设置。但若是程序中新增的一个方法呢?比如你新接触一个模块,某个界面有若干个跳转 Action,你得先找到定义这些 Action 的地方,而若一不小心将这些 Action 分散写在不同的地方,那对后面的维护和拓展可能就是一个灾难。

建议凡是符合语义的逻辑,都必须用 IntDef、StringDef 来约束,它比枚举节省内存,性能更优,其 RetentionPolicy.SOURCE 表示此注解只在源码中存在,编译时会剔除。你可以在 Android Studio 的 Live Templates 中添加 IntDef、StringDef 写法:


使用精准表达的变量类型

比如你需要声明一个变量来表示某个功能是否启用,譬如控制你的 App 是否展示广告,并且可以通过服务端在线下发开关来控制,如果没有接收到下发的开关,就根据地区来决定是否展示。

这种情况下你会使用什么类型的变量?

你可能会想到使用一个 int 类型变量来控制,然后需要给这个变量加上注释:

    // 0:展示; 1:不展示; 2:未接收到在线开关,需要根据地区决定是否展示
    private int mShouldShowAd;

以后每当改动到这部分逻辑,都需要查看一下这个变量数值对应的含义,随着时间的推移和代码量的增多,在此逻辑之上可能堆积了很多代码,然后就会出现各种各样的问题,别人可能在不存在的逻辑分支做了一些事:

        if (mShouldShowAd == 0) {
            //do something
        } else if (mShouldShowAd == 1) {
            //do something
        } else if (mShouldShowAd == 2) {
            //do something
        } else {
            //do something...
        }

甚至可能对这个变量赋值 [0,2] 区间之外的数值! 你可能对这个变量的意义很了解也绝不会用错,但你不能保证他人不会出现上面所说的荒唐的用法,因为这个变量类型并不能很精准的表达它的语义,也没有任何约束性。

我们可以怎样改善这种难维护、有风险的代码?

  • 可以使用 IntDef 规定这个变量的取值
  • 可以换成 Boolean 类型,用 null 表示未获取到在线开关,恰好的表达语义并且易读、易维护
使用尽可能少的变量

举个例子:

        mDebug = BuildConfig.DEBUG;

        if (mDebug) {
            Log.d(TAG, "...");
        }

你是否写过这样的逻辑?明明已经存在了一个可以直接使用的变量条件,你仍然要重新定义。这个例子逻辑还十分简单,此变量是 final 类型的,不会出错。而如果是非 final 类型的变量,那就是强行增加了一个赋值联动的逻辑,埋下了隐患,后续如果出了问题,白白的增加了定位问题的路径与复杂度。

实际开发中我们可能自己都意识不到使用了不必要的变量,比如我们的服务端接口一般会有多个接口环境,那你的代码可能是这样的:

    //是否是测试环境
    private static boolean sIsApiHostTest;
    //是否是beta环境
    private static boolean sIsApiHostBeta;
    //正式环境host
    private static String sApiHost = "http://api.com/";
    //测试环境host
    private static String sApiHostTest = "http://test.api.com/";
    //beta环境host
    private static String sApiHostBeta = "http://beta.api.com/";

    /**
     * 是否是测试环境
     */
    public static boolean isApiTest() {
        return sIsApiHostTest;
    }

    /**
     * 是否是beta环境
     */
    public static boolean isApiBeta() {
        return sIsApiHostBeta;
    }

    /**
     * 获取接口域名
     */
    public static String getApiHost() {
        if (isApiTest()) {
            return sApiHostTest;
        } else if (isApiBeta()) {
            return sApiHostBeta;
        } else {
            return sApiHost;
        }
    }

这样看起来好像没什么问题,只要维护好 sIsApiHostTest、sIsApiHostBeta 这两个变量就行了。如果后面又添加了一个环境呢?又添加了三四个环境呢?是不是还要维护多个变量?这个逻辑可以通过减少变量来改善:

    //当前环境host
    private static String sCurApiHost;
    //正式环境host
    private static String sApiHost = "http://api.com/";
    //测试环境host
    private static String sApiHostTest = "http://test.api.com/";
    //beta环境host
    private static String sApiHostBeta = "http://beta.api.com/";

    /**
     * 是否是测试环境
     */
    public static boolean isApiTest() {
        return sApiHostTest.equals(sCurApiHost);
    }
    
    /**
     * 是否是beta环境
     */
    public static boolean isApiBeta() {
        return sApiHostBeta.equals(sCurApiHost);
    }

    /**
     * 获取接口域名
     */
    public static String getApiHost() {
        return sCurApiHost;
    }

再加上 StringDef 就完美了:

    @StringDef({ApiHost.sApiHost, ApiHost.sApiHostTest, ApiHost.sApiHostBeta})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ApiHost {
        //正式环境host
        String sApiHost = "http://api.com/";
        //测试环境host
        String sApiHostTest = "http://test.api.com/";
        //beta环境host
        String sApiHostBeta = "http://beta.api.com/";
    }

    //当前环境host
    @ApiHost
    private static String sCurApiHost = ApiHost.sApiHost;

    /**
     * 是否是测试环境
     */
    public static boolean isApiTest() {
        return ApiHost.sApiHostTest.equals(sCurApiHost);
    }

    /**
     * 是否是beta环境
     */
    public static boolean isApiBeta() {
        return ApiHost.sApiHostBeta.equals(sCurApiHost);
    }

    /**
     * 获取接口域名
     */
    @ApiHost
    public static String getApiHost() {
        return sCurApiHost;
    }

    /**
     * 设置接口域名
     */
    @ApiHost
    public static void setApiHost(@ApiHost String apiHost) {
        sCurApiHost = apiHost;
    }

不知道你有没有感受到易读性、可维护性、拓展性都蹭蹭蹭的往上涨呢?

单一数据源

同时接受多个数据源数据的逻辑相比只接受一个数据源的数据需要考虑时序性等问题,要复杂很多。打个比方,可以把数据源当作你的直接上级,上级会不定时的分配任务给你做,如果你有多个上级,一个让你做任务 A,一个让你做任务 B,且 A 需要在 B 之前完成,你要怎么办?两个上级都让你做任务 A,但是只用做一次,你要怎么办?

在安卓中较为典型的场景就是同时加载网络和本地缓存数据到 UI 上,你的 UI 上展示的数据来自不同的地方,你需要考虑不同数据源之间如何协作。谷歌推出的 Jetpack 开发指南上推荐我们使用单一数据源,假如你的网络数据也需要缓存的话,那你的实现逻辑应该是这样:

  • 加载网络数据,返回后插入到本地
  • 统一从本地取数据展示到 UI 上

这点和上面说的“使用尽可能少的变量”有相通之处,都是尽量规避使用多个条件变量对程序产生影响的逻辑。

职责分离

强烈建议什么类里就干什么事,别把逻辑都揉到一块儿,这样随着代码量的增加,会愈发的难以维护,到最后就变成一颗存在重大隐患的地雷,看见就头疼。

举个例子,比如你要自定义一个 View,那就像系统控件一样,只负责一个控件该负责的事,处理一下渲染、展示,把手势交互通过接口开放出来,把数据的获取写在数据仓库中。这样如果数据展示出了问题,可以很快的定位到是数据获取出了问题,还是渲染展示出了问题;如果这个控件的渲染展示是经过验证的,之后就几乎不用改动此控件,至少你有机会可以将你的自定义 View 写的像系统的控件一样稳定。

这里再推荐一下谷歌的 Jetpack - MVVM 全家桶,MVC 真的是不易读、难维护、问题多、很简陋。

回归最初的完美主义

希望你我可以战胜感性,不向“生活”妥协,让优秀成为准则和习惯,回归最初的完美主义。

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