轻松搭建基于JetPack组件的MVVM框架

5,285 阅读5分钟

Brick

github gitee

介绍

辅助android开发者搭建基于JetPack组件构建MVVM框架的注解处理框架。通过注解自动生成ViewModel的Factory类、lazy方法等;支持在项目的任意位置注入ROOM的dao层接口与Retrofit库中的api接口。

特点

android开发者可以将brick理解为一个轻量级的注入框架,使用非常简单,使用4-6个注解即可工作。brick主要在编译期工作, 不会在App运行时产生任何额外的性能消耗 ,并且只有1个注解库会打包到你的android工程中,不用担心体积增大的问题。

适用范围

  1. 使用androidx而非support库。
  2. 使用JetPackViewModel组件。
  3. 使用Retrofit作为网络请求库。
  4. 使用ROOM数据库框架。(可选)
  5. 服务端为多端口、多IP的项目。(可选)

引入

  1. 在你的android工程的根目录下的build.gradle文件中的适当的位置添加以下代码:
buildscript {
    ...
    ext {
        brick_version = '0.2.0'
    }
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath "com.gitee.numeron.brick:plugin:$brick_version"
    }
}

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}
  1. 在你的android工程中要启用brick的android模块的build.gradle文件中的适当位置添加以下代码:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'brick'
...
dependencies {
    ...
    implementation "com.gitee.numeron.brick:annotation:$brick_version"
    kapt "com.gitee.numeron.brick:compiler:$brick_version"
}

使用

一、 @Provide注解的使用方法:

  1. 在你编写好的ViewModel子类上添加@Provide注解
@Provide
class WxAuthorViewModel: ViewModel() {
    ...
}
  1. 有3种方式让brick注解处理器开始工作:
  • 在Terminal终端上输入gradlew :[ModuleName]:kaptDebugKotlin运行脚本;
  • 在AndroidStudio右侧Gradle扩展栏中依次找到[PrjectName] -> [ModuneName] -> Tasks -> other -> kaptDebugKotlin并双击运行脚本;
  • Ctrl + F9编译整个项目。
    以上三种方式任选其一即可运行brick注解处理器。
  1. 脚本运行结束后,会生成两个包级方法:
  • lazyWxAuthorViewModel()扩展方法,在Activity或Fragment中直接调用即可。
  • get()方法,在不方便使用lazy方法时,可使用此方法获取ViewModel的实例。
    注:lazyWxAuthorViewModel方法就是对get()方法的包装。
    直接使用生成的方法,即可创建对应的ViewModel实例:
private val wxAuthorViewModel by lazyWxAuthorViewModel()

或在onCreate()之后,通过get创建:

private lateinit var wxAuthorViewModel: WxAuthorViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    wxAuthorViewModel = get(this)
}

二、 @Inject注解的使用方法

-2. (必需) 在获取Retrofit实例的方法上添加@RetrofitInstance,如:

@RetrofitInstance
val retrofit: Retrofit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(WANDROID_BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
}

val okHttpClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    val logInterceptor = HttpLoggingInterceptor()
    logInterceptor.level = HttpLoggingInterceptor.Level.BODY
    OkHttpClient.Builder()
        .addInterceptor(logInterceptor)
        .callTimeout(15, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(60, TimeUnit.SECONDS)
        .connectTimeout(15, TimeUnit.SECONDS)
        .build()
}

注:@RetrofitInstance注解只能标记在public修饰的val属性上或方法上,val属性上或方法可以在object 单例companion object中,也可以是包级属性/方法。

-1. (可选) 在获取RoomDatabase实例的属性或方法上标记@RoomInstance,如:

@RoomInstance
val wandroidDatabase: WandroidDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    Room.databaseBuilder(CONTEXT, WandroidDatabase::class.java, "wandroid.db")
        .build()
}

注:@RoomInstance注解只能标记在public修饰的val属性上或方法上,val属性上或方法可以在object 单例companion object中,也可以是包级属性/方法。

  1. 假设已有Retrofit Api接口和WxAuthorRepo
interface WxAuthorApi {
    @GET("wxarticle/chapters/json  ")
    suspend fun getWxAuthorList(): List<WxAuthor>
}

class WxAuthorRepo {
    ...
}

  1. 在WxAuthorRepo中添加lateinit var修饰的WxAuthorApi字段,并用@Inject标记:
class WxAuthorRepo {

    @Inject
    lateinit var wxAuthorApi: WxAuthorApi

}
  1. 在ViewModel中创建lateinit var修饰的WxAuthorRepo字段,并用@Inject标记:
