FrameWork层源码的分析(14)-Google新组件下的架构思考

主目录见:Android高级进阶知识(这是总目录索引)
mvvm框架地址:MarsArchitecture

 从谷歌发布新的框架组件,试用之后,不仅开始设想着改进之前的mvp模式架构,谷歌新框架新组件ViewModel,LiveData,Room,确实如谷歌所推荐的,从我感受来看,确实能省不少代码,代码也会比较优雅,包括加上前面的DataBinding,以及在mvp框架中用的流行框架rxjava,retrofit。那么我们就一起来封装下,当然大家可以改进,还是有很多空间的。下面我们看下下面的整体架构图片:


MVVM架构体系思考.png

一.基本使用

1.我们设想,每个界面需求最大的就是获取数据的接口,所以我们封装一个公用的接口,使用如下:

    mGirlsViewModel = ViewModelProviders.of(MainActivity.this).get(DynamicGirlsViewModel.class);
    mGirlsViewModel.getELiveObservableData(Constants.GIRLS_URL, "3").observe(MainActivity.this, girlsData -> {
            if (null != girlsData){
                List<GirlsData.ResultsBean> resultsBeans = girlsData.getResults();
            }
        });

2.基本的使用是下面这种:

tvTest.setOnClickListener(view -> mGirlsViewModel.getGirlsData("2","3").observe(MainActivity.this, girlsData -> {
     if (null != girlsData){
            List<GirlsData.ResultsBean> resultsBeans = girlsData.getResults();
      }
}));

二.封装

这几个组件怎么使用我这里就不用写了,不然文章可能逻辑会有点混乱,我这里直接就从封装开始,从图中可以知道,我们界面Activity和fragment和数据的交互是跟ViewModel进行的,我们这里封装了一个BaseViewModel,里面有公共的访问数据接口,这个方法是BaseViewModel#getELiveObservableData()方法:

 public LiveData<T> getELiveObservableData(String url, String... params) {
            IRequest request = (IRequest) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{IRequest.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object object = null;
                    try {
                        String url = redirectParseMethodUrl(args);
                        if (null != url){
                            object = getLiveObservableData(url);
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    return object;
                }
            });

            request.getLiveObservableData(url,params);
        return liveObservableData;
    }

这个方法的第一个参数是网络访问的url,这个url可以是全url地址,或者是接在base url后面的地址,第二个参数是要传到后台服务接口的参数,我们都知道,我们传到后台,后台要知道我们传的参数对应的键是什么,所以我们这里定了一个url的规则(这里我们封装的get的请求方式),url有两种方式,一种是接在url中的,另一种是接在后面key=value的方式,所以如果是第一种,我们会在url中加上{**},{}中的是对应的key,然后在最后加上一个标识/path,第二种是就不用接后面的标识/path,如下面:

public static final String PATH_URL = "/Path";
public static final String BASE_URL =  "http://gank.io";
public static final String GIRLS_URL = "api/data/%E7%A6%8F%E5%88%A9/20/{index}/"+PATH_URL;

然后我们继续看BaseViewModel#getELiveObservableData()方法,其实这个方法可以不用动态代理来做,但是当初设想着另外一种做法,后来就干脆不改,大家也可以不用,然后我们看invoke()方法里面,首先第一个方法redirectParseMethodUrl()方法就是为了解析传进来的url的,方法实现如下:

  private String redirectParseMethodUrl(Object[] args){
        if (null != args && args.length > 0){
            String url = (String) args[0];
            boolean isPath = false;
            if (null != url){
                isPath = url.contains(PATH_URL);
                if (isPath){
                   return parseMethodUrlPath(args);
                }else{
                   return parseMethodUrlKey(args);
                }
            }
        }
        return "";
    }

这个方法首先判断url中是否含有/path标识,如果有就用path的拼凑方式,如果没有就用key=value的形式拼凑,首先看path的方式:

 private String parseMethodUrlPath(Object[] args) {
        String url = "";
        if (null != args && args.length > 0){
            url = (String) args[0];
            url = url.substring(0,url.lastIndexOf("/") - 1);
            Matcher matcher = pattern.matcher(url);
            for (int i = 0;matcher.find();i++){
                Object[] value = (Object[]) args[1];
                String key = matcher.group();
                if (null != value){
                    url = url.replace(key,(String)value[i]);
                }
            }
        }

        return url;
    }

这个方法首先取出除path以外的url,然后用模式匹配出{}部分,循环替换{}部分的内容为真正的值。另外一种key=value的形式如下:

  private String parseMethodUrlKey(Object[] args){
        String baseUrl = "";
        if (null != args && args.length > 0){
            String url = (String) args[0];
            int index = url.indexOf("{");
            if (index > 0){
                baseUrl = url.substring(0,index-1);
                Matcher matcher = pattern.matcher(url);
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append("?");
                for (int i = 0;matcher.find();i++){
                    String key = matcher.group();
                    String realKey = key.substring(key.indexOf("{")+1,key.lastIndexOf("}"));
                    Object[] value = (Object[]) args[1];
                    if (value != null){
                        stringBuilder.append(realKey+"="+value[i]+ "&");
                    }
                }

                baseUrl += stringBuilder.toString();
            }
        }

        return baseUrl;
    }

这个方法截取出{}中的内容作为key,作为给后台的标识,最后拼凑出来的地址为baseurl?key=value&key1=value1&**形式。然后得到url之后传给BaseViewModel#getLiveObservableData():

 public LiveData<T> getLiveObservableData(String url,String...params) {
        DataRepository.getDynamicData(url,getTClass())
                .subscribeWith(new DisposableSubscriber<T>() {
                    @Override
                    public void onNext(T value) {
                        if(null != value){
                            liveObservableData.setValue(value);
                        }
                    }

                    @Override
                    public void onError(Throwable t) {
                    }

                    @Override
                    public void onComplete() {

                    }
                });
        return liveObservableData;
    }

