高级技巧
全局获取Context
许多地方都需要用到Context,弹出Toast时需要,启动Activity时需要…
当应用架构开始复杂时,很多逻辑代码将脱离Activity类,但若此时需要Context,就会有点麻烦了。
这时最简单的做法就是要求调用方法时传入context,至于调用方能否获得context就不是我们需要考虑的了。
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()
}
这样的写法多少有点推卸责任,因此我们学习一种技巧使得可在项目的任何地方获取context。
android提供了一个application类,每当应用程序启动时,系统会自动将这个类进行初始化。而我们可以定制一个自己的Application类,以便于管理程序内一些全局的状态信息,比如全局Context。
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设置为静态变量容易产生内存泄漏的问题,AS会给出警告提示,但这里获取的不是Activity或service的Context,而是Applicaion中的context,全局仅有一份实例,且在整个应用程序的生命周期内都不会回收,因此不会存在内存泄漏风险,
使用注解来忽略警告
@SuppressLint("StaticFileLeak")
在Androidmanifest中将application更改为MyApplication
<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>
将showToast优化为拓展函数,这样写,调用时无需传入context,且自带默认显示时长。
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()
}
"This is Toast".showToast()
使用Intent传递对象
Intent之前用来启动Activity,广播,传递数据,启动Service等,但putExtra支持的数据类型十分有限,仅有常见的数据类型。当想传递一些自定义对象的时候,就会发现无从下手。
使用Intent来传递对象通常有两种实现方式:Serializable和Parcelable。
Serializable
Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态,序列化后的对象可以再网络上进行传输,也可存储到本地,至于序列化十分简单,只需要让一个类去实现Serializable接口。
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对象。
val person = intent.getSerializableExtra("person_data") as Person
这种传递对象的工作原理是先将一个对象序列化成可存储或可传输的状态,传递给另一个activity后再将其反序列化成一个新对象,虽然两个对象间存储的数据完全一致,但它们实际上是不同的对象。
Parcelable
Parcelable的实现原理是将一个完整对象进行分解,而分解后的每一部分都是Intent支持的数据类型,通过这样实现传递对象。
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)
}
}
}
此处Person类实现了Parcelable接口,重写describeContents和writeToParcel这两个方法,前者直接返回,后者需要调用Parcel的write***方法,将person类中的字段一一写出,字符串数据调用writeString,整形就调用writeInt方法,以此类推。
除此之外,我们必须在Person类中提供一个名为CREATOR的匿名类实现,这里创建了Parcelable.Creator接口的一个实现,并将泛型指定为Person。接着重写其中的createFromParcel和newArray这两个方法,在createFromParcel方法中,我们要创建一个Person对象进行返回,并读取刚才写出的name和age字段,要保证读取顺序要和写出顺序完全一致,而newArray方法只需调用arrayofNulls方法,构架一个空的,数组大小为size的Person数组。
在activity获取对象
val person = intent,getParcelableExtra("person_data")as Person
定制自己的日志工具
前面我们就已经学习过android日志工具,但我们在写项目时可能会打印大量日志,调试完后必须得关闭这些日志。因为这些日志的打印不仅降低程序的运行效率,还可能将机密性数据泄漏出去。
费事费力的删除打印日志的代码不可取,因此需要个方法控制日志打印,开发阶段允许日志打印,程序上线后屏蔽日志。
要实现此功能,需要我们定制一个日志工具。
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)
}
}
}
在这里我们定义了5个等级常量,对应值是递增的,又定义了一个静态变量level,可将level值指定为5个常量其中一个。
提供了v,d,i,w,e这5个自定义的日志方法,在其内部调用了log.v等对应方法打印日志,当level小于或等于日志等级值时才允许打印。
LogUtil.d("TAG", "debug log")
LogUtil.w("TAG", "warn log")
开发时指定日志等级为最低值,上线时指定为最高值,这样日志就可以控制打印了。
调试Android程序
开发过程中总会遇到些奇怪的bug,但迟迟定位不出来原因,最好的解决方法是调试。调试允许我们逐行地执行代码,并可实时观察内存中的数据,从而轻易地查出问题的原因。
调试工作第一步是添加断点,添加好后点击Debug,使用调试模式启动程序。每按一次F8,代码就向下执行一次,并且通过Variables视图还可看到内存中的数据。
在调试模式下,程序运行效率大大降低,操作容易有卡顿感。因此android还提供另外一种调试方式,让程序随时进入调试模式。
这种模式不需要使用调试模式来启动程序,启动后点击顶部工具栏的Attach Debugger to Android Process。此时会弹出一个进程选择提示框,选择当前进程就会让这个进程进入调试模式。
深色主题
从android10.0开始,google引入了深色主题,深色主题除了能让眼部在晚上使用时更舒适,还能减少电量消耗,延长手机续航,我们要让应用程序支持深色主题功能。
最简单的一种适配方式是使用Force Dark,它能让应用程序快速适配深色主题,并且几乎不用编写额外代码的方式。ForceDark工作原理是系统会分析浅色主题应用下每一层View,并且在这些View绘制到屏幕之前,自动将它们的颜色转换成更加时候深色主题的颜色,但仅对原本使用浅色主题的应用才能使用这种方式。
右击res目录→New→Directory,创建一个values-v29目录,然后右击values-v29目录
→New→Values resource file,创建一个styles.xml文件。接着对这个文件进行编写,代码如下所示:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:forceDarkAllowed">true</item>
</style>
</resources>
除了android:forceDarkAllowed属性之外,其他的内容都是从之前的styles.xml文件中复制过来的。这里给AppTheme主题增加了android:forceDarkAllowed属性并设置为true,说明现在我们是允许系统使用Force Dark将应用强制转换成深色主题的。另外,values-v29目录是只有Android 10.0及以上的系统才会去读取的,因此这是一种系统差异型编程的实现方式。
但Force Dark十分简单粗暴,转换效果不尽如人意,始终是手动实现好用。
前面学习过,AppCompat库内置的主题恰好主要分为浅色主题和深色主题两类,如Theme.AppCompat.Light.NoActionBar就是浅色主题,Theme.AppCompat.NoActionBar就是深色主题。
现在多了一个选项:DayNight主题,当用户在系统设置开启深色主题时,应用程序就会自动开始深色主题,反之使用浅色主题。
下面我们动手来尝试一下吧。首先删除values-v29目录及其目录下的内容,然后修改
values/styles.xml中的代码,如下所示:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
...
</resources>
此时标题栏和悬浮按钮颜色保持与浅色主题时一致,因为这两个组件的颜色是我们在colors.xml中定义的色值,对控件的颜色进行了硬编码。而DayNight不对这种颜色进行动态转换。
要想解决这个问题,也是需要主题差异性编程。
右击res目录→New→Directory,创建一个values-night目录,然后右击values-night目录→New→Values resource file,创建一个colors.xml文件。接着在这个文件中指定深色主题下
的颜色值,如下所示:
<resources>
<color name="colorPrimary">#303030</color>
<color name="colorPrimaryDark">#232323</color>
<color name="colorAccent">#008577</color>
</resources>
这样的话,在普通情况下,系统仍然会读取values/colors.xml文件中的颜色值,而一旦用户开
启了深色主题,系统就会去读取values-night/colors.xml文件中的颜色值了。
整体效果就好很多了,标题栏和悬浮按钮颜色也改变了。
虽然主题差异形的编程方式几乎可以解决所有适配问题,在DayNight主题下,我们尽量减少硬编码指定控件颜色,应更多根据当前的主题切换颜色的主题属性,比如黑色的文字通常应该衬托在白色的背景下,反之亦然,那么此时我们就可以使用主题属性来指定背景以及文字的颜色
<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="40sp"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>
若你对浅色和深色主题下执行的逻辑不同,android也支持
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代码转换为kotlin代码
复制java代码到一个kotlin文件,就会弹出提示框,是否将其转为kotlin代码。或直接将一个Java文件
以及其中的所有代码一次性转换成Kotlin版本。具体操作方法是,首先在Android Studio中打
开该Java文件,然后点击导航栏中的Code→Convert Java File to Kotlin File
而其只能转换为基础的kotlin代码,而不会自动应用kotlin的优秀特性,仍需手动实现。
kotlin转换为java
kotlin不支持一键转换,但可先将kotlin代码转换为kotlin字节码,再通过反编译转换为java代码,虽然这样的代码可能无法正常运行,但有助于我们理解其背后原理。
操作方法:点击Android Studio导航栏中的Tools→Kotlin→Show Kotlin Bytecode,再点击compile即可反编译完成
我们一直以来通过kotlin-android-extensions插件获取控件id,十分方便无需findviewbyid,通过反编译可知,其实这个插件原理是先通过_$_findCachedViewById获取控件实例,再通过HashMap对实例进行缓存,避免重复查询。