@Provide
class WxAuthorViewModel: ViewModel() {
    @Inject
    private lateinit var wxAuthorRepo: WxAuthorRepo
}

标记后,继续编写业务代码即可,所有被@Inject标记的字段,都会在编译期自动获取或创建实例,无需担心它们在何时被赋值。
注:虽然是lateinit var修饰的字段,但是不要尝试为它们赋值,这会导致致命的错误。
注:@Inject可以注入的类型只有Retrofitapi接口和ROOMdao接口、以及有无参构造的类。

三、 多服务器或多端口的处理方法:

假设有另一个Retrofit api接口,它的访问地址或端口与baseUrl中的不一样,此时,可以在Retrofitapi接口上添加@Port@Url注解来设置它们的url或port。

  1. @Port的使用:
@Port(1080)
interface ArticleApi {

    @GET("wxarticle/list/{chapterId}/{page}/json")
    suspend fun getArticleList(@Path("chapterId") chapterId: Int, @Path("page") page: Int): Paged<Article>

}

添加此注解后,brick会在编译期根据@RetrofitInstance注解标记的Retrofit实例和@Port的端口号,重新创建一个Retrofit实例,并使用新的Retrofit实例创建ArticleApi的实例。

  1. @Url的使用:
@Url("http://www.wanandroid.com:1080/")
interface ArticleApi {
    @GET("wxarticle/list/{chapterId}/{page}/json")
    suspend fun getArticleList(@Path("chapterId") chapterId: Int, @Path("page") page: Int): Paged<Article>
}

@Port的使用基本一致,实现的原理也是一样的。

附录1

生成的WxAuthorViewModels.kt文件:

//kotlin 扩展方法,在Activity/Fragment中通过by调用
fun ViewModelStoreOwner.lazyWxAuthorViewModel(): Lazy<WxAuthorViewModel> =
    LazyWeChatAuthorViewModel(this)

//包级方法,在Activity/Fragment的onCreate方法之后调用
fun get(owner: ViewModelStoreOwner): WxAuthorViewModel {
  val factory = WxAuthorViewModelFactory()
  return ViewModelProvider(owner, factory).get(WxAuthorViewModel::class.java)
}

private class WxAuthorViewModelFactory : ViewModelProvider.Factory {
  @Suppress("UNCHECKED_CAST")
  override fun <VM : ViewModel> create(clazz: Class<VM>): VM = WxAuthorViewModel() as VM
}

private class LazyWxAuthorViewModel(
  private val owner: ViewModelStoreOwner
) : Lazy<WxAuthorViewModel> {
  private var _value: WxAuthorViewModel? = null

  override val value: WxAuthorViewModel
    get() {
      if(_value == null) {
        _value = get(owner)
      }
      return _value!!
    }

  override fun isInitialized(): Boolean = _value != null
}

附录2

反编译后的WxAuthorViewModel.class:

class WxAuthorViewModel extends ViewModel {
	
    private final WxAuthorRepo wxAuthorRepo = new WxAuthorRepo();

}

附录3

反编译后的WxAuthorRepo.class:

class WxAuthorRepo {
	
    private final WxAuthorApi wxAuthorApi = RuntimeKt.getRetrofit().create(WxAuthorApi.class);
    
    public final WxAuthorApi getWxAuthorApi() {
    	...
    	return wxAuthorApi;
    }

}

附录4

WxAuthorApi添加@Port注解后的WxAuthorRepo.class:

class WxAuthorRepo {
    
    private final WxAuthorApi wxAuthorApi = newRetrofit(RuntimeKt.getRetrofit(), 1080, null).create(WxAuthorApi.class);
    
    public final WxAuthorApi getWxAuthorApi() {
        ...
        return wxAuthorApi;
    }

    private final Retrofit newRetrofit(Retrofit retrofit, int port, String url) {
      if (port > 0) {
         HttpUrl httpUrl = retrofit.baseUrl().newBuilder().port(port).build();
         return retrofit.newBuilder().baseUrl(httpUrl).build();
      } else if(url != null && url.length() != 0) {
         return retrofit.newBuilder().baseUrl(url).build();
      }
       return retrofit;
   }

}

总结

通过反编译class后的代码以及整篇文章后可以得出一个大概的结论:brick就是在java编译成class后,class编译成dex之前,对class的字节码进行修改,给@Inject标记的字段赋值,实现的注入框架。 目前对ViewModel的注入还需要手动调用生成的方法来初始化,这在编译代码之前,AS上都会有红色的错误标记,接下来,让@Inject支持ViewModel的创建就是主要工作啦,等完成后,再发文章吧。