在这个方法里面我们调用了DataRepository里面的方法,*Repository中主要是获取数据,我们获取数据的方式一般有两种,从网络获取数据,从本地数据库获取数据,在*Repository中正好隔离了这两种数据获取方式,这个地方先看从网络获取数据:

 public static <T> Flowable getDynamicData(String url, final Class<T> clazz) {
        return ApiServiceModule.getInstance().getNetworkService()
                .getDynamicData(url)
                .compose(SwitchSchedulers.applySchedulers())
                .map(new Function<ResponseBody, T>() {
                    @Override
                    public T apply(ResponseBody responseBody) throws Exception {
                        return GsonHelper.getIntance().str2JsonBean(responseBody.string(),clazz);
                    }
                });
    }

这里网络访问用的是retrofit,retrofit这里做了下封装,retrofit我们知道访问的时候restful api是在接口中的,所以我们可以用动态代理的形式来做些网络访问错误重试,如下所示:

    private ApiService provideApiService(Retrofit retrofit){
        return getByProxy(ApiService.class,retrofit);
    }

    private ApiService getByProxy(Class<? extends ApiService> apiService, Retrofit retrofit){
        ApiService api = retrofit.create(apiService);
        return (ApiService) Proxy.newProxyInstance(apiService.getClassLoader(),new Class<?>[] { apiService },new ResponseErrorProxy(api,retrofit.baseUrl().toString()));
    }

具体的动态代理invoke方法内容如下:

 @Override
    public Object invoke(Object proxy, final Method method, final Object[] args) {
           return Flowable.just("")
                   .flatMap((Function<String, Flowable<?>>) s -> (Flowable<?>) method.invoke(mProxyObject, args))
                    .retryWhen(throwableFlowable -> throwableFlowable.flatMap((Function<Throwable, Publisher<?>>) throwable -> {
                        ResponseError error = null;
                        if (throwable instanceof ConnectTimeoutException
                                || throwable instanceof SocketTimeoutException
                                || throwable instanceof UnknownHostException
                                || throwable instanceof ConnectException) {
                            error = new ResponseError(HTTP_NETWORK_ERROR, "当前网络环境较差,请稍后重试!");
                        } else if (throwable instanceof HttpException) {
                            HttpException exception = (HttpException) throwable;
                            try {
                                error = new Gson().fromJson(exception.response().errorBody().string(), ResponseError.class);
                            } catch (Exception e) {
                                if (e instanceof JsonParseException) {
                                    error = new ResponseError(HTTP_SERVER_ERROR, "抱歉!服务器出错了!");
                                } else {
                                    error = new ResponseError(HTTP_UNKNOWN_ERROR, "抱歉!系统出现未知错误!");
                                }
                            }
                        } else if (throwable instanceof JsonParseException) {
                            error = new ResponseError(HTTP_SERVER_ERROR, "抱歉!服务器出错了!");
                        } else {
                            error = new ResponseError(HTTP_UNKNOWN_ERROR, "抱歉!系统出现未知错误!");
                        }

                        if (error.getStatus() == HTTP_UNAUTHORIZED) {
                            return refreshTokenWhenTokenInvalid();
                        } else {
                            return Flowable.error(error);
                        }
                    }));
    }

我们看到这里有出错重试机制,具体的错误类型如方法中所示。最后我们网络访问返回的数据经Gson处理得到具体实体类,最终设置给LiveData包装的数据,这样在Activity中监听得到数据的变化。网络访问处理成为实体类的时候我们也可以封装一个公用的类,因为每个接口的json返回数据都有可能有code,message,data这些,这里没有写,要是想看可以去看LArtemis这个工程。到这里网络访问的方式已经完成,讲的有点粗略请见谅,然后我们开始讲数据库获取数据的方式,也是从Respository里面获取的,如下所示:

 public static Flowable<User> getUserData(Application application){
        return AppDatabase.getInstance(application,mAppExecutors)
                .userDao().getUser();
    }

从方法里面可以看出,这里是获取AppDatabase实例,然后获取到dao实例,最后取出前端数据库的数据,AppDatabase实例获取方式如下:

 public static AppDatabase getInstance(final Context context, final AppExecutors executors) {
        if (sInstance == null) {
            synchronized (AppDatabase.class) {
                if (sInstance == null) {
                    sInstance = buildDatabase(context.getApplicationContext(), executors);
                    sInstance.updateDatabaseCreated(context.getApplicationContext());
                }
            }
        }
        return sInstance;
    }

    private static AppDatabase buildDatabase(final Context appContext,
                                             final AppExecutors executors) {
        return Room.databaseBuilder(appContext, AppDatabase.class, DATABASE_NAME)
                .addCallback(new Callback() {
                    @Override
                    public void onCreate(@NonNull SupportSQLiteDatabase db) {
                        super.onCreate(db);
                        executors.diskIO().execute(() -> {
                            AppDatabase database = AppDatabase.getInstance(appContext, executors);
                            // Time consuming task

                            // notify that the database was created and it's ready to be used
                            database.setDatabaseCreated();
                        });
                    }
                }).build();
    }

这里是从google的例子中借鉴过来的,这是单例模式获取这个实例。然后后面就是Room的标准操作方法,就不细说了。到这里已经讲完大概的一个流程,这也是简单的一个封装,如果有什么好的改进方法,可以提出来哈,可以交流交流。

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