JetPack 查漏补缺

本文只介绍 JetPack 学习中你可能需要注意或者你注意不到的知识点,需要你需要你有一定的 JetPack 基础。

LifeCycle 相关

LifeCycle 组件主要用于自定义组件对页面的生命周期的感知,同时又与页面解耦。所有具有生命周期的组件都能够使用 LifeCycle,而具有生命周期的系统组件不仅仅包括 Activity、Fragment,还有 Service 和 Application,自然 LifeCycle 中也对他们俩提供了相关的支持。

LifecycleService

为了对 Service 生命周期的监听,同时达到解耦 Service 与组件的目的,Jetpack 中提供了一个名为 LifecycleService 的类。它直接继承自 Service(使用起来与普通Service没有差别),并实现了 LifecycleOwner 接口。与 Activity、Fragment 类似,它也提供了一个名为 getLifecycle() 的方法供我们使用。

具体使用:
1. 自定义 LifecycleObserver 来监听 Service 生命周期不同状态的变化。

1
2
3
4
5
6
7
8
9
10
11
class ServiceLifecycleObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onServiceCreate() {
Log.e(javaClass.name, "Service is create.")
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onServiceDestroy() {
Log.e(javaClass.name, "Service is destroy.")
}
}

2. 在 Service 中绑定

1
2
3
4
5
6
7
8
class TestLifecycleService : LifecycleService() {

private val serviceLifecycleObserver = ServiceLifecycleObserver()

init {
lifecycle.addObserver(serviceLifecycleObserver)
}
}

ProcessLifecycleOwner

LifeCycle 中提供的 ProcessLifecycleOwner 类,用来感知整个应用的生命周期。应用当前状态是处在前台还是后台,或者应用从后台回到前台时状态的切换,我们都能够轻易感知到。

具体使用:
1. 自定义 LifecycleObserver 来监听应用不同状态的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ApplicationLifecycleObserver : LifecycleObserver {

/**
* 在应用程序的整个生命周期中只会被调用一次
*/
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onCreate()")
}

/**
* 当应用程序在前台出现时被调用
*/
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onStart()")
}

/**
* 当应用程序在前台出现时被调用
*/
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onResume()")
}

/**
* 当应用程序退出到后台时被调用
*/
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onPause()")
}

/**
* 当应用程序退出到后台时被调用
*/
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onStop()")
}

