作为一名安卓应用开发者来说,构建出稳定、高效、合理、易维护的安卓App一直都是我们追求的目标。对此老大哥谷歌给我们做出了很好的指导。废话不多说,先来了解一下官方对构建一个高质量的APP提出的建议。
- 整体框架图
可以看出采用的是MVVM的构架模式,Repository及其分支是数据中心(Model),Activity/Fragment是(View),ViewModel就是图中的ViewModel。数据中心采用Retrofit进行网络请求,Room来操作数据库做数据持久化。而ViewModel采用具有生命感知力的LiveData来刷新View层。每个模块分工明确、各施其职,符合官方提出的两大原则:1.关注点分离;2.持久化的Model驱动UI。所以我想我们的项目目录结构应该是这样的
我多了一个di目录这是Dagger2依赖注入,我是一个爱动手不爱说话的人,接下来我会记录下我是怎么一步一步实现这个框架的。
- 实战操作
-
在项目中新建一个versions.gradle文件用来管理用到的Java库,内容如下
-
/** * Shared file between builds so that they can all use the same dependencies and * maven repositories. **/ ext.deps = [:] def versions = [:] versions.arch_core = "2.0.0" versions.room = "2.1.0-alpha03" versions.lifecycle = "2.0.0" versions.support = "1.0.0" versions.dagger = "2.16" versions.junit = "4.12" versions.espresso = "3.1.0-alpha4" versions.retrofit = "2.3.0" versions.okhttp_logging_interceptor = "3.9.0" versions.mockwebserver = "3.8.1" versions.apache_commons = "2.5" versions.mockito = "2.7.19" versions.mockito_all = "1.10.19" versions.mockito_android = "2.22.0" versions.dexmaker = "2.2.0" versions.constraint_layout = "2.0.0-alpha2" versions.glide = "4.8.0" versions.timber = "4.5.1" versions.android_gradle_plugin = '3.3.2' versions.rxjava2 = "2.1.3" versions.rx_android = "2.0.1" versions.atsl_runner = "1.1.0-alpha4" versions.atsl_rules = "1.1.0-alpha4" versions.hamcrest = "1.3" versions.kotlin = "1.3.0" versions.paging = "2.1.0-rc01" versions.navigation = "1.0.0-alpha08" versions.work = "1.0.0-alpha12" def deps = [:] def support = [:] support.annotations = "androidx.annotation:annotation:$versions.support" support.app_compat = "androidx.appcompat:appcompat:$versions.support" support.recyclerview = "androidx.recyclerview:recyclerview:$versions.support" support.cardview = "androidx.cardview:cardview:$versions.support" support.design = "com.google.android.material:material:$versions.support" support.v4 = "androidx.legacy:legacy-support-v4:$versions.support" support.core_utils = "androidx.legacy:legacy-support-core-utils:$versions.support" deps.support = support def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room def lifecycle = [:] lifecycle.runtime = "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle" lifecycle.extensions = "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" lifecycle.java8 = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle" lifecycle.compiler = "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle" deps.lifecycle = lifecycle def arch_core = [:] arch_core.testing = "androidx.arch.core:core-testing:$versions.arch_core" deps.arch_core = arch_core def retrofit = [:] retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit" retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit" retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit" deps.retrofit = retrofit deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}" def dagger = [:] dagger.runtime = "com.google.dagger:dagger:$versions.dagger" dagger.android = "com.google.dagger:dagger-android:$versions.dagger" dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger" dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger" dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger" deps.dagger = dagger def espresso = [:] espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso" espresso.contrib = "androidx.test.espresso:espresso-contrib:$versions.espresso" espresso.intents = "androidx.test.espresso:espresso-intents:$versions.espresso" deps.espresso = espresso def atsl = [:] atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_runner" deps.atsl = atsl def mockito = [:] mockito.core = "org.mockito:mockito-core:$versions.mockito" mockito.all = "org.mockito:mockito-all:$versions.mockito_all" mockito.android = "org.mockito:mockito-android:$versions.mockito_android" deps.mockito = mockito def kotlin = [:] kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin" kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" kotlin.allopen = "org.jetbrains.kotlin:kotlin-allopen:$versions.kotlin" deps.kotlin = kotlin deps.paging_ktx = "androidx.paging:paging-runtime-ktx:$versions.paging" def glide = [:] glide.runtime = "com.github.bumptech.glide:glide:$versions.glide" glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide" deps.glide = glide deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker" deps.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.constraint_layout" deps.timber = "com.jakewharton.timber:timber:$versions.timber" deps.junit = "junit:junit:$versions.junit" deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver" deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2" deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android" deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest" deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin" ext.deps = deps def build_versions = [:] build_versions.min_sdk = 23 build_versions.target_sdk = 28 build_versions.build_tools = "28.0.3" ext.build_versions = build_versions def work = [:] work.runtime = "android.arch.work:work-runtime:$versions.work" work.testing = "android.arch.work:work-testing:$versions.work" work.firebase = "android.arch.work:work-firebase:$versions.work" work.runtime_ktx = "android.arch.work:work-runtime-ktx:$versions.work" deps.work = work def navigation = [:] navigation.runtime = "android.arch.navigation:navigation-runtime:$versions.navigation" navigation.runtime_ktx = "android.arch.navigation:navigation-runtime-ktx:$versions.navigation" navigation.fragment = "android.arch.navigation:navigation-fragment:$versions.navigation" navigation.fragment_ktx = "android.arch.navigation:navigation-fragment-ktx:$versions.navigation" navigation.safe_args_plugin = "android.arch.navigation:navigation-safe-args-gradle-plugin:$versions.navigation" navigation.testing = "android.arch.navigation:navigation-testing:$versions.navigation" deps.navigation = navigation ext.deps = deps def addRepos(RepositoryHandler handler) { handler.google() handler.jcenter() handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } ext.addRepos = this.&addRepos
这里包含了几乎所有的谷歌开发库,都是亲儿子,不管用没用到直接复制过去就好了。
-
然后在build.gradle文件(project下的)写入以下内容
-
/* * Copyright (c) 2018. 代码著作权归卢声波所有。 */ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { apply from: 'versions.gradle' addRepos(repositories) dependencies { classpath deps.android_gradle_plugin // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } repositories { google() jcenter() } } allprojects { addRepos(repositories) } task clean(type: Delete) { delete rootProject.buildDir }
在App下的build.gradle文件中写入以下内容
-
apply plugin: 'com.android.application' android { compileSdkVersion build_versions.target_sdk buildToolsVersion build_versions.build_tools defaultConfig { applicationId "com.jinkan.www.fastandroid" minSdkVersion 15 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } dataBinding { enabled = true } compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { // Support libraries implementation deps.support.app_compat implementation deps.support.v4 implementation deps.support.design implementation deps.support.cardview implementation deps.constraint_layout // Architecture components implementation deps.lifecycle.runtime implementation deps.lifecycle.extensions annotationProcessor deps.lifecycle.compiler implementation deps.room.runtime implementation deps.paging_ktx annotationProcessor deps.room.compiler // Android Testing Support Library's runner and rules androidTestImplementation deps.atsl.runner androidTestImplementation deps.atsl.rules androidTestImplementation deps.room.testing androidTestImplementation deps.arch_core.testing //dagger2 implementation deps.dagger.runtime implementation deps.dagger.android implementation deps.dagger.android_support annotationProcessor deps.dagger.android_support_compiler annotationProcessor deps.dagger.compiler //Retrofit2 implementation deps.retrofit.runtime implementation deps.retrofit.gson implementation deps.okhttp_logging_interceptor // Espresso UI Testing androidTestImplementation deps.espresso.core androidTestImplementation deps.espresso.contrib androidTestImplementation deps.espresso.intents // Resolve conflicts between main and test APK: androidTestImplementation deps.support.annotations androidTestImplementation deps.support.v4 androidTestImplementation deps.support.app_compat androidTestImplementation deps.support.design implementation fileTree(include: ['*.jar'], dir: 'libs') }
其中 dataBinding { enabled = true }启用了dataBingding,使得ViewModel和View层的交互更加便捷高效。
-
接下来创建项目目录如图2,为了较少重复代码,一些基本的封装是必要的,下面就介绍我封装的一些基类。如果要使用dagger2来注入依赖的话,需要做一下封装方便使用。说明:为了文理的流畅性,这里只是简单的走下流程,我用到的组件都会写一篇相关的文章的,敬请期待。想了解细节的童鞋去这里看看:https://blog.youkuaiyun.com/qq_17766199/article/details/73030696
-
-
这里我写了四个类,第一个ActivityBindingModule的作用是指明要把什么(依赖)注入到哪个Activity的,代码如下:
-
package com.jinkan.www.fastandroid.di; import com.jinkan.www.fastandroid.view.MainActivity; import dagger.Module; import dagger.android.ContributesAndroidInjector; /** * We want Dagger.Android to create a Subcomponent which has a parent Component of whichever module ActivityBindingModule is on, * in our case that will be AppComponent. The beautiful part about this setup is that you never need to tell AppComponent that it is going to have all these subcomponents * nor do you need to tell these subcomponents that AppComponent exists. * We are also telling Dagger.Android that this generated SubComponent needs to include the specified modules and be aware of a scope annotation @ActivityScoped * When Dagger.Android annotation processor runs it will create 4 subcomponents for us. */ @Module public abstract class ActivityBindingModule { @ActivityScoped @ContributesAndroidInjector() abstract MainActivity mainActivity(); }
第二个类ActivityScoped是一个注解类,指明依赖注入的作用范围。
-
package com.jinkan.www.fastandroid.di; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.inject.Scope; /** * In Dagger, an unscoped component cannot depend on a scoped component. As * {@link AppComponent} is a scoped component {@code @Singleton}, we create a custom * scope to be used by all fragment components. Additionally, a component with a specific scope * cannot have a sub component with the same scope. */ @Documented @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScoped { }
这里指明是单例的。