从零开始的Android新项目6 - Repository层(下) Realm、缓存、异常处理

承接上篇从零开始的Android新项目5 - Repository层(上) Retrofit、Repository组装,本文继续介绍Realm、缓存,以及统一的异常处理设计。

Realm

Realm在移动端数据库中也算是比较有名的一款了,以其跨平台和惊人的速度而闻名。啊,对了,还有文档多。

这里要黑的就是文档问题,Realm虽然乍一看文档很多,但是老实说,写的挺乱的。不过总体来说,实践和应用中感觉还不错,性能好,也比较方便,比起不稳定的DBFlow和麻烦至极的GreenDao来好了太多了,唯一的美中不足就是so比较大,会增大包的体积1MB。

引入

从Realm 0.90开始,用法与之前有了改变:

在root的build.gralde中:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:0.90.1"
    }
}

然后在对应需要应用到Realm的,比如data module的build.gradle:

apply plugin: 'realm-android'

即可使用Realm。

使用

使用起来也很方便,比如我们想要缓存用户的信息

public class UserPo extends RealmObject {
    @PrimaryKey
    public String id;
    public String name;
    public String headerUrl;
    public long updateTime;
}

这样就对应了一个表,其主键为id,另外有3列name, headerUrl, 以及updateTime。

如果想要查询,只需要:

UserPo user = getRealm().where(UserPo.class)
        .equalTo("id", userId)
        .findFirst();

如果要写入一条记录:

User user = new UserPo();
user.setName(userInfoEntity.getNickName());
user.setId(userInfoEntity.getUserId());
user.setHeaderUrl(userInfoEntity.getHeaderImageUrl());
user.setUpdateTime(System.currentTimeMillis());

getRealm().beginTransaction();
getRealm().copyToRealmOrUpdate(user);
getRealm().commitTransaction();

就是这么简单。

如果想要直接和Retrofit一起应用,去进行串行化,可以参考该Gist