/**
* 永远不会调用,系统不会分发调用 ON_DESTROY 事件
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
Log.e(javaClass.name, "ApplicationLifecycleObserver.onDestroy()")
}
}

2. 自定义 Application 进行绑定。

1
2
3
4
5
6
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(ApplicationLifecycleObserver())
}
}

使用方法如上,非常简单。使用时有以下几点值得注意:

  1. ProcessLifecycleOwner 是针对整个应用程序的监听,与 Activity 数量无关,你有一个 Activity 或多个 Activity,对ProcessLifecycleOwner 来说是没有区别的。
  2. Lifecycle.Event.ON_CREATE 只会被调用一次,而Lifecycle.Event.ON_DESTROY 永远不会被调用。
  3. 当应用程序从后台回到前台,或者应用程序被首次打开时,会依次调用 Lifecycle.Event.ON_START 和 Lifecycle.Event.ON_RESUME。
  4. 当应用程序从前台退到后台(用户按下 Home 键或任务菜单键),会依次调用 Lifecycle.Event.ON_PAUSE 和 Lifecycle.Event.ON_STOP。需要注意的是,这两个方法的调用会有一定的延后。这是因为系统需要为“屏幕旋转,由于配置发生变化而导致 Activity 重新创建”的情况预留一些时间。也就是说,系统需要保证当设备出现这种情况时,这两个事件不会被调用。因为当旋转屏幕时,你的应用程序并没有退到后台,它只是进入了横/竖屏模式而已。

在实际的开发中,不同的页面(这里的页面是 fragment)常常有着不同的顶部标题栏和对应不同的 menu 菜单。为了方便管理,Jetpack 引入了 NavigationUI 组件,使顶部标题栏中的按钮和菜单能够与导航图(res/navigation/navigation_xxx.xml)中的页面关联起来。

其实就算没有 NavigationUI,我们也有多种以往的方法来实现切换 fragment 时对应不同的标题栏。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 方式一:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}

override fun onCreateView(...): View? {
val inflaterView = inflater.inflate(R.layout.fragment_XXX, container, false)
(activity as AppCompatActivity).setSupportActionBar(inflaterView.findViewById(R.id.toolbar))
// (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
// (activity as AppCompatActivity).supportActionBar?.setHomeButtonEnabled(true)
}

// 方式二:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}


override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_xxx, menu)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.xxx -> {
...
return true
}
}
return super.onOptionsItemSelected(item)
}

而 NavigationUI 有着独特的方式对 App bar 和页面切换进行管理。由于这方面的内容繁多,非少量篇幅能够讲解的。这里推荐两篇教程可以详细学习:

Navigation 组件还有一个非常重要和实用的特性 DeepLink,即深层链接。通过该特性,我们可以利用 PendingIntent 或一个真实的 URL 链接,直接跳转到应用程序中的某个页面(Activity/Fragment)。平时用的 Navigation.findNavController().navigate(...) 只是应用内页面按流程的切换跳转。

DeepLink 常见的两种应用场景如下:

  • PendingIntent 的方式。当应用程序接收到某个通知推送,你希望用户在单击该通知时,能够直接跳转到展示该通知内容的页面,那么可以通过PendingIntent 来完成此操作。
  • URL 的方式。当用户通过手机浏览器浏览网站上的某个页面时,可以在网页上放置一个类似于“在应用内打开”的按钮。如果用户的手机安装有我们的应用程序,那么通过 DeepLink 就能打开相应的页面;如果没有安装,那么网站可以导航到应用程序的下载页面,从而引导用户安装应用程序。

PendingIntent 方式
我们通过 sendNotification() 发送一个通知栏消息,然后这个通知栏消息的点击事件我们设置为跳转到 Navigation 导航图中的某 fragment 页面并传递参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
btn.setOnClickListener {
...
val notificationBuilder = NotificationCompat
.Builder(this,CHANNEL_ID)
.setContentIntent(getPendingIntent())
...
...
NotificationManagerCompat.from(this)
.notify(1,notificationBuilder.build())
}

private fun getPendingIntent() = Navigation
.findNavController(this, R.id.XXX)
.createDeepLink()
.setGraph(R.navigation.xxx_navigation)
.setDestination(R.id.xxxNavigationFragment)
.setArguments(bundleOf("params" to "xxx"))
.createPendingIntent()
...

另外一种创建的方式:

1
2
3
4
5
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.android)
.setArguments(args)
.createPendingIntent()

URL 方式

1. 在导航图中为页面添加 <deepLink/> 标签。在 app:uri 属性中填入的是你的网站的相应 Web 页面地址,后面的参数会通过 Bundle 对象传递到页面中。

1
2
3
4
5
6
7
8
9
// 导航图文件:res/navigation/nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation ...>
...
<fragment ...>
<deepLink app:uri="https://www.ocnyang.com/{placeholder_name}" />
</fragment>
...
<navigation>

2. 您还必须向应用的 manifest.xml 文件中添加内容。将一个 <nav-graph> 元素添加到指向现有导航图的 Activity,这样当用户在 Web 页面中访问你的网站时,应用程序便能得到监听。如下例所示:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<application ... >
<activity name=".MainActivity" ...>
...
<nav-graph android:value="@navigation/nav_graph" />
...
</activity>
</application>
</manifest>

3. 测试

方式一:直接使用命令行在 adb 中测试:
adb shell am start -W -a android.intent.action.VIEW -d "https://www.ocnyang.com/about"

方式二:在网页文件中点击 deeplink 链接:

1
2
3
4
5
<!DOCTYPE html>
<head> ... </head>
<html>
<input type="button" value="点击打开 Deeplink" onclick="javascrtpt:window.location.href='https://www.ocnyang.com/about'">
</html>

这里介绍的两种方式都是为了实现,能够直接跳转到导航图 Navigation 中某一页面。但对于 PendingIntent 和 DeepLink 都是两个独立的知识点,更详细的内容大家可以自行搜索教程学习。

ViewModel 相关

原理

ViewModel

使用者通过工具类(ViewModelProvider)在拥有者(ViewModelStoreOwner,例如:Fragment,FragmentActivity)中获取数据中心(ViewModelStore,简单说就是一个 Map)中的某个数据(ViewModel)。如果数据中心没有,会通过工厂(Factory)创建,最常用的工厂是 AndroidViewModelFactory,它创建的数据包含 Application。

注意:
ViewModel 生命周期图

  • 由上图可知,ViewModel 生命周期长,存在于所属对象(Activity,Fragment)的全部生命周期,因此不要向 ViewModel 中传入任何类型的 Context 或带有 Context 引用的对象,这可能会导致页面无法被销毁,从而引发内存泄漏。
  • 横竖屏切换,Activity 重建,所对应的 ViewModel 是同一个,它并没有被销毁,它所持有的数据也一直都存在着。

实例化

无参实例化

1
val viewModel = ViewModelProvider(this).get(SharedViewModel::class.java)

构造器有参实例化
当 ViewModel 的构造器需要穿参数的时候,就不能像上面一样进行实例化了。而需要借助于 ViewModelProvider 的 Fatory 来进行构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SharedViewModel(sharedName: String) : ViewModel() {
var sharedName: MutableLiveData<String> = MutableLiveData()

init {
this.sharedName.value = sharedName
}

class SharedViewModelFactory(private val sharedName: String) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return SharedViewModel(sharedName) as T
}
}
}

实例化的代码需要改成:

1
val viewModel = ViewModelProvider(this, SharedViewModel.SharedViewModelFactory("ocnyang")).get(SharedViewModel::class.java)

AndroidViewModel

如果您需要在 viewmodel 中使用上下文,可以选择使用 AndroidViewModel, 因为它包含应用程序上下文 (以检索上下文调用 getApplication()), 否则使用常规 ViewModel。

因可以直接调用 getAppLication() ,所以你可以在 AndroidViewModel 中访问全局资源文件 getApplication().getResources().getString(R.string.XXX); 或通过 SharedPreferences 对数据进行持久化存储等等。

数据库的实例化也需要 Context,ViewModel 是用于存放数据的,因此我们有时将数据库放在 ViewModel 中进行实例化,这时我们就可以选择使用 AndroidViewModel。

LiveData 相关

数据共享

1. Fragment 与 Fragment 之间数据共享(同一个导航图,即同一个 Activity 下的两个页面)

在两个 Fragment 之间共享数据是非常简单的,只需要在两个 Fragment 中实例化 ViewModel 时,通过 getActivity() 让它和 Activity 直接绑定。

1
val viewModel = ViewModelProvider(getActivity()).get(SharedViewModel::class.java)

2. 两个 Activity 之间数据共享

当然你可以直接将 ViewModel 中的 LiveData 数据静态化:

1
2
3
4
5
6
class ShareViewModel : ViewModel() {
companion object {
val shareData = MutableLiveData<ShareBean>()
}
...
}

这种方式虽然也能实现我们想要的功效,但是我们不提倡这样做。
我们常常是通过使 LiveData 作为单例的形式来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 单例 使用时才初始化
class SingletonLiveData : MutableLiveData<ShareBean>() {
companion object {
private lateinit var sInstance: SingletonLiveData

@MainThread
fun get(): SingletonLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else SingletonLiveData()
return sInstance
}
}
}

class ShareViewModel : ViewModel() {
val singletonLiveData = SingletonLiveData.get()
...
}

这样的话,不同的 Activity 就算实例化多次 ViewModel,但使用的都是同一个 LiveData。

LiveData.observeForever()

LiveData 还提供了一个名为 observeForever() 的方法,使用起来与 observe() 没有太大差别。它们的区别主要在于,当 LiveData 所包装的数据发生变化时,无论页面处于什么状态,observeForever() 都能收到通知。因此,在用完之后,一定要记得调用 removeObserver() 方法来停止对 LiveData 的观察,否则 LiveData 会一直处于激活状态,Activity 则永远不会被系统自动回收,这就造成了内存泄漏。

-------------本文结束 感谢阅读-------------

本文标题:JetPack 查漏补缺

文章作者:

发布时间:2020年09月17日 - 11:09

最后更新:2021年07月09日 - 22:07

原始链接:http://ocnyang.com/2020/09/17/JetPack%20%E6%9F%A5%E6%BC%8F%E8%A1%A5%E7%BC%BA/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持好文章的分享,您的支持将是对我最大的鼓励!