Android提供了一个Application类,每当应用程序启动的时候,系统就会自动将这个类进行初始化。如果我们想要在任何地方轻松获取Context,可以自己定制一个Application类,方便管理程序内的一些全局状态信息。
首先创建一个MyApplication类继承自Application:
class MyApplication: Application() {
companion object{
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
我们先定义了一个context变量,然后重写了onCreate()方法,并调用getApplicationContext()方法的返回值赋给context变量,这样我们就可以以静态变量的形式获取Context对象了。
注意:将Context设置成静态变量很容易造成内存泄漏的问题,所以Android studio会提示有风险。实际上这里获取的并不是Activity或Service中的Context,而是Application中的Context,它全局只会存在一份实例,并且在整个应用程序的生命周期内都不会回收,因此是不存在内存泄漏的风险的。
我们可以Alt+Enter使用注解,让AS忽略警告:
companion object{
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
此外,我们还需要告诉系统,当程序启动的时候应该初始化MyApplication类,而不是Application类。所以去到AndroidManifest.xml文件的<application>标签下:
<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/Theme.JetpackTest">
...
</application>
此时,在任何地方都可以调用MyApplication.context获取Context了。
2.使用Intent传递对象
我们在使用Intent传递数据的时候,putExtra()方法中所支持的数据类型是有限的,如果想要传递一些自定义对象就比较困难。这里就学习一下如何使用Intent来传递对象,有两种方式:Serializable和Parcelable。
2.1 Serializable方式
Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。我们只需要让一个类去实现Serializable接口即可。
比如有一个Person类,包含name和age字段,想要序列化它,如下:
class Person : Serializable {
var name = ""
var age = 0
}
此时,所有的Person对象都可以序列化了。
在MainActivity中只需要这样写:
val person = Person()
person.name = "shuFu"
person.age = 23
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person_data", person)
startActivity(intent)
然后在SecondActivity中获取这个对象,写法如下:
val person = intent.getSerializableExtra("person_data") as Person
操作很简单。
注意:这种传递对象的工作原理是先将一个对象序列化成可存储或可传输的状态,传递给另外一个Activity后再将其反序列化成一个新的对象。虽然这两个对象中存储的数据完全一致,但是它们实际上是不同的对象。
2.2 Parcelable方式
Parcelable也是实现相同的效果,不过它的原理是将一个完整的对象进行分解,分解后的每一部分都是Intent所支持的数据类型,这样就实现了传递对象的功能。
首先修改Person类中的代码:
class Person : Parcelable {
var name = ""
var age = 0
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(p0: Parcel, p1: Int) {
p0.writeString(name) //写出name
p0.writeInt(age) //写出age
}
companion object CREATOR : Parcelable.Creator<Person> {
override fun createFromParcel(p0: Parcel): Person {
val person = Person()
person.name = p0.readString() ?: "" //读取name
person.age = p0.readInt() //读取age
return person
}
override fun newArray(p0: Int): Array<Person?> {
return arrayOfNulls(p0)
}
}
}
首先实现Parcelable接口,重写describeContents()和writeToParcel()两个方法,第一个方法返回0即可,第二个方法需要调用Parcel的writeXxx()方法将Person类中的字段一一写出。然后还必须为Person类提供一个CREATOR的匿名类实现,这里创建了一个Parcelable.Creator接口的一个实现,泛型为Person。
然后再重写createFromParcel()和newArray()两个方法,第一个方法中需要创建一个Person对象并返回,同时要读取写出的字段,读取顺序一定要与写出顺序一致。第二个方法只需要调用arrayOfNulls()方法,并使用参数中传入的size作为数组大小,创建一个空的Person数组即可。
接着在MainActivity中的用法一样,只是要修改SecondActivity中的获取方式:
val person = intent.getParcelableExtra<Person>("person_data")
这种写法还是复杂,不过Kotlin提供了一种简便的用法,前提是要传递的所有数据都必须封装在对象的主构造函数中。
修改Person类:
@Parcelize
class Person(var name: String, var age: Int) : Parcelable{
}
只要把字段移到主构造函数中,然后添加注解即可。
3.定制自己的日志工具
如果我们在编程一个项目,期间为了方便调试,在很多地方都进行了打印日志,但是到了项目完成上线的时候,那些日志仍会打印,就会产生一些风险,但如果自己一行一行地去删,也显得太麻烦了。所以就需要做到能够控制日志的打印。项目开发时就打印,上线后就屏蔽掉。
接下来就定制一个日志工具,新建一个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.v("tag", "verbose")
只需要通过修改level变量的值,就可以自由地控制日志的打印。比如让level等于VERBOSE就可以把所有的日志都打印出来,让level等于ERROR就可以只打印程序的错误日志。使用了这种方法,在开发阶段将level指定成VERBOSE,当项目正式上线的时候将level指定成ERROR就可以了。
4.调试Android程序
学习一下如何让程序随时进入调试模式,先正常启动程序,进行一些操作之后,需要开始调试的时候,点击AS顶部工具栏的“AttachDebugger to Android Process”按钮,会弹出一个进程选择框,选择我们当前程序的进程,就会进入调试状态了。
5.深色主题
想要实现最佳的深色主题效果,应该针对每一个界面都进行浅色和深色两种主题的界面设计。不过我们还是有技巧的。
我们现在是有一个DayNight主题的,表示当用户在系统中开启深色主题时,应用程序会自动使用深色主题,反之就是浅色主题。新建项目时是自动应用了这个主题的,可以看到style.xml中的代码:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MaterialTest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
...
</style>
</resources>
不过运行程序,开启深色主题的效果如下:
效果还是不错,只可惜标题栏和悬浮按钮没有变化,原因是它们使用的是定义在colors.xml文件中的颜色值,如下:
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
...
</resources>
这种指定颜色值的引用方式相当于对控件颜色的硬编码,DayNight主题是不能对这些颜色动态转换的。
解决办法呢就是进行主题差异性编程,在values-night目录下新建一个colors.xml文件,在里面指定深色主题下颜色值:
<resources>
<color name="purple_200">#303030</color>
<color name="purple_500">#232323</color>
<color name="purple_700">#343737</color>
...
</resources>
这些颜色的name要统一,因为第一个colors.xml文件中使用的是这种命名方式,所以我直接复制过来就只更改了颜色值。结果如下:
效果还是不错的,使用主题差异性编程几乎可以解决所有的适配问题,但是在DatNight主题下,最好还是尽量减少硬编码的方式来指定控件颜色。而是应该更多地使用能够根据当前主题自动切换颜色的主题属性。
比如说黑字应该衬托在白色的背景下,反之白字通常应该衬托在黑色的背景下,那么此时可以使用主题属性来指定背景以及文字的颜色,写法如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="hello world"
android:textSize="42sp"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>
这些主题属性会自动根据系统当前的主题模式选择最合适的颜色来呈现。
如果需要在不同主题下执行不同的代码,使用的时候就判断一下当前系统是否为深色主题:
fun isDarkTheme(context: Context): Boolean {
val flag = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return flag == Configuration.UI_MODE_NIGHT_YES
}
Kotlin是取消了按位运算符的写法的,改成了英文关键字,比如and关键字就对应了Java中的&运算符,or关键字对应|运算符,xor关键字对应^运算符。
网友评论