暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Android快速搭建MVVM

周末随心分享 2021-08-07
785

背景

我们平时开发应用程序基本都会用到一些架构,主要目的要么为了解耦方便后续开发,要么为了更好进行扩展,特别服务端对架构的要求非常高,不仅考虑结构是否合理,也要兼顾性能等。那对于一个普通应用程序我们有没有一种快速尝试架构体验的方式呢?答案是肯定的,今天就来介绍一种基于Jectpack的MVVM架构设计


架构实践

一、整体架构设计

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

  • 通过Jetpack的Navigation构建单Activity多Fragment结构,我们知道Activity是属于比较重的组件,而Fragment是比较轻量化的,因此这种结构对界面性能方面有很大影响

  • 通过koin这个依赖注入框架来管理ViewModel等实例的生命周期,早期的SSH框架也是因为Spring这个依赖注入特性而更加出名

  • 使用当前比较流行的数据请求框架来负责各种类型数据的处理

  • 麻雀虽小,五脏俱全,任何一个app都离不开这些基础的架构,而上面的框架搭建起来很简洁,后期维护也很清晰


二、具体分析

1、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相关依赖,如下:

        App的build.gradle文件添加:

          apply plugin: 'androidx.navigation.safeargs'

          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管理的好处不仅是对各种异常情况的处理,代码也会简洁很多,而且参数传递也多了一些特性


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


                        3、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的定义刚好不谋而合


                                4、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还处于试验阶段,可以等它稳定后再引入


                                        5、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形式,不会直接崩溃,适用于直接跳过异常情况



                                            总结

                                            经过上面对基于Jetpack搭建的MVVM每个主要模块的介绍,小伙伴们应该对整体有了大致了解,大家也可以试着使用这些组合快速搭建一个app框架,这个框架还可以适配Android各种版本的差异,并且后期维护也会更简单高效些


                                            好了,今天的分享就到这了,下周继续~

                                            文章转载自周末随心分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                            评论