一种清晰, 便于扩展android项目架构方案

2,557 阅读9分钟

当前android开发主推MVVM, Google在其官方文档提到如下架构, 该方案利用ViewModel可以方便的将View和Model进行解藕,方便代码的维护,使整体的结构更加清晰。 final-architecture.png

MVVM

再来回顾一下MVVM架构,MVVM架构将整个视图,业务逻辑和数据分开了,其优点在于:

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
  3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
  4. 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。 摘自百度百科

ViewModel

对于MVVM架构,View层很好理解,就是Android中的Activity/Fragment, Model层也很清晰,就是用来进行数据的访问,处理,持久化操作。在以上结构中抽出了一个Repository层,由该层去管理数据源。

初次接触时可能对于ViewModel的定义会把握不准好。以上面的架构为例,以上的结构虽然是基于Jetpack套件来实现的,但对于开发而言,并没有必要纠结于Jetpack, 比如上面提到的ViewModel,其实如果项目中没有用到Jetpack, 我们也是可以采用这种架构方案去写代码。可以将ViewModel就理解成一个中间层,一个业务处理层。再直白一点,就是一个java类,该类的作用便是用来分担一部份Activity/Fragment的工作,同时与数据层面打交道,拿到数据后再传给View层。

采用以上架构,我们在开发一个功能时,可以很清晰的去组织代码。但这种方式,只能保证某个业务维度,某个功能点相关的代码是清晰的,好维护的。对于整个项目而言,我们该如何架构整个项目呢?

项目层面

对于一个小型的项目,由于代码量少,业务通常不复杂,没有过多的考虑整体代码的组织,结构的管理也不会给后期的维护带来很大的负担,毕竟代码量少,后期维护的成本相对较低,但对于一个中大型项目,代码组织混乱,对后期的维护人员来讲就是灾难。此处引入的方案是参看这篇文章,我把做了一些改动,引入到公司里某一款app开发上,大家反馈都很不错,开发体验大大提升。

我们先来看一下这篇文章提到的方案,作者有把示例代码上传到github上,其地址在这里。 其目录结构如下:

image.png

作者将功能与项目的基础架构的代码分离开。整个工程分为corefeatures两个包,core下面放的是整个工程的基础代码。将业务进行分类组织在features目录下,这样便将代码按照功能模块的维度组织起来。某个功能(如login)相关的代码都会在features.login下面。其好处:

  1. 代码的结构更加清晰,后面维护方便
  2. 后期如果业务起来了,想要做组件化,因为代码都在同一个目录下面,抽取出去也更加方便。

细化

再回到Google推荐的架构图,我们再将Google推荐的这个架构整合进项目中,于是,项目的结构便如下所示:

com.xxxx.app

  • core 整个app内所有模块共享,或公共的配置
    • data 全局的数据访问,如用户信息,app配置等
      • model
        • AppConfigInfo
        • UserInfo
      • datasource
        • impl
          • AppConfigRemoteDataSource
          • AppConfigLocalDataSource
          • UserRemoteDataSource
          • UserLocalDataSource
        • IUserRemoteDataSource
        • IUserLocalDataSource
        • IAppConfigRemoteDataSource
        • IAppConfigLocalDataSource
      • AppConfigRepository
      • UserRepository
    • util
    • view
    • base
      • BaseActivity
      • BaseFragment
      • BaseViewModel
    • hybrid
  • features 下面是按业务来分组
    • login
      • ui
        • xxxActivity
        • xxxViewModelFactory
        • xxxViewModel 可直接调用core/data里的UserRepository
    • product
      • data 考虑到同一个模块中的数据层面可以共用,故放在外层
        • model
          • ProductListInfo
          • ProductDetailInfo
        • datasource
          • IProductLocalDataSource
          • IProductRemoteDataSource
          • impl
            • ProductLocalDataSourceImpl
            • ProductRemoteDataSourceImpl
        • ProductRepository
      • ui
        • detail
          • view 放置自定义View
            • ProductLabelView
          • ProductDetailActivity
          • ProductDetailViewModel
        • list
          • rendermodel
          • view 放置自定义View
          • ...
          • ProductListViewModel (和ProductDetailViewModel 都调用ProductRepository来获取数据)
          • ....
        • xxxViewModelFactory如果业务简单, ViewModelFactory可以写在这里, detail模块和list可以共用
    • personal
      • data
      • ui
        • xxxViewModelFactory
        • xxxViewModel此处可能需要获取用户信息,则直接调用core/data里的UserRepository

以下对上面的结构进行说明

core 目录

服务于整个工程,一些基础代码。关于系统配置,用户信息等数据的操作全部放在这个里面。

  • data目录:DAO相关的操作,model包下面的是pojo, 数据的获取或持久化全部由xxxRepository去调用datasource包下面的数据源去实现
  • util目录:放置一些公共的工具类
  • view目录: 放置公共的自定义view,这些自定义view脱离具体的业务联系,能够在各业务模块使用
  • base目录:用来放置一些基础的组件,如BaseActivity, BaseFragment, BaseViewModel

features目录

按业务模块来划分不同的包,组织在该目录下。以下对代码的组织做一些说明

login模块

在上面的目录中,由于涉及到用户登录相关的DAO操作都已经放到core目录下了, 假设可以满足要求,那么login模块下只有UI相关文件和ViewModel,xxxViewModelFactory

product模块

这个模块是用来模拟某个模块下多个页面的场景。以商品列表页和详情页为例。由于DAO的操作可能会有重叠的场景,这里将它们的数据操作写在一起。UI层面按功能再分为list, detail两个包。list, detail两个页面的ViewModel可以采用同一个ViewModelFactory来创建。

  • rendermodel包:这个目录下有一个需要单独说明一下,当我们从服务器拿到数据了如ProductInfo之后,将数据显示在页面上,我们显示在页面上的一些信息很有可能是需要根据ProductInfo的数据进行加工的。为此,我们定义一个ProductInfoRenderModel.java用来承载只需要显示在页面上的数据。ProductInfoRenderModel.java则放在rendermodel包下面。

