-
架構
上面是從一個開源項目中瞭解到的框架結構,以最簡潔的方式搭建一個app的基礎框架。
框架的幾個特點是:
- 通過Jetpack的Navigation構建單Activity多Fragment結構,我們知道Activity是屬於比較重的組件,而Fragment是比較輕量化的,因此這種結構對界面性能方面有很大影響
- 通過koin這個依賴注入框架來管理ViewModel等實例的生命週期,早期的SSH框架也是因爲Spring這個依賴注入特性而更加出名
- 使用當前比較優秀的數據請求框架來負責各種類型數據的處理
- 麻雀雖小,五臟俱全,任何一個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各種版本的差異,並且後期維護也會更簡單高效些