一个MVVM架构单Activity的Android工程
该示例用Navigation进行页面管理
解决了下面几个问题:
当向用户显示初始屏幕达几秒钟时,通常会滥用初始屏幕,并且用户在已经可以与应用程序交互的同时浪费时间在初始屏幕上。取而代之的是,您应该尽快将它们带到可以与应用程序交互的屏幕。因此,以前的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>
<bitmap
android: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)
.....
}
<fragment
android:id="@+id/splashFragment"
android:name="com.siy.mvvm.exm.ui.splash.SplashFragment"
android:label="SplashFragment">
<action
android:id="@+id/action_splashFragment_to_loginFragment"
app:destination="@id/loginFragment"
app:popUpTo="@id/splashFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/loginFragment"
android:name="com.siy.mvvm.exm.ui.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login">
<action
android: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 = id
inclusive = PopUpToBuilder().apply(popUpToBuilder).inclusive
}
popUpTo: 导航之前,弹出至给定的目的地。这将从后堆栈中弹出所有不匹配的目标,直到找到该目标为止。
id:弹出目的地,清除所有中间目的地。
inclusive:如果为true,也会从后堆栈中弹出给定的目标,false不会
你在文档和官方demo中都找不到相关的内容,但是可以找到这么一句话
通常,强烈建议您仅在目标之间传递最少的数据量。例如,您应该传递键来检索对象而不是传递对象本身,因为所有保存状态的总空间在Android上受到限制。如果需要传递大量数据,请考虑使用ViewModel,如在Fragments之间共享数据中所述。
Navigation推荐使用ViewModel在Fragment之间共享数据,这种方式在startActivityForResult并不友好。因此Google Issue Tracker有这么一个Issue:Navigation: startActivityForResult analog,但是它的优先级并不高。所以在官方给出解决方案之前我这有一种解决方式。
interface NavigationResult {
fun onNavigationResult(result: Bundle)
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路由。
Google Issue Tracker有2个相关的Issue:
Open fragment without lose the previous fragment states
Transaction type is not available with Navigation Architecture Component
可以看到这2个问题下面google工程师给出的回答是:Status: Won't Fix (Intended Behavior)。
那么这个问题真的没有解决方案么?最终我在Ian Lake(Android Toolkit Developer and Runner)的twitter下面找到了答案。关于这个问题的Twitter原文地址:https://twitter.com/ianhlake/status/1103522856535638016
您不必每次调用onCreateView时都为新视图inflater-您可以保留对您第一次创建的View的引用,然后再次返回它。请记住,即使不缓存视图本身,Fragment视图也会自动保存和恢复其状态。如果不是这种情况,则应首先解决该问题(确保视图具有android:id等)
为什么要确保视图有id才能自动缓存视图?答案看这里
这个图也是来自于Android应用架构指南。
它首先观察资源的数据库。首次从数据库中加载条目时,NetworkBoundResource会检查结果是好到足以分派,还是应从网络中重新获取。请注意,考虑到您可能会希望在通过网络更新数据的同时显示缓存的数据,这两种情况可能会同时发生。
如果网络调用成功完成,它会将响应保存到数据库中并重新初始化数据流。如果网络请求失败,NetworkBoundResource 会直接分派失败消息。
Tips:
注意:在将新数据保存到磁盘后,我们会重新初始化来自数据库的数据流。不过,通常我们不需要这样做,因为数据库本身正好会分派更改。
请注意,依赖于数据库来分派更改将产生相关副作用,这样不太好,原因是,如果由于数据未更改而使得数据库最终未分派更改,就会出现这些副作用的未定义行为。
此外,不要分派来自网络的结果,因为这样将违背单一可信来源原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有新数据的情况下分派 SUCCESS
,因为如果这样做,客户端会接收错误版本的数据。
Kotlin代码实现也可以在google的官方demo中找到:NetworkBoundResource
Java版:NetworkBoundResource
协程版:CoroutineNetworkBoundResource
关于kotlin协程与架构组件一起使用的文档在这里
为什么要用协程实现这个呢?因为Room 和 retrofit2-2.6.0都支持协程的支持用起来很方便
还有一个优点:liveData构建块用作协程和LiveData之间的结构化并发原语。当LiveData变为活动状态时,该代码块开始执行;当LiveData变为非活动状态时,该代码块在可配置的超时后自动取消。如果在完成之前将其取消,则如果LiveData再次变为活动状态,它将重新启动。如果它在先前的运行中成功完成,则不会重新启动。请注意,只有自动取消后,它才会重新启动。如果由于任何其他原因取消了该块(例如,引发CancelationException),则不会重新启动它。
在Android Dev Summit (ADS) 2019 app中的最佳实践中又提出了一种应用程序体系结构,它遵循Android应用架构指南并添加了一个UseCases层,该层有助于分离关注点,使类保持小巧,集中,可重用和可测试:
与许多Android应用程序一样,ADS应用程序也从网络或缓存中延迟加载数据;我们发现这是的理想用例Flow。对于单次请求操作,suspend functions 更合适。
ADS应用程序所遵循的原则LiveData,即不将其LiveData用于体系结构的所有层,仅用于View和ViewModel之间的通信,而协程用于UseCase和体系结构的较低层。
因为这个原因NetworkBoundResource又多了一个Flow的版本
Flow版:FlowNetworkBoundResource.kt
Flow版参考这里实现
有关的介绍视频:https://www.jianshu.com/p/52f30bcf1945
详细内容可以查看这边文章:https://blog.csdn.net/baidu_34012226/article/details/102458177