// 结合 Realm, Retrofit 和 RxJava (使用了Retrolambda以简化符号)的例子。
// 读取所有Person,然后与从GitHub获取的最新状态merge到一起
Realm realm = Realm.getDefaultInstance();
GitHubService api = retrofit.create(GitHubService.class);
realm.where(Person.class).isNotNull("username").findAllAsync().asObservable()
    .filter(persons.isLoaded)
    .flatMap(persons -> Observable.from(persons))
    .flatMap(person -> api.user(person.getGithubUserName())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(user -> showUser(user));

更多详情可以去官网看,migration/relationship等等支持应有尽有,我只能说,文档实在太长太长了。

内存

内存,也就是直接使用变量存储在对应repository中,如果非空则优先直接返回内存中的变量。

LruCache

LruCache限定了最大的entry数量,近期最少使用算法保证了淘汰机制的合理性。使用场景如用户信息缓存,会淘汰那些最近没有访问过的用户的信息缓存。使用可参考Google官网:LruCache

变量存储

变量存储很简单,直接在Repository实现类中直接变量存储上一次的返回结果,在下一次请求的时候优先使用内存缓存。使用场景如请求后直接刷新本地的变量,下次调用repository方法使用啊concat先返回内存里的变量,然后再使用网络数据进行刷新。

统一异常处理

作为Repository层,本身不会,也不应该去处理任何异常和错误(比如请求的错误码),一切都将作为Exception异常抛给上层去做统一处理,而RxJava的onError机制也帮助我们能优雅地去做这件事。

Observable.error

类似在上一篇中提到的方法,我们可以使用Observable.error返回异常,供上层根据该异常做对应处理。无论是网络异常,数据库异常,亦或是服务器response异常等等,都可以进行分类创建对应的Exception类,抛给上层。

public static <T> Observable<T> extractData(Observable<MrResponse> observable, Class<T> clazz) {
    return observable.flatMap(response -> {
        if (response == null) {
            return Observable.error(new NetworkConnectionException());
        } else if (response.getStatusCode() == ResponseException.STATUS_CODE_SUCCESS) {
            return Observable.just(mGson.fromJson(mGson.toJson(response.data), clazz));
        } else {
            Logger.e(TAG, response.data);
            return Observable.error(new ResponseException(response));
        }
    });
}

Subscriber.onError

我们使用Subscriber的基类来处理通用错误,其他所有Subscriber继承它:

public class MrSubscriber<T> extends DefaultSubscriber<T> {
   @Override
   public void onError(Throwable e) {
       super.onError(e);
       if (!handleCommonResponseError((Exception) e)) {
           if (e != null && e.getMessage() != null) {
               Logger.w(TAG, e.getMessage());
           }
           showErrorMessage(new DefaultErrorBundle((Exception) e));
       }
   }
}

protected void showErrorMessage(ErrorBundle errorBundle) {
    String errorMessage = ErrorMessageFactory.create(this, errorBundle.getException());
    showErrorMessage(errorMessage);
}

protected void showErrorMessage(String errorMessage) {
    ToastUtils.show(this, errorMessage);
}

DefaultErrorBundle是exception的wrapper,管理了其错误。

public class DefaultErrorBundle implements ErrorBundle {

    private static final String DEFAULT_ERROR_MSG = "Unknown error";

    private final Exception exception;

    public DefaultErrorBundle(Exception exception) {
        this.exception = exception;
    }

    @Override
    public Exception getException() {
        return exception;
    }

    @Override
    public String getErrorMessage() {
        return (exception != null) ? this.exception.getMessage() : DEFAULT_ERROR_MSG;
    }
}

ErrorMessageFactory是错误消息工厂,根据exception创建对应的错误消息提示,让用户不至于碰到错误莫名其妙。

/**
 * Factory used to create error messages from an Exception as a condition.
 */
public class ErrorMessageFactory {

    private static final String TAG = "ErrorMessageFactory";

    private ErrorMessageFactory() {
        //empty
    }

    /**
     * Creates a String representing an error message.
     *
     * @param context   Context needed to retrieve string resources.
     * @param exception An exception used as a condition to retrieve the correct error message.
     * @return {@link String} an error message.
     */
    public static String create(Context context, Exception exception) {
        if (StringUtils.isNotEmpty(exception.getMessage())) {
            Logger.e(TAG, exception.getMessage());
        }

        String message = context.getString(R.string.exception_message_generic);

        if (exception instanceof NetworkConnectionException) {
            message = context.getString(R.string.exception_message_no_connection);
        } else if (exception instanceof NotFoundException) {
            message = context.getString(R.string.exception_message_not_found);
        } else if (exception instanceof ResponseException) {
            message = exception.getMessage();
        } else if (exception instanceof HttpException) {
            message = exception.getMessage();
        }
        return message;
    }
}

handleCommonResponseError

通常,服务器会返回错误信息,我们需要根据一些code进行对应处理,MrSubscriber的onError就调用了handleCommonResponseError来处理这些通用错误:

protected boolean handleCommonResponseError(Exception exception) {
    boolean handled = false;
    if (exception instanceof ResponseException) {
        ResponseException responseException = (ResponseException) exception;
        switch (responseException.getStatusCode()) {
            case ResponseException.ERROR_CODE_NEED_LOGIN:
                handled = true;
                getUserSystem().setVuser("");
                getNavigator().navigateToLoginPage(this);
                break;
            case ResponseException.ERROR_CODE_NEED_PERFECT_PROFILE:
                handled = true;
                if (responseException.getVuser() != null) {
                    getUserSystem().setVuser(responseException.getVuser().getVuser());
                }
                getNavigator().navigateToPerfectProfile(this);
                break;
            case ResponseException.ERROR_CODE_NEED_THIRD_PARTY_BIND:
                handled = true;
                getNavigator().navigateToThirdPartyBind(this);
                break;
        }
    }
    return handled;
}

Log & 上报

出错了当然要上报啦,bugly、友盟,本地写文件打zip包上传,Logger做的就是写文件log了,这些常见的app都会去做,这里就不赘述了。

总结和下集预告

本系列两篇文章描述了Android项目中,Repository层的设计与实现,也可以理解它为data或者model层。一个好的Repository层和上层相对独立,内聚完成业务逻辑的数据部分,即便内部有修改,比如添加了缓存,对外仍然保持一致。而好的异常处理设计一方面让代码中不会充斥着杂七杂八的 try & catch,另一方,恰当的错误展示也让用户知道究竟出了什么错,不至于莫名其妙。

下一次不知是何时相见,希望能为大家带来我们项目中使用React Native进行混合开发的苦与甜。

另外,打个小广告,本司的新产品Crew已经在各大Android应用市场上线,专注于职场垂直社交。一搜和兴趣相投的人聊天。iOS版本正在审核中。

2个字找到志趣相投的职场伙伴,秒搜陌生人同类,智能自动破冰。多关键字叠加,高效率锁定职场同僚。精准匹配兴趣对象,超轻聊天,更能一键组建群聊,加入一群人的狂欢。

demo没空写了,反正我也没混淆,直接反编译来黑我吧。哈哈。有bug或者功能上的意见建议欢迎直接反馈给我。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,097评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 我已古佛枯灯相与共 竹影摇曳半夜凉初透 你眼中仍柔情难收 如那一场梦 喜悦又惶恐 那日叶落归根依旧 秋夜微凉 神形...
    若风月夜阅读 493评论 13 8
  • 1 从小姐姐就属于别人家的孩子的那种,成绩一级棒,是父母的骄傲,老师的得意门生。而我至今还记得小学一次数学测试我的...
    不倾城阅读 446评论 1 4