您的当前位置:首页项目架构分析之MVVM

项目架构分析之MVVM

2024-12-14 来源:哗拓教育

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

架构描述图.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);
}
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,仅用来学习,无商业目的

显示全文