前言
谈Android架构大家很容易想到MVC、MVP和MVVM。
1、MVC
首先分析一下上面各层之前对应的Android代码,layout.xml里面的xml文件就对应于MVC的view层,里面都是一些view的布局代码,而各种Java bean,还有一些类似repository类就对应于model层,至于controller层嘛,当然就是各种activity。理论上应该是这么分,但是实际开发中Activity实际也承担View的责任,这样就出了上面我提到Activity把该做的不该做的都做了的问题,最直接的表现就是Activity类的代码量庞大,逻辑不清晰,维护困难,单元测试也就很难进行(纯Java代码和Android代码杂糅在一起)。
2、MVP
最明显的差别就是view层和model层不再相互可知,完全的解耦,取而代之的presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层,整个过程中view层和model层完全没有联系。看到这里大家可能会问,虽然view层和model层解耦了,但是view层和presenter层不是耦合在一起了吗?其实不是的,对于view层和presenter层的通信,我们是可以通过接口实现的,具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的presenter中通过接口调用方法。不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。
谷歌给出Android架构演示项目:https://github.com/android/architecture-samples/tree/master
3、MVVM
根据Android应用架构指南可以看出google也是比较推荐使用MVVM架构,而且采用MVVM能够更好使用 Android Jetpack组件,具体事例可以看sunflower和architecture-components-samples。
上图显示了设计应用后所有模块应如何彼此交互,下面文章就围绕这个图展开。
正文
我这里用的是Dagger2,github地址:https://github.com/google/dagger
这里简单的介绍一下Dagger的依赖注入如何实现,然后再说说再Android项目中如何做。
Dagger2提供依赖的步骤:
步骤1:查找Module
中是否存在创建该类的方法。
步骤2:若存在创建类方法,查看该方法是否存在参数
步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数
步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
步骤3:若不存在创建类方法,则查找Inject
注解的构造函数,看构造函数是否存在参数
步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数
步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
有2种方法可以提供被依赖的对象,一种是使用@Inject注解,另一种是在Module中用@Provides注解。
@Inject提供被依赖对象
class Father @Inject constructor() {
val name = "老王"
}
@Component
interface FatherComponent {
fun inject(runner: Runner)
}
class Runner {
init {
DaggerFatherComponent.builder().build().inject(this)
}
@Inject
lateinit var father: Father
fun runner() {
println(father.name)
}
}
fun main() {
Runner().runner()
}
@Provides提供被依赖对象
class Father {
val name = "老王"
}
@Module
class FatherModule {
@Provides
fun providerFather() = Father()
}
@Component(modules = [FatherModule::class])
interface FatherComponent {
fun inject(runner: Runner)
}
class Runner {
init {
DaggerFatherComponent.builder().build().inject(this)
}
@Inject
lateinit var father: Father
fun runner() {
println(father.name)
}
}
fun main() {
Runner().runner()
}
@Component注解用来将被依赖的类和依赖的类连接起来。
将Dagger2用于Android需要增加:
implementation("com.google.dagger:dagger-android:2.x")
implementation("com.google.dagger:dagger-android-support:2.x")
kapt("com.google.dagger:dagger-compiler:2.x")
kapt("com.google.dagger:dagger-android-processor:2.x")
Dagger的文档上面给出比较详细的集成说明:戳这里
注入Activity对象
a、在您的 application component中添加AndroidInjectionModule,以确保这些基本类型所需要的所有绑定均可用。
@dagger.Component(
modules = {AndroidInjectionModule.class, MainActivity.Module.class, BuildModule.class})
/* @ApplicationScoped and/or @Singleton */
interface Component extends AndroidInjector {
@dagger.Component.Builder
abstract class Builder extends AndroidInjector.Builder {}
}
b、编写一个继承AndroidInjector
@Subcomponent(modules = ...)
public interface YourActivitySubcomponent extends AndroidInjector {
@Subcomponent.Factory
public interface Factory extends AndroidInjector.Factory {}
}
c、定义Subcomponect之后,通过定义的一个绑定subcomponect factory的Module将其添加到注入应用程序的component,进而添加到组件层次结构中。
@Module(subcomponents = YourActivitySubcomponent.class)
abstract class YourActivityModule {
@Binds
@IntoMap
@ClassKey(YourActivity.class)
abstract AndroidInjector.Factory>
bindYourAndroidInjectorFactory(YourActivitySubcomponent.Factory factory);
}
@Component(modules = {..., YourActivityModule.class})
interface YourApplicationComponent {}
tips:如果在你的Subcomponect和它的factory中没有步骤2中提到的方法(methods)和超类型(supertypes)以外的内容。则可以使用@ContributesAndroidInjector为您生成它们。代替步骤2和3,添加一个返回您的Activity的抽象模块方法,用@ContributesAndroidInjector对其进行注释,然后指定要安装到子组件中的模块。如果子组件需要范围(Scope),则也将范围注释应用于方法。
@ActivityScope
@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })
abstract YourActivity contributeYourAndroidInjector();
d、
如果你像我一样用的:
implementation("com.google.dagger:dagger-android:2.22.1")
implementation("com.google.dagger:dagger-android-support:2.22.1")
kapt("com.google.dagger:dagger-compiler:2.22.1")
kapt("com.google.dagger:dagger-android-processor:2.22.1")
使你的Application 实现HasActivityInjector接口,然后声明一个DispatchingAndroidInjector
public class YourApplication extends Application implements HasActivityInjector{
@Inject DispatchingAndroidInjector activityInjector;
@Override
public void onCreate() {
super.onCreate();
DaggerYourApplicationComponent.create()
.inject(this);
}
@Override
public AndroidInjector activityInjector() {
return activityInjector;
}
}
e、在您的Activity的onCreate()方法中在调用super.onCreate();之前调用AndroidInjection.inject(this);
public class YourActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
}
}
f、Congratulations!
注入Fragment对象
注入Fragment和注入Activity的方式差不多
不像Activity那样在onCreated注入,在Fragment中应该在onAttach()注入。
与为“Activity”定义的modules不同,您可以选择在何处安装Fragments的modules。您可以将Fragment组件(component)设为另一个Fragment组件(component),Activity组件(component)或Application组件(component)的子组件(subcomponent),这都取决于您的Fragment需要哪些其他的绑定。确定组件位置后,使相应的类型实现HasAndroidInjector(如果尚未实现)。例如,如果您的Fragment需要来自YourActivitySubcomponent的绑定,则您的代码将如下所示:
public class YourActivity extends Activity
implements HasAndroidInjector {
@Inject DispatchingAndroidInjector
注入ViewModel对象
a、自定义一个ViewModelProvider.Factory
@Singleton
class MvvmViewModelFactory @Inject constructor(
private val creators: Map, @JvmSuppressWildcards Provider>
) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
这段代码出处在这里
顺便贴一个翻译的Java代码:
@Singleton
public class MvvmViewModelFactory implements ViewModelProvider.Factory {
private final Map, Provider> creators;
@Inject
public MvvmViewModelFactory(Map, Provider> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@Override
public T create(Class modelClass) {
Provider extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry, Provider> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
这段代码出处在这里
也可以在这里找到
b、定义一个@module 标识ViewModelModule,并且把ViewModelModule绑定到AppComent,如:
@Suppress("unused")
@Module
abstract class ViewModelModule {
@Binds
abstract fun bindViewModelFactory(factory: MvvmViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(LoginViewModel::class)
abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel
}
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
MainActivityModule::class,
ViewModelModule::class
]
)
interface AppCompoent : AndroidInjector {
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: MvvmApplication): Builder
fun build(): AppCompoent
}
}
为了理解上面步骤说的内容多说几句:
①、subcomponen
subcomponent简单翻译就是子组件,详细了解可以戳这里,处理的场景就是@Component需要用另一个@Component提供依赖。
接着上面那个Father例子继续举例,现在Father有了一个Son,Son的实例化需要一个Father对象。
class Son(private val father: Father) {
val fatherName: String
get() = father.name
}
在讲subcomponent之前,先看看用dependencies怎么处理这种情况:
class Father {
val name = "老王"
}
@Module
class FatherModule {
@Provides
fun providerFather() = Father()
}
@Component(modules = [FatherModule::class])
interface FatherComponent {
fun offerFather(): Father
}
class Son(private val father: Father) {
val fatherName: String
get() = father.name
}
@Module
class SonModule {
@Provides
fun providerSon(father: Father) = Son(father)
}
@Component(modules = [SonModule::class], dependencies = [FatherComponent::class])
interface SonComponent {
fun inject(runner: Runner)
}
class Runner {
init {
DaggerSonComponent.builder().fatherComponent(DaggerFatherComponent.create()) .build().inject(this)
}
@Inject
lateinit var son: Son
fun runner() {
println(son.fatherName)
}
}
fun main() {
Runner().runner()
}
这个比较好理解,Son的实例化依赖于Father对象对应于SonComponent依赖于FatherComponent,用dependencies来表示。那实例化Son的需要的Father对象哪里来呢?所以FatherComponent需要一个抽象方法来提供Father对象。注入的时候先实例化FatherComponent然后再参数去实例化SonComponent进行注入。
可以不要一个抽象方法来提供Father对象么?
class Father {
val name = "老王"
}
@Module(subcomponents = [SonComponent::class])
class FatherModule {
@Provides
fun providerFather() = Father()
}
@Component(modules = [FatherModule::class])
interface FatherComponent {
fun buildChildComponent(): SonComponent.Builder
}
class Son(private val father: Father) {
val fatherName: String
get() = father.name
}
@Module
class SonModule {
@Provides
fun providerSon(father: Father) = Son(father)
}
@Subcomponent(modules = [SonModule::class])
interface SonComponent {
fun inject(runner: Runner)
@Subcomponent.Builder
interface Builder {
fun build(): SonComponent
}
}
class Runner {
init {
DaggerFatherComponent.create().buildChildComponent().build().inject(this)
}
@Inject
lateinit var son: Son
fun runner() {
println(son.fatherName)
}
}
fun main() {
Runner().runner()
}
subcomponent可以不在Component暴露依赖。
同样Son依赖Father,所以SonComponent的注解换成了Subcomponent,在FatherModule中被subcomponents引用,这样父类的依赖就全部暴露给了子类。我们还要在父类的Component中构建Subcomponent,所以在Subcomponent需要一个Builder
@Subcomponent(modules = [SonModule::class])
interface SonComponent {
fun inject(runner: Runner)
@Subcomponent.Builder
interface Builder {
fun build(): SonComponent
}
}
@Subcomponent.Builder表示是顶级@Subcomponent的内部类。
②、Subcomponent.Builde和Subcomponent.Factory
详细解释戳这里,这里我把文章的解释简单说一下
这里需要提到 @BindsInstance这个注解:将组件component builder上的方法或 component factory上的参数标记为将实例绑定到组件内的某些键。
@Component.Builder
interface Builder {
@BindsInstance Builder foo(Foo foo);
@BindsInstance Builder bar( @Blue Bar bar);
...
}
// or
@Component.Factory
interface Factory {
MyComponent newMyComponent(
@BindsInstance Foo foo,
@BindsInstance @Blue Bar bar);
}
Component.Factory带来了编译时的安全性:在以前,如果我们有多个构建器方法,可能会忘记调用其中一个方法并且仍然可以编译。现在总有一个方法,每当我们调用它时,我们都必须提供每个参数,因此再也不能忘记为组件提供强制性的依赖了。
③、IntoMap
这个注解很简单,想要理解可以戳这里
使用注入形式初始化 Map集合时,可以在 Module 中多次定义一系列返回值类型相同的方法:
@Provides
@IntoMap
@IntKey(0)
fun provideFish() = Animal("鱼")
@Provides
@IntoMap
@IntKey(1)
fun provideHuman() = Animal("人")
@IntKey
里面就是 Map 中的 key,providesXXX() 返回值是 key 对应的 value,如果 key 是 String 类型的,则使用@StringKey()
输入 key,此外,还可以自定义 key:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@MapKey
annotation class ZhuangBiKey(val f: Float)
因为是单Activity应该,所以页面管理理所应当采用的是Navigation。
Navigation的使用方法可以在https://developer.android.google.cn/guide/navigation/找到。
这里说说在官网找不到的。
a、闪屏页用Naviagtion怎么实现
方法一:Theme
当向用户显示初始屏幕达几秒钟时,通常会滥用初始屏幕,并且用户在已经可以与应用程序交互的同时浪费时间在初始屏幕上。取而代之的是,您应该尽快将它们带到可以与应用程序交互的屏幕。因此,以前的Splash屏幕在Android上被视为反模式。但是Google意识到,用户单击图标与您的第一个应用程序屏幕之间仍然存在短暂的窗口,可以进行交互,在此期间,您可以显示一些品牌信息。这是实现启动屏幕的正确方法。
因此,以正确的方式实施“启动画面”时,您不需要单独的“启动画面片段”,因为这会导致App加载过程中不必要的延迟。为此,您只需要特殊的主题。理论上讲,App主题可以应用于UI,并且比您的App UI初始化并变得可见的时间要早得多。简而言之,您只需要这样的SplashTheme即可:
splash_background:
-
-
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
.....
}
上面解决方案来自于:Navigation Architecture Component - Splash screen
方法二:popUpToInclusive
注意action的属性:
app:popUpTo="@id/splashFragment"
app:popUpToInclusive="true"
用代码也可以实现同样的效果:
navController.navigateAnimate(
SplashFragmentDirections.actionSplashFragmentToLoginFragment(),
navOptions {
popUpTo(R.id.splashFragment) {
inclusive = true
}
})
个人比较喜欢用代码实现。
解释一下:
/**
* Pop up to a given destination before navigating. This pops all non-matching destinations
* from the back stack until this destination is found.
*/
fun popUpTo(@IdRes id: Int, popUpToBuilder: PopUpToBuilder.() -> Unit) {
popUpTo = id
inclusive = PopUpToBuilder().apply(popUpToBuilder).inclusive
}
popUpTo: 导航之前,弹出至给定的目的地。这将从后堆栈中弹出所有不匹配的目标,直到找到该目标为止。
id:弹出目的地,清除所有中间目的地。
inclusive:如果为true,也会从后堆栈中弹出给定的目标,false不会
上面的解决方案来自于:Android Navigation Component Tips & Tricks — Implementing Splash screen
b、startActivityForResult用Navigation怎么实现
你在文档和官方demo中都找不到相关的内容,但是可以找到这么一句话:
通常,强烈建议您仅在目标之间传递最少的数据量。例如,您应该传递键来检索对象而不是传递对象本身,因为所有保存状态的总空间在Android上受到限制。如果需要传递大量数据,请考虑使用ViewModel,如在Fragments之间共享数据中所述。
Navigation推荐使用ViewModel在Fragment之间共享数据,这种方式在startActivityForResult并不友好。因此Google Issue Tracker有这么一个Issue:Navigation: startActivityForResult analog,但是它的优先级并不高。所以在官方给出解决方案之前我这有一种解决方式。
①、定义一个这样的接口
interface NavigationResult {
fun onNavigationResult(result: Bundle)
}
②、将下面的方法添加到您的Activity中
fun navigateBackWithResult(result: Bundle) {
val childFragmentManager =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.childFragmentManager
var backStackListener: FragmentManager.OnBackStackChangedListener by Delegates.notNull()
backStackListener = FragmentManager.OnBackStackChangedListener {
(childFragmentManager?.fragments?.get(0) as NavigationResult).onNavigationResult(result)
childFragmentManager.removeOnBackStackChangedListener(backStackListener)
}
childFragmentManager?.addOnBackStackChangedListener(backStackListener)
navController().popBackStack()
}
因为从另一个Fragment分发的结果必须要经过Activity路由。
③、在您要接受结果的Fragment中实现NavigationResult
上面的解决方法来自于Using Navigation Architecture Component in a large banking app。
c、Navigation页面切换Fragment的生命周期
说这个问题就得知道Navigation的页面是怎么切换的。看下面一段源码:
FragmentNavigator
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
...
//根据classname反射获取Fragmnent
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
//获取Fragment事务
final FragmentTransaction ft = mFragmentManager.beginTransaction();
//切换动画设置
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
//切换Fragment
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
......
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
........
}
答案揭晓了是通过replace进行页面切换的,并且加入回退栈。我看看replace操走了Fragment哪些生命周期。
Fragment生命周期的详细介绍:https://developer.android.google.cn/guide/components/fragments?hl=zh-CN
FragmentTransaction中的方法 | Fragment触发的生命周期函数 |
---|---|
add | onAttach-> onCreate-> onCreateView-> onActivityCreated-> onStart-> onResume |
remove | onPause-> onStop-> onDestoryView-> onDestory-> onDetach |
attach |
(调用attach之前需要先调用detach) |
detach | (在调用detach之前需要先通过add添加Fragment) onPause-> onStop-> onDestoryView |
replace | replace可拆分为add和remove |
hide | 不会触发任何生命周期函数 onHiddenChanged(boolean hidden) hidden为false |
show | 不会触发任何生命周期函数 onHiddenChanged(boolean hidden) hidden为true |
如果加入回退栈
replace:onPause->onStop->onDestroyView->新Fragment的add生命周期
点击返回:新Fragment的remove生命周期->onCreateView->onViewCreated->onActivityCreated->onStart->onResume 就是第一张图的线
上面就是Navigation的Fragment切换走的生命周期。我们发现replace会调用老Fragment的onDestroyView方法,返回时候会调用老Fragment的onCreateView,这样就会造成视图的销毁重建视图的编辑状态丢失。Google Issue Tracker也有一个Issue: Open fragment without lose the previous fragment states,还有一个Issue问是否可以开放api替换replace为add/hide: Transaction type is not available with Navigation Architecture Component,可以看到这个问题下面google工程师给出的回答是:Status: Won't Fix (Intended Behavior)。
那么这个问题真的没有解决方案么?最终我在Ian Lake(Android Toolkit Developer and Runner)的twitter下面找到了答案。关于这个问题的Twitter原文地址:https://twitter.com/ianhlake/status/1103522856535638016
不能打开的小伙伴我这个给译文:
Laxman(提问人):
当我们在当前Fragment的顶部添加新片段时,调用onDestroyView()是Jetpack Navigation中的唯一行为吗?或者有一些标记我们可以更改以避免从Fragment backStack还原片段视图时避免重新创建片段视图。
Ian Lake:
您不必每次调用onCreateView时都为新视图inflater-您可以保留对您第一次创建的View的引用,然后再次返回它。当然,对于不可见的内容,这会不断浪费内存和资源。保持数据>>您的视图
Laxman:
在不泄漏内存的情况下有任何好的模式吗?对我来说,我一直想保留的reference一直在泄漏。
Ian Lake:
确保您没有将setRetainInstance(true)与带有Views的Fragments一起使用,或者不在ViewModel中存储任何引用context的Views和things由于视图引用了旧的上下文,因此视图将永远无法幸免于configuration更改驱动的Activity 重启。
Laxman:
他们不需要在Activity重启后生存下来,而必须在Jetpack Navigation中生存下来(UseCase:当我们创建一个帖子并且用户试图标记好友并且将用户发送给TagFriendsFragment并返回时,我们应该能够保留视图)
Ian Lake:
请记住,即使不缓存视图本身,Fragment视图也会自动保存和恢复其状态。如果不是这种情况,则应首先解决该问题(确保视图具有android:id等)。否则,保留片段中的视图不是泄漏。
Krishna Sharma:
从其他生命周期方法(例如onViewCreated和onActivityCreated)进行的网络/数据库调用呢?我们是否需要保留另一个标志来避免在返回该片段时再次调用这些代码?
Ian Lake:
如果您使用的是LiveData或viewLifecycleOwner.lifecycleScope或launchWhenStarted(https://developer.android.google.cn/topic/libraries/architecture/coroutines#suspend),则可以为您解决。否则,只需检查一下您的视图是否为空。
总结一下上面的对话:
您不必每次调用onCreateView时都为新视图inflater-您可以保留对您第一次创建的View的引用,然后再次返回它。请记住,即使不缓存视图本身,Fragment视图也会自动保存和恢复其状态。如果不是这种情况,则应首先解决该问题(确保视图具有android:id等)
为什么要确保视图有id才能自动缓存视图?答案看这里
这个图也是来自于Android应用架构指南。
代码实现也可以在google的官方demo中找到:NetworkBoundResource
Java版:NetworkBoundResource
有代码我还讲什么?(⊙o⊙)…
我要讲的是Kotlin的协程的实现,因为我在网上并没有找到实现方式。参考文档在这里
本来想把代码贴在这儿的,但是代码比较长。就不贴了自己点击去看吧:BaseRepository.kt
为什么要用协程实现这个呢?因为Room 和 retrofit2-2.6.0都支持协程的支持用起来很方便
还有一个优点:liveData构建块用作协程和LiveData之间的结构化并发原语。当LiveData变为活动状态时,该代码块开始执行;当LiveData变为非活动状态时,该代码块在可配置的超时后自动取消。如果在完成之前将其取消,则如果LiveData再次变为活动状态,它将重新启动。如果它在先前的运行中成功完成,则不会重新启动。请注意,只有自动取消后,它才会重新启动。如果由于任何其他原因取消了该块(例如,引发CancelationException),则不会重新启动它。
文章涉及代码所在项目:https://github.com/Siy-Wu/mvvm_exm