项目架构分析之MVVM

说起该架构的时候,其实之前看过一篇关于这方面的文章如何构建Android MVVM应用程序,当时看的时候,真的是一头雾水啊,毕竟里面涉及到的框架都只是听说过,自从咱公司用到了该架构的时候,才用时间来学习了下。

下面用一张图来描述下该架构:

架构描述图.png

这里借用网上的一张图了,望大家勿喷~~
总的来说分为三层,View层,ViewModel层Repository层,其中Repository层又可以分两块来处理,一块是数据库的分支,一块是网络的分支。

本来是打算自己做个该框架的demo,由于当前只是讲解部分,因此这里直接拿公司的项目说事了,先偷个懒哈:
Activity、Fragment或者View中请求相应model中的方法

private void loadConfig() {
    viewModel.loadConfig().observe(this, configResource -> {
        LogUtils.d("Config loaded " + configResource);
        assert configResource != null;
        switch (configResource.status) {
            case EMPTY:
                break;
            case ERROR:
                break;
            case LOADING:
            case SUCCESS:
                //这里看不懂没关系咯,知道成功后处理页面就行了
                if (configResource.data != null) {
                    String surveyUrl = configResource.data.getSurveyUrl();
                    if (!TextUtils.isEmpty(surveyUrl)) {
                        LogUtils.v("Config loaded: survey: " + surveyUrl);
                        viewModel.setSurveyUrl(surveyUrl);
                        invalidateOptionsMenu();
                    }
                    FeatureConfiguration.setConfig(configResource.data);
                }
                break;
        }
    });
}

其实刚看到该写法的时候,我自己也是挺不习惯的,网上查了说是RxJavalambda表达式。

好吧,说到这的时候,可能大家都尴尬了。先不管了,咋们看下该方法的链式结构:
viewModel.loadConfig()返回的是LiveData<Resource<Config>>这里就涉及到了lifecycleLiveData数据结构,为什么它也有observe方法呢。知道Rxjava的小伙伴们,也知道Rxjava有个observe方法。其中
observe方法中需要两个参数LifecycleOwnerObserver。其实咋们要处理的逻辑都是在ObserveronChange方法中,上面代码如果不用lambda表达式的话,就是下面的样子了:

private void loadConfig() {
    viewModel.loadConfig().observe(this, new Observer<Resource<Config>>() {
        @Override
        public void onChanged(@Nullable Resource<Config> configResource) {
            LogUtils.d("Config loaded " + configResource);
            assert configResource != null;
            switch (configResource.status) {
                case EMPTY:
                    break;
                case ERROR:
                    break;
                case LOADING:
                case SUCCESS:
                    if (configResource.data != null) {
                        String surveyUrl = configResource.data.getSurveyUrl();
                        if (!TextUtils.isEmpty(surveyUrl)) {
                            LogUtils.v("Config loaded: survey: " + surveyUrl);
                            viewModel.setSurveyUrl(surveyUrl);
                            invalidateOptionsMenu();
                        }
                        FeatureConfiguration.setConfig(configResource.data);
                    }
                    break;
            }
        }
    });
}

lambda表达式看上去就是简洁,可读性更高了。好了,咋们要去model层的loadConfig方法了:

public LiveData<Resource<Config>> loadConfig() {
    return configRepository.reload();
}

好吧,model层只是调用了repository层的方法而已。这里要提一点configRepository生成的方式:

private ConfigRepository configRepository;
@Inject
MainViewModel(@NonNull Application application, ConfigRepository configRepository, DeviceRepository
        deviceRepository, StorageRepository storageRepository, OrderRepository orderRepository) {
    super(application);
    //省略代码
    this.configRepository = configRepository;
    //省略代码
}

这里在构造器上面加了@Inject注解,用到了dagger2注解部分,加上该注解,相当于不需要传该实参,该注解会去找到该类的实例。那咋们去看看ConfigRepositoryreload方法吧:

