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關鍵字對應^運算符。