Android的MVVM架构的单Activity应用实践
前言
谈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。
上图显示了设计应用后所有模块应如何彼此交互,下面文章就围绕这个图展开。
正文
1、管理组件之间的依赖关系
我这里用的是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)}@Injectlateinit var father: Fatherfun runner() {println(father.name)}
}fun main() {Runner().runner()
}
@Provides提供被依赖对象
class Father {val name = "老王"
}@Module
class FatherModule {@Providesfun providerFather() = Father()
}@Component(modules = [FatherModule::class])
interface FatherComponent {fun inject(runner: Runner)
}class Runner {init {DaggerFatherComponent.builder().build().inject(this)}@Injectlateinit var father: Fatherfun 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<SimpleApplication> {@dagger.Component.Builderabstract class Builder extends AndroidInjector.Builder<SimpleApplication> {}}
b、编写一个继承AndroidInjector <YourActivity>并且@Subcomponent标识的interface。interface带有一个继承AndroidInjector.Factory <YourActivity>的并且@ Subcomponent.Factory标识的interface:
@Subcomponent(modules = ...)
public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> {@Subcomponent.Factorypublic interface Factory extends AndroidInjector.Factory<YourActivity> {}
}
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<Activity>变量用@Inject标识它,并且在activityInjector()方法中返回。
public class YourApplication extends Application implements HasActivityInjector{@Inject DispatchingAndroidInjector<Activity> activityInjector;@Overridepublic void onCreate() {super.onCreate();DaggerYourApplicationComponent.create().inject(this);}@Overridepublic AndroidInjector<Activity> 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 Activityimplements HasAndroidInjector {@Inject DispatchingAndroidInjector<Object> androidInjector;@Overridepublic void onCreate(Bundle savedInstanceState) {AndroidInjection.inject(this);super.onCreate(savedInstanceState);// ...}@Overridepublic AndroidInjector<Object> androidInjector() {return androidInjector;}
}public class YourFragment extends Fragment {@Inject SomeDependency someDep;@Overridepublic void onAttach(Activity activity) {AndroidInjection.inject(this);super.onAttach(activity);// ...}
}@Subcomponent(modules = ...)
public interface YourFragmentSubcomponent extends AndroidInjector<YourFragment> {@Subcomponent.Factorypublic interface Factory extends AndroidInjector.Factory<YourFragment> {}
}@Module(subcomponents = YourFragmentSubcomponent.class)
abstract class YourFragmentModule {@Binds@IntoMap@ClassKey(YourFragment.class)abstract AndroidInjector.Factory<?>bindYourFragmentInjectorFactory(YourFragmentSubcomponent.Factory factory);
}@Subcomponent(modules = { YourFragmentModule.class, ... }
public interface YourActivityOrYourApplicationComponent { ... }
注入ViewModel对象
a、自定义一个ViewModelProvider.Factory
@Singleton
class MvvmViewModelFactory @Inject constructor(private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {override fun <T : ViewModel> create(modelClass: Class<T>): 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<Class<? extends ViewModel>, Provider<ViewModel>> creators;@Injectpublic MvvmViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {this.creators = creators;}@SuppressWarnings("unchecked")@Overridepublic <T extends ViewModel> T create(Class<T> modelClass) {Provider<? extends ViewModel> creator = creators.get(modelClass);if (creator == null) {for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> 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 {@Bindsabstract 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<MvvmApplication> {@Component.Builderinterface Builder {@BindsInstancefun application(application: MvvmApplication): Builderfun build(): AppCompoent}
}
为了理解上面步骤说的内容多说几句:
①、subcomponen
subcomponent简单翻译就是子组件,详细了解可以戳这里,处理的场景就是@Component需要用另一个@Component提供依赖。
接着上面那个Father例子继续举例,现在Father有了一个Son,Son的实例化需要一个Father对象。
class Son(private val father: Father) {val fatherName: Stringget() = father.name}
在讲subcomponent之前,先看看用dependencies怎么处理这种情况:
class Father {val name = "老王"
}@Module
class FatherModule {@Providesfun providerFather() = Father()
}@Component(modules = [FatherModule::class])
interface FatherComponent {fun offerFather(): Father
}class Son(private val father: Father) {val fatherName: Stringget() = father.name}@Module
class SonModule {@Providesfun 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)}@Injectlateinit var son: Sonfun 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 {@Providesfun providerFather() = Father()
}@Component(modules = [FatherModule::class])
interface FatherComponent {fun buildChildComponent(): SonComponent.Builder
}class Son(private val father: Father) {val fatherName: Stringget() = father.name}@Module
class SonModule {@Providesfun providerSon(father: Father) = Son(father)
}@Subcomponent(modules = [SonModule::class])
interface SonComponent {fun inject(runner: Runner)@Subcomponent.Builderinterface Builder {fun build(): SonComponent}
}class Runner {init {DaggerFatherComponent.create().buildChildComponent().build().inject(this)}@Injectlateinit var son: Sonfun 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.Builderinterface Builder {fun build(): SonComponent}
}
@Subcomponent.Builder表示是顶级@Subcomponent的内部类。
②、Subcomponent.Builde和Subcomponent.Factory
详细解释戳这里,这里我把文章的解释简单说一下
这里需要提到 @BindsInstance这个注解:将组件component builder上的方法或 component factory上的参数标记为将实例绑定到组件内的某些键。
@Component.Builderinterface Builder {@BindsInstance Builder foo(Foo foo);@BindsInstance Builder bar( @Blue Bar bar);...}// or@Component.Factoryinterface 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)
2、页面管理
因为是单Activity应该,所以页面管理理所应当采用的是Navigation。
Navigation的使用方法可以在https://developer.android.google.cn/guide/navigation/找到。
这里说说在官网找不到的。
a、闪屏页用Naviagtion怎么实现
方法一:Theme
当向用户显示初始屏幕达几秒钟时,通常会滥用初始屏幕,并且用户在已经可以与应用程序交互的同时浪费时间在初始屏幕上。取而代之的是,您应该尽快将它们带到可以与应用程序交互的屏幕。因此,以前的Splash屏幕在Android上被视为反模式。但是Google意识到,用户单击图标与您的第一个应用程序屏幕之间仍然存在短暂的窗口,可以进行交互,在此期间,您可以显示一些品牌信息。这是实现启动屏幕的正确方法。
因此,以正确的方式实施“启动画面”时,您不需要单独的“启动画面片段”,因为这会导致App加载过程中不必要的延迟。为此,您只需要特殊的主题。理论上讲,App主题可以应用于UI,并且比您的App UI初始化并变得可见的时间要早得多。简而言之,您只需要这样的SplashTheme即可:
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar"><item name="android:windowBackground">@drawable/splash_background</item>
</style>
splash_background:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"android:opacity="opaque"> <!-- android:opacity="opaque" should be here --><item><color android:color="@color/colorPrimary" /></item><item><bitmapandroid:antialias="true"android:filter="true"android:src="@drawable/splash" /></item>
</layer-list>
<activity android:name=".ui.MainActivity"android:theme="@style/SplashTheme">
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {setTheme(R.style.AppTheme)super.onCreate(savedInstanceState).....
}
上面解决方案来自于:Navigation Architecture Component - Splash screen
方法二:popUpToInclusive
<fragmentandroid:id="@+id/splashFragment"android:name="com.siy.mvvm.exm.ui.splash.SplashFragment"android:label="SplashFragment"><actionandroid:id="@+id/action_splashFragment_to_loginFragment"app:destination="@id/loginFragment"app:popUpTo="@id/splashFragment"app:popUpToInclusive="true" /></fragment><fragmentandroid:id="@+id/loginFragment"android:name="com.siy.mvvm.exm.ui.login.LoginFragment"android:label="LoginFragment"tools:layout="@layout/fragment_login"><actionandroid:id="@+id/action_loginFragment_to_mainFragment"app:destination="@id/mainFragment" /></fragment>
注意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 = idinclusive = 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)?.childFragmentManagervar 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反射获取Fragmnentfinal 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);}//切换Fragmentft.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才能自动缓存视图?答案看这里
3、数据管理
a、NetworkBoundResource
这个图也是来自于Android应用架构指南。
它首先观察资源的数据库。首次从数据库中加载条目时,NetworkBoundResource
会检查结果是好到足以分派,还是应从网络中重新获取。请注意,考虑到您可能会希望在通过网络更新数据的同时显示缓存的数据,这两种情况可能会同时发生。
如果网络调用成功完成,它会将响应保存到数据库中并重新初始化数据流。如果网络请求失败,NetworkBoundResource
会直接分派失败消息。
Tips:
注意:在将新数据保存到磁盘后,我们会重新初始化来自数据库的数据流。不过,通常我们不需要这样做,因为数据库本身正好会分派更改。
请注意,依赖于数据库来分派更改将产生相关副作用,这样不太好,原因是,如果由于数据未更改而使得数据库最终未分派更改,就会出现这些副作用的未定义行为。
此外,不要分派来自网络的结果,因为这样将违背单一可信来源原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有新数据的情况下分派 `SUCCESS`,因为如果这样做,客户端会接收错误版本的数据。
Kotlin版代码实现也可以在google的官方demo中找到:NetworkBoundResource
Java版:NetworkBoundResource
协程版:CoroutineNetworkBoundResource
关于kotlin协程与架构组件一起使用的文档在这里
为什么要用协程实现这个呢?因为Room 和 retrofit2-2.6.0都支持协程的支持用起来很方便
还有一个优点:liveData构建块用作协程和LiveData之间的结构化并发原语。当LiveData变为活动状态时,该代码块开始执行;当LiveData变为非活动状态时,该代码块在可配置的超时后自动取消。如果在完成之前将其取消,则如果LiveData再次变为活动状态,它将重新启动。如果它在先前的运行中成功完成,则不会重新启动。请注意,只有自动取消后,它才会重新启动。如果由于任何其他原因取消了该块(例如,引发CancelationException),则不会重新启动它。
b、ADS
在Android Dev Summit (ADS) 2019 app中的最佳实践中又提出了一种应用程序体系结构,它遵循Android应用架构指南并添加了一个UserCases层,该层有助于分离关注点,使类保持小巧,集中,可重用和可测试:
与许多Android应用程序一样,ADS应用程序也从网络或缓存中延迟加载数据;我们发现这是的理想用例Flow。对于单次请求操作,suspend functions 更合适。
ADS应用程序所遵循的原则LiveData
,即不将其LiveData
用于体系结构的所有层,仅用于View和ViewModel之间的通信,而协程用于UseCase和体系结构的较低层。
因为这个原因NetworkBoundResource又多了一个Flow的版本
Flow版:FlowNetworkBoundResource.kt
Flow版参考这里实现
有关的介绍视频:https://www.jianshu.com/p/52f30bcf1945
文章涉及代码所在项目:https://github.com/Siy-Wu/mvvm_exm
Android的MVVM架构的单Activity应用实践相关推荐
- 适合初学者入门的项目,通过对 Kotlin 的系统运用,实现的一个功能完备符合主流市场标准 App。包含知识点(MVVM 开发架构、单 Activity 多 Fragment 项目设计、暗夜模式、屏幕
fragmject 项目地址:miaowmiaow/fragmject 简介: 适合初学者入门的项目,通过对 Kotlin 的系统运用,实现的一个功能完备符合主流市场标准 App.包含知识点(MVVM ...
- 云炬Android开发笔记 4单Activity界面架构设计与验证
1.4-2本应用没有使用多个activity进行界面的展示,而是通过一个activity管理多个fragment来进行处理. fragment里面有很多坑,推荐使用开源库fragmentation. ...
- 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...
- Android App的架构设计:从VM、MVC、MVP到MVVM
随着Android应用开发规模的扩大,客户端业务逻辑也越来越复杂,已然不是简单的数据展示了.如同后端开发遇到瓶颈时采用的组件拆分思想,客户端也需要进行架构设计,拆分视图和数据,解除模块之间的耦合,提高 ...
- 《Android构建MVVM》系列(一) 之 MVVM架构快速入门
前言 本文属于<Android构建MVVM>系列开篇,共六个篇章,详见目录树. 该系列文章旨在为Android的开发者入门MVVM架构,掌握其基本开发模式. 辅以讲解Android Arc ...
- android数据流分类,【Android工程之类】1 MVVM架构 - MVVM与单向数据流
前言 这个系列将讲述使用MVVM架构.LiveData.Room.Kodein.Retrofit.EventBus来建立一个统一的.优雅的.可维护的TODO程序,本系列分为多个章节,从0开始一步一步引 ...
- Android MVVM架构
1.MVC,MVP,MVVVM 1.1什么是MVVM 1.MVVM,是Model-View-ViewModel的简写,是M-V-VM三部分组成.它本质上就是MVC 的改进 版.MVVM 就是将其中的V ...
- Android从零开始搭建MVVM架构(1)————DataBinding
在真正接触并使用MVVM架构的时候,整个人都不好了.因为个人觉得,MVVM相对于MVC.MVP学习难度比较大,设计的知识点不是一点半点.所以想慢慢记录下自己的成长.如有错误希望指正. 从零开始搭建MV ...
- Android MVVM 架构应用实现
以前项目中虽然也使用MVVM架构,但由于整体框架不是我自己搭建的,导致我对于MVVM架构的整体还是很不熟悉,所以这次就自己搭建并实现一次MVVM架构. MVVM架构使用的组件有ViewModel.Li ...
最新文章
- cookie的expires属性和max-age属性
- JAVA开发工具整理
- Java并发编程之CountDownLatch/CyclicBarrierDemo/SemaphoreDemo详解
- 【★】EIGRP终极解析!
- python groupby用法_Python 标准库实践之合并字典组成的列表
- android audio 自动播放,HTML5之audio无法自动播放的问题
- String:字符串常量池
- 芯片之战!亚马逊、Google、苹果群起“围攻”英特尔
- 4周第4次课 压缩打包介绍 gzip bzip2 xz压缩工具
- 快应用实现网络测速功能_网络阅卷系统应用系统功能实现情况
- uboot网络调试方法
- java error_java基础:Error和Exception
- Linux中fasttext安装
- oracle 字段对错,oracle 两表之间字段赋值错误解析
- Jmeter---Jmeter安装教程
- 批量域名解析为IP地址
- 网络请求及各类错误代码含义总结(Errors Code)
- 致远OA漏洞分析、利用与防护合集
- 律师学python有什么用呢_《律》字意思读音、组词解释及笔画数 - 新华字典 - 911查询...
- putty连接设备时报错 Can’t agree a key change algorithm