public LiveData<Resource<Config>> reload() {
    String imei = DeviceInfoUtils.getImei();
    return new NetworkBoundResource<Config, Config>() {

        @Override
        protected void saveCallResult(@NonNull Config config) {
            LogUtils.i(logTag, "configure saved " + config);
            configDao.save(config);
        }

        @Override
        protected boolean shouldFetch(@Nullable Config config) {
            return config == null
                    || config.shouldFetch()
                    || (!TextUtils.isEmpty(imei) && !imei.equals(config.getImei()));
        }

        @NonNull
        @Override
        protected LiveData<Config> loadFromDb() {
            return configDao.loadLiveConfig();
        }

        @NonNull
        @Override
        protected LiveData<Resource<Config>> createCall(Config config) {
            LogUtils.v(logTag, "load launch config with imei: " + imli);
            MutableLiveData<Resource<Config>> result = new MutableLiveData<>();
            LaunchConfigMessage message = new LaunchConfigMessage(DeviceInfoUtils.getModel(), imei, DeviceInfoUtils.getCountryCode());
            ServerConnector.sendMessage(message)
                    .subscribe(new Observer2<LaunchConfigMessage>() {

                        @Override
                        public void onError(Throwable e) {
                            result.setValue(Resource.error(e.getMessage(), null));
                        }

                        @Override
                        public void onNext(LaunchConfigMessage launchConfigMessage) {
                            Config config = new Config();
                            config.setImei(imli);
                            config.setShowChat(launchConfigMessage.isShowOnlineChat());
                            config.setSurveyUrl(launchConfigMessage.getSurveyUrl());
                            config.setChatUrl(launchConfigMessage.getChatUrl());
                            config.setEmailUrl(launchConfigMessage.getEmailUrl());
                            config.setChatAPIUsername(launchConfigMessage.getUsername());
                            config.setChatAPIPassword(launchConfigMessage.getPassword());
                            config.setShowEmail(launchConfigMessage.isShowEmail());
                            config.setShowMessenger(launchConfigMessage.isShowMessenger());
                            config.setShowTwitter(launchConfigMessage.isShowTwitter());
                            config.setShowPricing(launchConfigMessage.isShowPricing());
                            config.setLastSyncedAt(new Date());
                            result.setValue(Resource.success(config));
                        }
                    });

            return result;
        }
    }.getAsLiveData();
}

其实拿到Repository层代码的时候,也是一头雾水。那咱们对照着开篇的图找找哪部分是走网络的,哪部分是走数据库的。这里看返回数据先是new了一个NetworkBoundResource对象,然后调用了getAsLiveData方法,最后返回了LiveData<Resource<Config>>数据类型。既然NetworkBoundResource重写了几个方法,想必是一个抽象类了,咱们也进去瞧瞧该类:

@MainThread
protected NetworkBoundResource() {
    reload();
}

这里走了reload()方法:

@MainThread
public void reload() {
    //设置状态,表示正在loading
    result.setValue(Resource.loading(null));
    result.removeSource(dbSource);
    dbSource = loadFromDb();
    result.addSource(dbSource, data -> {
        LogUtils.d(logTag,"reload changed");
        result.removeSource(dbSource);
        if (shouldFetch(data)) {
            LogUtils.d(logTag,"fetch from network");
            fetchFromNetwork(dbSource, data);
        } else {
            LogUtils.d(logTag,"fetch from db");
            fetchFromDb(dbSource);
        }
    });
}

首先一上来就是loadFromDb方法了,然后把该dbSource通过result对象添加到返回的data中了,然后传给了shouldFetch方法,这里如果shouldFetch返回true,那么就走网络,否则去走本地了。咋们先看fetchFromDb方法:

private void fetchFromDb(final LiveData<ResultType> dbSource) {
    result.addSource(dbSource,
            newData -> {
                if (newData == null ||
                        (newData instanceof Collection && ((Collection)newData).size() == 0)) {
                    result.setValue(Resource.empty());
                } else {
                    result.setValue(Resource.success(newData));
                }
            });
}

其实这里只是将数据转化成newData后,在判断是不是空了,如果是空对result设置空的状态,否则返回成功状态,并返回数据。

咱们再回头看看子类NetworkBoundResourceloadFromDb方法:

@NonNull
@Override
protected LiveData<Config> loadFromDb() {
    return configDao.loadLiveConfig();
}

这里就是走数据库的查询方法了:

@Dao
public interface ConfigDao {
    // table will have one record. The id is always set to 1
    @Query("SELECT * FROM config where id = 1")
    LiveData<Config> loadLiveConfig();

    @Insert(onConflict = REPLACE)
    void save(Config config);
}

数据库部分用到了google的room框架,还没学习room框架的小伙伴们赶快去看下吧Android Room Orm框架学习。这里就不多赘述room框架的持久化特性了。
咱们还是回到fetchFromNetwork部分,看下是怎么走到网络的:

private void fetchFromNetwork(final LiveData<ResultType> dbSource, ResultType data) {
    LiveData<Resource<RequestType>> apiResponse = createCall(data);
    // 重新附加 dbSource 作为新的来源,
    // 它将会迅速发送最新的值。
    result.addSource(dbSource,
            newData -> result.setValue(Resource.loading(newData)));
    result.addSource(apiResponse, response -> {
        result.removeSource(apiResponse);
        result.removeSource(dbSource);
        if (response == null || response.status == Status.SUCCESS
                || response.status == Status.EMPTY) {
            saveResultAndReInit(response == null ? Resource.empty() : response);
        } else {
            Assert.assertTrue(response.status == Status.ERROR);
            onFetchFailed();
            result.addSource(dbSource,
                    newData -> result.setValue(
                            Resource.error(response.message, newData)));
        }
    });
}

