架构
上面是从一个开源项目中了解到的框架结构,以最简洁的方式搭建一个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,配置如下内容:
上面fragment和activity标签就是代表需要跳转的具体类,action标签代表一个具体的跳转信息,argument代表的是跳转到这个类时可以传递的参数定义
2)界面跳转,比如上面的TabFragment跳转到BrowserActivity时可以这样操作:
Navigation.findNavController(homeRecycleView).navigate(TabFragmentDirections.actionTabToBrowser().setUrl("http://www.baidu.com"))
而BrowserActivity里面只要两行代码就能获取到参数:
val args by navArgs()
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 { ServiceImpl1() }
single(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 Koin.getViewModel(parameters: ViewModelParameters): T {
val vmStore: ViewModelStore = parameters.owner.getViewModelStore(parameters)
val viewModelProvider = rootScope.createViewModelProvider(vmStore, parameters)
return viewModelProvider.getInstance(parameters)
}
fun LifecycleOwner.getViewModelStore(
parameters: ViewModelParameters
): 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 weather(@Query("key")String key,@Query("location")String location);
@FormUrlEncoded
@POST("/article/query/{page}/json")
WanResponse searchHot(@Path("page") int page, @Field("k") String key)
@POST("users/new")
Call createUser(@Body User user);
//QueryMap可以实现将参数统一放到Map里面,减少参数定义
@GET("/v3/weather/now.json")
Call weather(@QueryMap Map key,@QueryMap Map 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 call = iWeather.weather("rot2enzrehaztkdk","beijing");
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
WeatherBean weatherBean = response.body();
Log.d("cylog",weatherBean.results.get(0).now.temperature+"");
}
@Override
public void onFailure(Call 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 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().build()
val request2 = OneTimeWorkRequestBuilder().build()
val request3 = OneTimeWorkRequestBuilder().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(24,TimeUnit.SECONDS)
.setConstraints(myConstraints)//注意看这里!!!
.build()
3)加入队列后监听任务状态:
val liveData: LiveData =WorkManager.getInstance().getStatusById(request.id)
public final class WorkStatus {
private @NonNull UUID mId;
private @NonNull State mState;
private @NonNull Data mOutputData;
private @NonNull Set mTags;
public WorkStatus(
@NonNull UUID id,
@NonNull State state,
@NonNull Data outputData,
@NonNull List 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各种版本的差异,并且后期维护也会更简单高效些