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各種版本的差異,並且後期維護也會更簡單高效些

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