面对接口脏数据你还在V层if str==null else setText?

5,380 阅读5分钟

本文同步自我是一只香脆的大鸡排

嘿,我亲爱的Android老司机。

你是否还依稀记得第一次使用TextView的setText方法设置了一个空数据,得到了这么个玩意。

Caused by: java.lang.NullPointerException (文中错误,见底部解释)

对的,这玩意老烦人了,写习惯了OC PHP等语言的程序员在java和android里估计会疯掉。

要满世界判断是否为空,是否等于双引号【“”】。

我们也许是使用过TextUtils.isEmpty(s),StringUtils.isEmpty(s)这种判断。

但是无一例外它们都需要在我们设置到View之前做一次判断。

不,我受够了!!

Scenes1


我们来搞事情解决一下这种问题。

首先我们知道,大多数情况下控件上的数据都是来自服务端的接口吐出。接口中给出的数据有空的情况是非常常见的。

多数情况下后台给出的数据如:size=1 name=null转成json后

会变成:{size:1,name:null}

或者这样{size:1,"name":"null"}

或者这样:{size:1}

看见没,name可以是null,也可以是双引号”null“,甚至是直接不返回。

移动端同学新手上路第一天看完返回的结果:nmpp啊啊啊啊啊啊!!!。

后端同学:你咬我?就是没有数据呀,反正我是不会改的。

方案一


能难倒我?不改就不改。蛮了不起了吧?

看我来个初值大法。

 public class SmartZero {
        String name = "";
        String pwd = "";
        int size;
}

额,如果我有一万个字段怎么办,这里的双引号写一万个内存会不会有影响啊?

有了,这样写。

public class SmartOne {
    final static String NULL = "";
    String name = NULL;
    String pwd = NULL;
    int size;
}

啊哈哈,这样就只有一个静态引用了。

提示:

常量字符串被引用时,如果内容在一致的情况下,会在常量池里只有一份,所有的引用将指向该地址。

简单来说,这里根即便写与不写static String NULL = "";和前者的写法在运行时内存里的变化并无区别。他们的区别仅是第一种写法会导致寄存器上空字符会被从新设值指向空字符串。所以后则的写法可能会节约细微的时间,几乎可以忽略不计。(详情分析读者可以看smali逆向文件)

这样写完后得到的结果就是:

马上就有大兄弟要说了:

你这个好low哇。不对吧,你这个只满足了是null的情况下。

如果name服务端返回的是双引号的“null”这种玩意,怎么搞?

你还不是一样要在TextView.setText之前判断一把?

赫,大兄弟勿躁,勿躁嘛!且听我把话说完,我们还有方案二的,坚决抵制在View层做数据脏检查。

方案二


目前主流GsonFastjson是序列化json最好用的方式。大鸡排这里尝试了下Gson的方式。我们不能直接使用Gson来转换。这里需要用到Gson库里的TypeAdapter来扩展一下我们自定义的对象。实现方式如下。

还是前面的对象:

 public class Smart {
        String name;
        String pwd;
        int size;
}

不同的是,这里我们没有改动它。而是通过Adpter来解析,或者说是串改本身json中的键值数据。

public  class SmartTypeAdapter extends TypeAdapter<Smart> {
        @Override
        public Smart read(JsonReader in) throws IOException {
                 final Smart smart = new Smart();
            smart.name = "";
            smart.pwd = "暂无";//初始化值是因为Gson不会遍历在json中没有的字段
            in.beginObject();
            String s=null;
            while (in.hasNext()) {
                if (in.peek() == JsonToken.NULL) {
                    in.nextNull();
                    in.endObject();
                    return smart;
                }
                switch (in.nextName()) {
                    case "name":
                        if (in.peek() != JsonToken.NULL){
                            s = in.nextString();
                            if (s != null&&!s.equals("null")){
                                smart.name = s;
                                break;
                            }
                        }
                        smart.name = "";
                        break;
                    case "pwd":
                        if (in.peek() != JsonToken.NULL){
                            s = in.nextString();
                            if (s != null&&!s.equals("null")){
                                smart.pwd = s;
                                break;
                            }
                        }
                        smart.pwd = "暂无";
                        break;
                    case "size":
                        smart.size = in.nextInt();
                        break;
                }
            }
            in.endObject();
            return smart;
        }
}

现在我们运行一下看看效果。

当json为:{size:18} 的情况下。

当json为:{size:18,name:"null",pwd:"null"} 的情况下。

我们可以发现方案二的办法要比方案一好很多,可定制化程度高。完全可以过滤掉字符串"null"这种情况的脏数据。

当json为:{"size": 18,"name": "null","pwd": null} null没有双引号的情况就不展示了,效果同上。

TypeAdapter的扩展性其实不仅仅可以用在这里做不同类型的空判断,它还可以做动态的json键值或者多类型映射。

总结


大多数情况在View层里再做很多脏数据判断并不合适,但实际情况我们还是这样写着。

直到我们学会了偷懒

不再写

if if if

等等等于空

错误 错误 清除 清除 记忆清除 归零

bug fixed:


1: 这里并不会空指针,是我以前记错了。该打脸,该打!昨天晚上从2.2的源码翻到7.0所有的setText方法都不会引起空指针异常。是我的疏忽。近凭借以前的记忆去认为会空指针。给大家添麻烦了。

    private void setText(CharSequence text, BufferType type,
                        boolean notifyBefore, int oldlen) {
        if (text == null) {
            text = "";
       }
       .....
}

但是如果服务端返回空数据,我们就不管了,这个还是不够体面的。要么服务端改,要么我们在中间层过滤掉。再不行就是日常if else。 可我总觉着在View层做这些事情是不够明智的。View应该只关注自己要展示什么东西,而不应该出现异常数据还放到了V层处理。应该在M层就要被过滤掉或处理。

一些不成熟的想法:

1.通用形DataAdpter在反序列化时进行过滤,但如果做到通用。就一定不再满足个性化的需求。

2.我有想过Hook TextView的方法来实现脏数据偷懒行为,这不外乎两种做法。要么反射再注入Hook。要么编译时对代码埋点。做法都不是很完美。

3.参考lombok的实现,或许也能做到在对象Get的时候生成一些校验。

4.依赖as做成插件Gradle Pulgin,在编译时像lomBok一样。他们都不会产生后期运行时和或像反射带来的性能影响。

拼死挣扎的Android程序员。