Android快速搭建MVVM框架

 

  • 架构

上面是从一个开源项目中了解到的框架结构,以最简洁的方式搭建一个app的基础框架。

框架的几个特点是:

  1. 通过Jetpack的Navigation构建单Activity多Fragment结构,我们知道Activity是属于比较重的组件,而Fragment是比较轻量化的,因此这种结构对界面性能方面有很大影响
  2. 通过koin这个依赖注入框架来管理ViewModel等实例的生命周期,早期的SSH框架也是因为Spring这个依赖注入特性而更加出名
  3. 使用当前比较优秀的数据请求框架来负责各种类型数据的处理
  4. 麻雀虽小,五脏俱全,任何一个app都离不开这些基础的架构,而上面的框架搭建起来很简洁,后期维护也很清晰

 

  • 具体剖析

一、Navigation

简介:

Navigation是Jetpack四大组件中的其中一个,目前也比较稳定了

我们都知道fragment有非常多的优势,它本身是一个VIew派生而来的控件,嵌套灵活,渲染所消耗的资源明显小于activity,数据的传递也更加方便,当然它的优点并不止这些。

但是在应用开发的过程中,开发者们也发现了不少这种做法带来的坑。例如需要维护复杂的fragment回退栈、使用不当的情况下经常出现fragment重叠、经常由于activity已经销毁导致使用上下文crash、等等等等的问题。

navigation就是为了解决这些问题而出现的,用于实现单activity多fragment形式的官方解决方案

使用样例:

1)先配置跳转信息,在res/navigation目录下新建一个navigation.xml,配置如下内容:

<?xml version="1.0" encoding="utf-8"?>

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@+id/tabFragment">

    <fragment
        android:id="@+id/tabFragment"
        android:name="luyao.wanandroid.ui.TabFragment"
        android:label="fragment_tab"
        tools:layout="activity_bottom_navigation">
        <action
            android:id="@+id/action_tab_to_browser"
            app:destination="@id/browserActivity"/>
    </fragment>


    <activity
        android:id="@+id/browserActivity"
        android:name="luyao.wanandroid.ui.BrowserActivity"
        android:label="activity_browser"
        tools:layout="@layout/activity_browser">
        <argument android:name="url"
            app:argType="string"
            android:defaultValue="www.wanandroid.com"/>
    </activity>

</navigation>

上面fragment和activity标签就是代表需要跳转的具体类,action标签代表一个具体的跳转信息,argument代表的是跳转到这个类时可以传递的参数定义

2)界面跳转,比如上面的TabFragment跳转到BrowserActivity时可以这样操作:

Navigation.findNavController(homeRecycleView).navigate(TabFragmentDirections.actionTabToBrowser().setUrl("http://www.baidu.com"))

而BrowserActivity里面只要两行代码就能获取到参数:

val args by navArgs<BrowserActivityArgs>()
val url = args.url

要使用上面的argument必须在gradle里面引入safeArgs相关依赖,如下:

1)App的build.gradle文件添加:

apply plugin: 'androidx.navigation.safeargs'

2)Project的build.gradle文件中添加:

dependencies {
        classpath 'com.android.tools.build:gradle:3.6.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.2.1"
    }

当然也可以不使用argument标签来进行参数传递,不过这个标签的好处就是对类型做了限定,所以也是safe argument的由来,个人感觉另一个好处就是每个界面传递的参数一目了然,不会漏掉或者传错

findNavController传入的参数可以是Activity或者View,最终逻辑都是寻找到NavHostFragment,然后获取它的mNavController,这样做得好处是我们只要传递给它一个view就能进行跳转了,源码如下:

private static NavController findViewNavController(@NonNull View view) {
        while (view != null) {
            NavController controller = getViewNavController(view);
            if (controller != null) {
                return controller;
            }
            ViewParent parent = view.getParent();
            view = parent instanceof View ? (View) parent : null;
        }

        return null;
}

从上面大概可以了解到使用Navigation进行fragment管理的好处不仅是对各种异常情况的处理,代码也会简洁很多,而且参数传递也多了一些特性

 

二、koin框架

简介:

Koin框架,适用于使用Kotlin开发 ,是一款轻量级的依赖注入框架,无代理,无代码生成,无反射。相对于dagger 而言更加适合Kotlin语言

使用样例:

1)app的build.gradle中引入依赖:

dependencies {
    // Koin for Android
    implementation 'org.koin:koin-android:2.0.1'
    // or Koin for Lifecycle scoping
    implementation 'org.koin:koin-androidx-scope:2.0.1'
    // or Koin for Android Architecture ViewModel
    implementation 'org.koin:koin-androidx-viewmodel:2.0.1'

}

2)初始化,在Application onCreate中注册组件:

override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@App)
            //注册组件
            modules(appModule)
        }
   }

3)module定义:

val viewModelModule = module {
    viewModel { LoginViewModel(get(),get()) }
   }

val repositoryModule = module {
   single { SquareRepository() }
   single { HomeRepository() }
   single<Service> { ServiceImpl1() }
   single<Service>(named(name = "test")) { ServiceImpl2() }
   single{ (view : View) -> Presenter(view) }
}

val appModule = listOf(viewModelModule, repositoryModule)

module定义的原理其实就是注册类的定义,这样在依赖注入的时候才能根据你要的类型来构建对应的实例

4)依赖注入:

val service : Service by inject() //默认注入的是 ServiceImpl1
val service : Service by inject(name = "test") //注入的是ServiceImpl2
val presenter : Presenter by inject { parametersOf(view) }
val loginViewModel:LoginViewModel by viewModel()

上面的依赖注入by inject是koin框架会根据注册类的定义构建一个实例,by viewModel()比较特殊,因为viewModel是和activity或者fragment的生命周期绑定的,所以这边注入也是注入到当前的fragment或者activity,可以看段代码:

fun <T : ViewModel> Koin.getViewModel(parameters: ViewModelParameters<T>): T {
    val vmStore: ViewModelStore = parameters.owner.getViewModelStore(parameters)
    val viewModelProvider = rootScope.createViewModelProvider(vmStore, parameters)
    return viewModelProvider.getInstance(parameters)
}


fun <T : ViewModel> LifecycleOwner.getViewModelStore(
        parameters: ViewModelParameters<T>
): ViewModelStore =
     when {
            parameters.from != null -> parameters.from.invoke().viewModelStore
            this is FragmentActivity -> this.viewModelStore
            this is Fragment -> this.viewModelStore
            else -> error("Can't getByClass ViewModel '${parameters.clazz}' on $this - Is not a FragmentActivity nor a Fragment neither a valid ViewModelStoreOwner")

     }

从上面可以看到创建的viewModel会绑定到当前的viewModelStore,这个也是真正做到依赖注入对创建对象的生命周期管理作用

相比dagger框架,koin框架不需要对注入对象手动调用注入,因为它创建的对象不是全局的,而是和当前对象绑定的,也就不需要等待注入参数准备好后再进行构建,特别如果注入对象里面还有注入对象,手动注入就会变得混乱

 

三、Retrofit2

简介:

Retrofit2简单的说就是一个网络请求的适配器,它将一个基本的Java接口通过动态代理的方式翻译成一个HTTP请求,并通过OkHttp去发送请求。此外它还具有强大的可扩展性,支持各种格式转换以及RxJava

使用样例:

1)创建interface 服务接口:

public interface IWeather {
     @GET("/v3/weather/now.json")
     Call<WeatherBean> weather(@Query("key")String key,@Query("location")String location);

 
     @FormUrlEncoded
     /* @FormUrlEncoded注解,表示以表单键值对形式传递,方法内部的参数以@Field标记,注解内的是key值,而传递的形参是value值 */
     @POST("/article/query/{page}/json")
     WanResponse<ArticleList> searchHot(@Path("page") int page, @Field("k") String key)

     @POST("users/new")
     Call<User> createUser(@Body User user);

     //QueryMap可以实现将参数统一放到Map里面,减少参数定义
     @GET("/v3/weather/now.json")
     Call<WeatherBean> weather(@QueryMap Map<String,String> key,@QueryMap Map<String,String> location);

}