上面方法中第一句就创建了我们网络的实例,那咱们去看看createCall方法吧:

@NonNull
@Override
protected LiveData<Resource<Config>> createCall(Config config) {
    LogUtils.v(logTag, "load launch config with imei: " + imli);
    MutableLiveData<Resource<Config>> result = new MutableLiveData<>();
    LaunchConfigMessage message = new LaunchConfigMessage(DeviceInfoUtils.getModel(), imei, DeviceInfoUtils.getCountryCode());
    //这里就是走网络部分的方法了
    ServerConnector.sendMessage(message)
            .subscribe(new Observer2<LaunchConfigMessage>() {

                @Override
                public void onError(Throwable e) {
                    result.setValue(Resource.error(e.getMessage(), null));
                }

                @Override
                public void onNext(LaunchConfigMessage launchConfigMessage) {
                    Config config = new Config();
                    config.setImei(imli);
                    config.setShowChat(launchConfigMessage.isShowOnlineChat());
                    config.setSurveyUrl(launchConfigMessage.getSurveyUrl());
                    config.setChatUrl(launchConfigMessage.getChatUrl());
                    config.setEmailUrl(launchConfigMessage.getEmailUrl());
                    config.setChatAPIUsername(launchConfigMessage.getUsername());
                    config.setChatAPIPassword(launchConfigMessage.getPassword());
                    config.setShowEmail(launchConfigMessage.isShowEmail());
                    config.setShowMessenger(launchConfigMessage.isShowMessenger());
                    config.setShowTwitter(launchConfigMessage.isShowTwitter());
                    config.setShowPricing(launchConfigMessage.isShowPricing());
                    config.setLastSyncedAt(new Date());
                    result.setValue(Resource.success(config));
                }
            });

    return result;
}

大家定位到注释部分,看看是如何生成Observable对象的:

public static <T extends APIDefinition> Observable<T> sendMessage(final T message) {
    return Observable.create((Observable.OnSubscribe<T>) subscriber -> {
        doSendMessage(subscriber, message);
    }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}

不难看出被观察者里面调用了doSendMessage方法:

private static <T extends APIDefinition> void doSendMessage(Subscriber<? super T> subscriber, final T message) {
    if (!NetworkUtil.isConnected()) {
        subscriber.onError(new NetworkException());
    }

    String resultString = null;

    try {

        for (String method : message.methods()) {
            if (method.equalsIgnoreCase("post")) {
                //网络的关键地方
                resultString = doPost(message);
                break;
            }
            if (method.equalsIgnoreCase("get")) {
                //网络的关键地方
                resultString = doGet(message);
                break;
            }
        }
        parseResult(resultString, message);
        subscriber.onNext(message);
        subscriber.onCompleted();
    } catch (RequestFailException e) {
        subscriber.onError(e);
    } catch (IOException e) {
        subscriber.onError(new NetworkException());
    } catch (JSONException e) {
        LogUtils.e("fail to parse result for " + message.api());
        LogUtils.e("Invalid Json response: " + resultString);
        subscriber.onError(new Exception("internal error", e));
    } catch (ParameterCheckFailException e) {
        subscriber.onError(new Exception("internal error " + e.getMessage(), e));
    } catch (Exception e) {
        subscriber.onError(new Exception("response format error", e));
    }
}

可以看到此处调用了doGet或是doPost方法,后面就不说明了,都是些okHttp的代码。走完了网络的代码,那什么时候将数据保存到数据库的呢?咋们还是去看fetchFromNetwork方法中调用了saveResultAndReInit方法:

@SuppressLint("StaticFieldLeak")
@MainThread
private void saveResultAndReInit(@NonNull Resource<RequestType> response) {
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
            if (response.data != null) {
                saveCallResult(response.data);
            }
            return null;
        }
        @Override
        protected void onPostExecute(Void aVoid) {
            if (response.data != null) {
                // We need recreate the dbSource here, so subclass have a chance to change
                // the load policy
                result.removeSource(dbSource);
                dbSource = loadFromDb();
                fetchFromDb(dbSource);
            }
        }
    }.execute();
}

可以看到doInBackground方法中调用了saveCallResult方法,咋们还要回到子类去看怎么保存到数据库的:

@Override
protected void saveCallResult(@NonNull Config config) {
    LogUtils.i(logTag, "configure saved " + config);
    configDao.save(config);
}

看到了没,这里才是保存数据库的地方。最后可以看到saveResultAndReInit方法中的AsyncTask会调用fetchFromDb方法,将最后的状态交给了LiveData

整个的流程就是这么回事了,关于页面上的databing部分,详情看后续的demo部分

主页面.gif
详情页.gif

数据都是来自知乎api,仅用来学习,无商业目的

代码传送门

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

推荐阅读更多精彩内容