阅读 25

JetPack 教程之查漏补缺

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 生命周期不同状态的变化。

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 中绑定

class TestLifecycleService : LifecycleService() {

    private val serviceLifecycleObserver = ServiceLifecycleObserver()

    init {
        lifecycle.addObserver(serviceLifecycleObserver)
    }
}
复制代码

ProcessLifecycleOwner

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

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

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 进行绑定。

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 重新创建”的情况预留一些时间。也就是说,系统需要保证当设备出现这种情况时,这两个事件不会被调用。因为当旋转屏幕时,你的应用程序并没有退到后台,它只是进入了横/竖屏模式而已。

Navigation 相关

NavigationUI

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

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

// 方式一:
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 和页面切换进行管理。由于这方面的内容繁多,非少量篇幅能够讲解的。这里推荐两篇教程可以详细学习:

DeepLink

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

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

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

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

...
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()
...
复制代码

另外一种创建的方式:

val pendingIntent = NavDeepLinkBuilder(context)
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.android)
    .setArguments(args)
    .createPendingIntent()
复制代码

URL 方式

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

// 导航图文件: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 页面中访问你的网站时,应用程序便能得到监听。如下例所示:

<?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 链接:

<!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 是同一个,它并没有被销毁,它所持有的数据也一直都存在着。

实例化

无参实例化

val viewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
复制代码

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

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
        }
    }
}
复制代码

实例化的代码需要改成:

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 直接绑定。

val viewModel = ViewModelProvider(getActivity()).get(SharedViewModel::class.java)
复制代码

2. 两个 Activity 之间数据共享

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

class ShareViewModel : ViewModel() {
    companion object {
        val shareData = MutableLiveData<ShareBean>()
    }
    ...
}
复制代码

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

// 单例 使用时才初始化
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 则永远不会被系统自动回收,这就造成了内存泄漏。