Retrofit2要求我们创建如上面所示的interface接口,而创建该接口的目的是,retrofit通过获取接口的@GET注解里面的值,与下面即将讲到的baseUrl拼接成一个请求网址,另外通过调用接口的方法,填充相应参数之类的

2)创建Retrofit:

Retrofit retrofit2 = new Retrofit.Builder()
        .baseUrl("https://api.thinkpage.cn")
        .addConverterFactory(GsonConverterFactory.create())
        .client(new OkHttpClient())
        .build();

IWeather iWeather = retrofit2.create(IWeather.class);

通过Retrofit.Builder()方法来创建一个Retrofit实例,baseUrl()是设置Url,这是必须的,addConverterFactory()该方法是设置解析器,即上面提到的GsonConverterFactory,最后通过build()完成创建

3)创建请求,设置请求参数,执行请求:

Call<WeatherBean> call = iWeather.weather("rot2enzrehaztkdk","beijing");
call.enqueue(new Callback<WeatherBean>() {
      @Override
      public void onResponse(Call<WeatherBean> call, Response<WeatherBean> response) {
         WeatherBean weatherBean = response.body();
         Log.d("cylog",weatherBean.results.get(0).now.temperature+"");
      }

      @Override
      public void onFailure(Call<WeatherBean> call, Throwable t) {
        Log.d("cylog", "Error" + t.toString());
      }
 });

通过调用IWeather的weather方法(我们在接口中定义的),把两个关键参数传递了进入,这两个参数均是使用@Query注解标记的,因此构成了url中的请求参数,而返回的call则是我们的请求。最后,调用call.enqueue方法,执行一个异步请求,如果成功了,则回调onResponse方法,否则回调onFailure方法。另外,这里补充一下:call.enqueue是一个异步方法,不在同一线程内,而call.execute是一个同步方法,在同一线程内

4) 上传文件

@Multipart
@PUT("user/photo") 
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

@Multipart表示能使用多个Part,而@Part注解则是对参数进行标记,RequestBody是一种类型,是okHttp3里面的一个类,既然请求参数是RequestBody类型的,那么我们要把请求体封装到RequestBody里面去,通过RequestBody.creat()方法进行创建,RequestBody创建有两个参数,第一个参数是MediaType,是媒体类型,第二个参数可为String、byte、file等,通过上述方法创建的RequestBody是一个请求体,将与其他的请求体一起发送到服务端,它们的key值是@Part("key")注解的值

Retrofit2的好处就是对各种请求的封装,这样代码写起来就简洁很多,还有一个特性是比较符合HTTP2.0多路复用,多路复用正是同一个域名下的请求可以共用一个连接,这与Retrofit2的定义刚好不谋而合

 

四、WorkManager

简介:

WorkManager 在工作的触发器 满足时, 运行可推迟的后台工作。WorkManager会根据设备API的情况,自动选用JobScheduler, 或是AlarmManager来实现后台任务,WorkManager里面的任务在应用退出之后还可以继续执行,这个技术适用于在应用退出之后任务还需要继续执行的需求,对于在应用退出的之后任务也需要终止的需求,可以选择ThreadPool、AsyncTask

使用样例:

1)使用状态机

val request1 = OneTimeWorkRequestBuilder<MyWorker>().build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>().build()
val request3 = OneTimeWorkRequestBuilder<MyWorker>().build()

WorkManager.getInstance().beginWith(request1)
        .then(request2)
        .then(request3)
        .enqueue()

2)设置约束条件:     

val myConstraints = Constraints.Builder()
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}运行时设备是否为空闲
        .setRequiresCharging(true)//指定要运行的{@link WorkRequest}是否应该插入设备
        .setRequiredNetworkType(NetworkType.NOT_ROAMING)
        .setRequiresBatteryNotLow(true)//指定设备电池是否不应低于临界阈值
        .setRequiresCharging(true)//网络状态
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}运行时设备是否为空闲
        .setRequiresStorageNotLow(true)//指定设备可用存储是否不应低于临界阈值
        .addContentUriTrigger(myUri,false)//指定内容{@link android.net.Uri}时是否应该运行{@link WorkRequest}更新
        .build()