personal模块

personal模块中也会涉及到用户相关的信息,这也就是为什么一开始设计把用户相关信息的dao操作放到core目录下。personal模块下的xxViewModel如果要查用户相关的信息,可以直接调用core下面的UserRepository

至此,整个项目的大体架构便梳理完成了。采用这种方案将代码以功能模块进行划分,方便后期的维护。既使后续某个模块中进行了技术方案的改革,也能保证其影响的粒度最小。当然这里面主要是为了说明项目的主要结构,在实际项目中,除了这些,我们还会有adapter, 自己写的各种工具等等,这个就根据实际情况再自己分包了。接下来我们看一下涉及到的相关技术栈

技术栈

在这种项目架构中我们主要用到的技术栈有Jetpack中的ViewModel, ViewModelFactory, LiveData, ROOM,下面简单介绍一下这几种技术以及它们之间的整合。当然用于网络请求相关的我们可以用Okhttp, retrofit,此处就不介绍。

ViewModel

Jetpack组件中提供了ViewModel可以方便的将数据,对象与组件的生命周期绑定起来,方便进行组件间的数据共享,如一个activity中多fragment的情况。同时它可以有效的从架构层面上进行解藕,和mvp架构模式相比,可以大大减少接口/方法的个数。以登录为例,用户调用登录接口时需要调用presenter.login方法,login成功后调用 view.loginSuccess方法。而采用ViewMode后,用户在登录时调用viewModel.login方法,登录成功后,更新ViewModel中的LiveData,然后在调用处观察LiveData做相应的行为就可以。

ViewModelProvider.Factory

用来创建ViewModel,ViewModel不可以自己创建,必须要借助ViewModelProvider.Factory来创建。在创建时通常为ViewModel指定数据仓库,如下:

public class LoginViewModelFactory implements ViewModelProvider.Factory {
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (modelClass.isAssignableFrom(LoginViewModel.class)) {
            return (T) new LoginViewModel(LoginRepository.getInstance(new LoginDataSource()));
        } else {
            throw new IllegalArgumentException("Unknown ViewModel class");
        }
    }
}

LiveData

在数据发生变化时,需要通知给页面。通常可以采用接口的方工去做,但如果要观察的数据很多,就需要定义大量的接口,代码会十分冗余。为此, Google提供了LiveData组件,它是一个可被观察的数据容器类,将数据包装起来,使数据成为被观察者,当该数据发生变化时,观察者能获得通知。

ViewModel是用来存储数据,LiveData的作用是在ViewModel发生变化时通知页面。因此, LiveData通常放在ViewModel中使用,用于包装ViewModel中那些需要被外界观察的数据。

我们来结合具体的例子(登录)看这三者的配合使用

示例

UI层面(LoginActivity)

 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = ActivityLoginBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());
    ...
    viewModel = new ViewModelProvider(this, new LoginViewModelFactory()).get(LoginViewModel.class);
    registerObserver();
    bindClickEvent();
    
}

private void registerObserver() {
    viewModel.getLoginResult().observe(this, new Observer<LoginResult>() {
        @Override
        public void onChanged(LoginResult loginResult) {
            if (loginResult.success) {
               //登录成功
            } 
        }
    });
    //出现异常
    viewModel.exceptionLiveData.observe(this, new Observer<AppException>() {
        @Override
        public void onChanged(AppException e) {
            Toast.makeText(LoginActivity.this, e.getBizMsg(), Toast.LENGTH_SHORT).show();
        }
    });

}

private void bindClickEvent() {
    binding.btnLogin.setOnClickListener(v-> {
        viewModel.login(phone, pwd, verifyCode, verifyKey);
    });
}
  • 采用ViewBinding进行资源的绑定
  • 绑定ViewModel
  • ViewModel监听数据的变化
  • 利用ViewModel去访问数据

ViewModel

public class LoginViewModel extends BaseViewModel {
    ...
    private MutableLiveData<LoginResult> loginResult = new MutableLiveData<>();

    private LoginRepository loginRepository;

    LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public LiveData<LoginResult> getLoginResult() {
        return loginResult;
    }

    public void login(String username, String password, String verifyCode, String verifyKey) {
        loginRepository.login(username, password, verifyCode, verifyKey, new Gson2ModelCallback<NetResult<LoginNetResponse>>() {

            @Override
            public void onSuccess(NetResult<LoginNetResponse> result) {
                //成功
                if ("SUCCESS".equals(result.getCode())) {                  
                    loginResult.postValue(new LoginResult(result.getData()));
                }
            }

            @Override
            public void onFail(Throwable e) {
                handleException(e);
            }
        });
    }
}

其功能是调用repository相关的业务方法去拿数据,拿到数据后通过postValue方法再传递给ViewModel

Repository

Repository层便会调用具体的数据源去加载数据,这里就不详情说明。Repository在创建ViewModel的时候就已经注入了。

至此,该方案主体结构基本描述完整。至于里面再细节一些的方案,就根据具体的项目实际情况来安排。比如,还要不要用Rxjava, 要不要引入Retrofit, 不同模块的开发负责人可以自己安排,以模块的粒度来组织代码,就再不用担心别人模块中的乱代码恶心到你了,你可以在自己负责的模块中享有最大的技术自由度。哪怕你后面在自己负责的模块中就不想用mvvm架构,你的设计思路影响的范围也只是在自己负责的模块,项目中的其它模块还是依旧保持整洁一致的结构。

参考