课本
书籍资源进入官网下载,PC端进入
第十四章-继续进阶,你还应该掌握的高级技巧
全局获取Context的技巧
回想这么久以来我们所学的内容,你会发现有很多地方都需要用到Context,弹出Toast的时候需要,启动Activity的时候需要,发送广播的时候需要,操作数据库的时候需要,使用通知的时候需要……
或许目前你还没有为得不到Context而发愁过,因为我们很多的操作是在Activity中进行的,而Activity本身就是一个Context对象。但是,当应用程序的架构逐渐开始复杂起来的时候,很多逻辑代码将脱离Activity类,但此时你又恰恰需要使用Context,也许这个时候你就会感到有些伤脑筋了。
例如,在第12章的Kotlin课堂中,我们编写了一个Toast.kt文件,并在这里对Toast的用法进行了封装,代码如下所示:
fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
可以看到,由于Toast的makeText()方法要求传入一个Context参数,但是当前代码既不在Activity当中,也不在Service当中,是没有办法直接获取Context对象的。于是这里我们只好给showToast()方法添加了一个Context参数,让调用showToast()方法的人传递一个Context对象进来。
虽说这也是一种解决方案,但是有点推卸责任的嫌疑,因为我们将获取Context的任务转移给了showToast()方法的调用方,至于调用方能不能得到Context对象,那就不是我们需要考虑的问题了。
由此可以看出,在某些情况下,获取Context并非是那么容易的一件事,有时候还是挺伤脑筋的。不过别担心,下面我们就来学习一种技巧,让你在项目的任何地方都能够轻松获取Context。
Android提供了一个Application类,每当应用程序启动的时候,系统就会自动将这个类进行初始化。而我们可以定制一个自己的Application类,以便于管理程序内一些全局的状态信息,比如全局Context。
定制一个自己的Application其实并不复杂,首先需要创建一个MyApplication类继承自Application,代码如下所示:
class MyApplication : Application() {
companion object {
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
可以看到,MyApplication中的代码非常简单。这里我们在companion object中定义了一个context变量,然后重写父类的onCreate()方法,并将调用getApplicationContext()方法得到的返回值赋值给context变量,这样我们就可以以静态变量的形式获取Context对象了。
需要注意的是,将Context设置成静态变量很容易会产生内存泄漏的问题,所以这是一种有风险的做法,因此Android Studio会给出如图所示的警告提示。
但是由于这里获取的不是Activity或Service中的Context,而是Application中的Context,它全局只会存在一份实例,并且在整个应用程序的生命周期内都不会回收,因此是不存在内存泄漏风险的。那么我们可以使用如下注解,让Android Studio忽略上述警告提示:
class MyApplication : Application() {
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
...
}
接下来我们还需要告知系统,当程序启动的时候应该初始化MyApplication类,而不是默认的Application类。这一步也很简单,在AndroidManifest.xml
文件的<application>
标签下进行指定就可以了,代码如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.materialtest">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>
这样我们就实现了一种全局获取Context的机制,之后不管你想在项目的任何地方使用Context,只需要调用一下MyApplication.context就可以了。
那么接下来我们再对showToast()方法进行优化,代码如下所示:
fun String.showToast(duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(MyApplication.context, this, duration).show()
}
fun Int.showToast(duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(MyApplication.context, this, duration).show()
}
可以看到,showToast()方法不需要再通过传递参数的方式得到Context对象,而是调用一下MyApplication.context就可以了。这样showToast()方法的用法也得到了进一步的精简,现在只需要使用如下写法就能弹出一段文字提示:
"This is Toast".showToast()
有了这个技巧,你就再也不用为得不到Context对象而发愁了。
使用Intent传递对象
Intent的用法相信你已经比较熟悉了,我们可以借助它来启动Activity、启动Service、发送广播等。在进行上述操作的时候,我们还可以在Intent中添加一些附加数据,以达到传值的效果,假设在FirstActivity中添加如下代码:
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("string_data", "hello")
intent.putExtra("int_data", 100)
startActivity(intent)
这里调用了Intent的putExtra()方法来添加要传递的数据,之后在SecondActivity中就可以得到这些值了,代码如下所示:
intent.getStringExtra("string_data")
intent.getIntExtra("int_data", 0)
但是不知道你有没有发现,putExtra()方法中所支持的数据类型是有限的,虽然常用的一些数据类型是支持的,但是当你想去传递一些自定义对象的时候,就会发现无从下手。不用担心,下面我们就学习一下使用Intent来传递对象的技巧。
使用Intent来传递对象通常有两种实现方式:Serializable和Parcelable。
Serializable方式
Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。至于序列化的方法非常简单,只需要让一个类去实现Serializable这个接口就可以了。
比如说有一个Person类,其中包含了name和age这两个字段,如果想要将它序列化,就可以这样写:
class Person : Serializable {
var name = ""
var age = 0
}
这里我们让Person类实现了Serializable接口,这样所有的Person对象都是可序列化的了。
然后在FirstActivity中只需要这样写:
val person = Person()
person.name = "Tom"
person.age = 20
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person_data", person)
startActivity(intent)
可以看到,这里我们创建了一个Person的实例,并将它直接传入了Intent的putExtra()方法中。由于Person类实现了Serializable接口,所以才可以这样写。
接下来在SecondActivity中获取这个对象也很简单,写法如下:
val person = intent.getSerializableExtra("person_data") as Person
这里调用了Intent的getSerializableExtra()方法来获取通过参数传递过来的序列化对象,接着再将它向下转型成Person对象,这样我们就成功实现了使用Intent传递对象的功能。
需要注意的是,这种传递对象的工作原理是先将一个对象序列化成可存储或可传输的状态,传递给另外一个Activity后再将其反序列化成一个新的对象。虽然这两个对象中存储的数据完全一致,但是它们实际上是不同的对象
,这一点希望你能了解清楚。
Parcelable方式
除了Serializable之外,使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样就能实现传递对象的功能了。
下面我们来看一下Parcelable的实现方式,修改Person中的代码,如下所示:
class Person : Parcelable {
var name = ""
var age = 0
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name) // 写出name
parcel.writeInt(age) // 写出age
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Person> {
override fun createFromParcel(parcel: Parcel): Person {
val person = Person()
person.name = parcel.readString() ?: "" // 读取name
person.age = parcel.readInt() // 读取age
return person
}
override fun newArray(size: Int): Array<Person?> {
return arrayOfNulls(size)
}
}
}
Parcelable的实现方式要稍微复杂一些。可以看到,首先我们让Person类实现了Parcelable接口,这样就必须重写describeContents()和writeToParcel()这两个方法。其中describeContents()方法直接返回0就可以了,而在writeToParcel()方法中,我们需要调用Parcel的writeXxx()方法,将Person类中的字段一一写出。注意,字符串型数据就调用writeString()方法,整型数据就调用writeInt()方法,以此类推。
除此之外,我们还必须在Person类中提供一个名为CREATOR的匿名类实现。这里创建了Parcelable.Creator接口的一个实现,并将泛型指定为Person。接着需要重写createFromParcel()和newArray()这两个方法,在createFromParcel()方法中,我们要创建一个Person对象进行返回,并读取刚才写出的name和age字段。其中name和age都是调用Parcel的readXxx()方法读取到的,注意这里读取的顺序一定要和刚才写出的顺序完全相同。而newArray()方法中的实现就简单多了,只需要调用arrayOfNulls()方法,并使用参数中传入的size作为数组大小,创建一个空的Person数组即可。
接下来,在FirstActivity中我们仍然可以使用相同的代码来传递Person对象,只不过在SecondActivity中获取对象的时候需要稍加改动,如下所示:
val person = intent.getParcelableExtra("person_data") as Person
注意,这里不再是调用getSerializableExtra()方法,而是调用getParcelableExtra()方法来获取传递过来的对象,其他的地方完全相同。
不过,这种实现方式写起来确实比较复杂,为此Kotlin给我们提供了另外一种更加简便的用法,但前提是要传递的所有数据都必须封装在对象的主构造函数中才行。
修改Person类中的代码,如下所示:
@Parcelize
class Person(var name: String, var age: Int) : Parcelable
没错,就是这么简单。将name和age这两个字段移动到主构造函数中,然后给Person类添加一个@Parcelize注解即可,是不是比之前的用法简单了好多倍?
这样我们就把使用Intent传递对象的两种实现方式都学习完了。对比一下,Serializable的方式较为简单,但由于会把整个对象进行序列化,因此效率会比Parcelable方式低一些,所以在通常情况下,还是更加推荐使用Parcelable的方式来实现Intent传递对象的功能。
定制自己的日志工具
早在1.4节中我们就已经学过了Android日志工具的用法,并且日志工具贯穿了我们整本书的学习。虽然Android中自带的日志工具功能非常强大,但也不能说完全没有缺点,例如在打印日志的控制方面就做得不够好。
打个比方,你正在编写一个比较庞大的项目,期间为了方便调试,在代码的很多地方打印了大量的日志。最近项目已经基本完成了,但是却有一个非常让人头疼的问题,之前用于调试的那些日志,在项目正式上线之后仍然会照常打印,这样不仅会降低程序的运行效率,还有可能将一些机密性的数据泄露出去。
那该怎么办呢?难道要一行一行地把所有打印日志的代码都删掉吗?显然这不是什么好点子,不仅费时费力,而且以后你继续维护这个项目的时候可能还会需要这些日志。因此,最理想的情况是能够自由地控制日志的打印,当程序处于开发阶段时就让日志打印出来,当程序上线之后就把日志屏蔽掉。
看起来好像是挺高级的一个功能,其实并不复杂,我们只需要定制一个自己的日志工具就可以轻松完成了。新建一个LogUtil单例类,代码如下所示:
object LogUtil {
private const val VERBOSE = 1
private const val DEBUG = 2
private const val INFO = 3
private const val WARN = 4
private const val ERROR = 5
private var level = VERBOSE
fun v(tag: String, msg: String) {
if (level <= VERBOSE) {
Log.v(tag, msg)
}
}
fun d(tag: String, msg: String) {
if (level <= DEBUG) {
Log.d(tag, msg)
}
}
fun i(tag: String, msg: String) {
if (level <= INFO) {
Log.i(tag, msg)
}
}
fun w(tag: String, msg: String) {
if (level <= WARN) {
Log.w(tag, msg)
}
}
fun e(tag: String, msg: String) {
if (level <= ERROR) {
Log.e(tag, msg)
}
}
}
可以看到,我们在LogUtil中首先定义了VERBOSE、DEBUG、INFO、WARN、ERROR这5个整型常量,并且它们对应的值都是递增的。然后又定义了一个静态变量level,可以将它的值指定为上面5个常量中的任意一个。
接下来,我们提供了v()、d()、i()、w()、e()这5个自定义的日志方法,在其内部分别调用了Log.v()、Log.d()、Log.i()、Log.w()、Log.e()这5个方法来打印日志,只不过在这些自定义的方法中都加入了一个if判断,只有当level的值小于或等于对应日志级别值的时候,才会将日志打印出来。
这样就把一个自定义的日志工具创建好了,之后在项目里,我们可以像使用普通的日志工具一样使用LogUtil。比如打印一行DEBUG级别的日志可以这样写:
LogUtil.d("TAG", "debug log")
打印一行WARN级别的日志可以这样写:
LogUtil.w("TAG", "warn log")
我们只需要通过修改level变量的值,就可以自由地控制日志的打印行为。比如让level等于VERBOSE就可以把所有的日志都打印出来,让level等于ERROR就可以只打印程序的错误日志。
使用了这种方法之后,刚才所说的那个问题也就不复存在了,你只需要在开发阶段将level指定成VERBOSE,当项目正式上线的时候将level指定成ERROR就可以了。
调试Android程序
不讲
深色主题
我们一直以来使用的操作系统都是以浅色主题为主的,这种主题模式在白天或者是光线充足的情况下使用起来没有任何问题,可是在夜晚灯光关闭的情况下使用就会显得非常刺眼。
于是,许多应用程序为了能够让用户在光线昏暗的环境下更加舒适地使用,会在应用内部提供一个一键切换夜间模式的按钮。当用户开启了夜间模式,就会将应用程序的整体色调都调整成更加适合于夜间浏览的颜色。
不过,这种由应用程序自发实现夜间模式的方式很难做到全局统一,即有些应用可能支持夜间模式,有些应用却不支持。而且重复操作的问题也很让人头疼,比如说我在一个应用中开启了夜间模式,在另外一个应用中还需要再开启一次,关闭夜间模式也需要进行同样重复的操作。
因此,很多开发者一直呼吁,希望Android能够在系统层面支持夜间模式功能。终于在Android10.0系统中,Google引入了深色主题这一特性,从而让夜间模式正式成为了官方支持的功能。
或许你会有些疑惑,这种看上去并没有太多技术难度的功能,为什么Android直到10.0系统中才进行支持呢?这是因为仅仅操作系统自身支持深色主题是没有用的,还得让所有的应用程序都能够支持才行,而这从来都不是一件容易的事情。
为此,我希望你以后开发的应用程序都能够按照Android系统的要求对深色主题进行很好地支持,不然当用户开启了深色主题之后,只有你的应用还使用的是浅色主题的话,就会显得格格不入。
除了让眼部在夜间使用时更加舒适之外,深色主题还可以减少电量消耗,从而延长手机续航,是一项非常有用的功能。那么接下来,我们就开始学习如何才能让应用程序支持深色主题功能。
首先,Android 10.0及以上系统的手机,都可以在Settings→Display→Dark theme中对深色主题进行开启和关闭。开启深色主题后,系统的界面风格包括一些内置的应用程序都会变成深色主题的色调。
不过,如果这时你打开我们自己编写的应用程序,你会发现目前界面的风格还是使用的浅色主题模式,这就和系统的主题风格不同了,说明我们需要对此进行适配。这里我准备使用在第12章中编写的MaterialTest项目来作为示例,看一看如何才能让它更加完美地适配深色主题模式。
最简单的一种适配方式就是使用Force Dark,它是一种能让应用程序快速适配深色主题,并且几乎不用编写额外代码的方式。Force Dark的工作原理是系统会分析浅色主题应用下的每一层View,并且在这些View绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色。注意,只有原本使用浅色主题的应用才能使用这种方式,如果你的应用原本使用的就是深色主题,Force Dark将不会起作用。
这里我没写这个项目,可以自己去第一行代码3查看本章.
Kotlin课堂:Java与Kotlin代码之间的转换
本章了解即可