val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS)
        .setConstraints(myConstraints)//注意看这里!!!
        .build()

3)加入队列后监听任务状态:  

val liveData: LiveData<WorkStatus> =WorkManager.getInstance().getStatusById(request.id)

    public final class WorkStatus {   
      private @NonNull UUID mId;  
      private @NonNull State mState;   
      private @NonNull Data mOutputData;   
      private @NonNull Set<String> mTags;   

      public WorkStatus(
            @NonNull UUID id,
            @NonNull State state,
            @NonNull Data outputData,
            @NonNull List<String> tags) {

        mId = id;
        mState = state;
        mOutputData = outputData;
        mTags = new HashSet<>(tags);
    }

    public enum State {
      ENQUEUED,//已加入队列
      RUNNING,//运行中
      SUCCEEDED,//已成功
      FAILED,//已失败
      BLOCKED,//已刮起
      CANCELLED;//已取消


      public boolean isFinished() {
        return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
      }

   }

4)combine 操作符-组合

现在我们有个复杂的需求:共有A、B、C、D、E这五个任务,要求 AB 串行,CD 串行,但两个串之间要并发,并且最后要把两个串的结果汇总到E,代码如下:  

   val chuan1 = WorkManager.getInstance()
    .beginWith(A)
    .then(B)

   val chuan2 = WorkManager.getInstance()
   .beginWith(C)
   .then(D)

   WorkContinuation
    .combine(chuan1, chuan2)
    .then(E)
    .enqueue()

使用WorkManager的好处就是对android各种API的策略做了适配,特别目前android对后台执行任务的限制越来越厉害,app需要做很多处理来适配各个版本,不仅代码逻辑复杂,效果也不能做到非常好。不过目前WorkManager还处于试验阶段,可以等它稳定后再引入

 

五、kotlin suspendCoroutine

kotlin的一大特色就是协程,其中一个作用就是将异步回调写成同步方式,这里就用到了suspendCoroutine,它可以挂起当前协程而不阻塞线程,这样就能等待异步回调返回前挂起当前协程,比如想获取camera实例,正常是监听camera打开的回调来获取,这样写逻辑就比较复杂,但是用suspend fun可以实现没有线程阻塞的执行暂停(suspend只能在协程里面调用,注册回调后就结束,只是挂起当前协程,不会阻塞线程,影响其他协程运行),直到调用resume方法返回结果,这样就能等待camera实例返回再继续执行,代码如下:   

 private suspend fun openCamera(
            manager: CameraManager,
            cameraId: String,
            handler: Handler? = null
    ): CameraDevice = suspendCancellableCoroutine { cont ->
        manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
            override fun onOpened(device: CameraDevice) = cont.resume(device)
            override fun onDisconnected(device: CameraDevice) {
                Log.w(TAG, "Camera $cameraId has been disconnected")
                requireActivity().finish()
            }

            override fun onError(device: CameraDevice, error: Int) {
                val msg = when(error) {
                    ERROR_CAMERA_DEVICE -> "Fatal (device)"
                    ERROR_CAMERA_DISABLED -> "Device policy"
                    ERROR_CAMERA_IN_USE -> "Camera in use"
                    ERROR_CAMERA_SERVICE -> "Fatal (service)"
                    ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
                    else -> "Unknown"
                }
                val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
                Log.e(TAG, exc.message, exc)
                if (cont.isActive) cont.resumeWithException(exc)
            }

        }, handler)
    }

获取camera实例代码:    

private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
        // Open the selected camera
        camera = openCamera(cameraManager, args.cameraId, cameraHandler)
        //use camera..
     }

suspend fun可以像上面直接返回结果,也可以使用use{result -> }来返回,前者是遇到异常直接抛出,没有处理就会崩溃,后者是try-catch形式,不会直接崩溃,适用于直接跳过异常情况

 

  • 总结

经过上面对一些主要用到的框架和组件的介绍,我们基本可以了解到它们的主要作用,使用这些组合可以很快的搭建一个app的框架并且可以适配android各种版本的差异,并且后期维护也会更简单高效些

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章