前言
Android Jetpack想必大家都耳熟能詳了手幢,Android KTX
,LiveData
拱雏,Room
等等一系列庫都是出自 Jetpack
贪惹。那么Jetpack
到底是什么?又包含哪些你還沒用過的東西攻晒?Google
推出這個的原因又是什么顾复?今天我們就一起來完善一下我們腦中的Jetpack構(gòu)圖
。(篇幅較長鲁捏,建議點贊關(guān)注Mark哦?? )
介紹
2018年谷歌I/O芯砸,Jetpack
橫空出世,官方介紹如下:
Jetpack 是一套庫给梅、工具和指南假丧,可幫助開發(fā)者更輕松地編寫優(yōu)質(zhì)應(yīng)用。這些組件可幫助您遵循最佳做法动羽、讓您擺脫編寫樣板代碼的工作并簡化復(fù)雜任務(wù)包帚,以便您將精力集中放在所需的代碼上。
好好琢磨這段介紹就能解釋我們剛才的問題运吓。
Jetpack
到底是什么渴邦?
- 是一套庫、工具和指南羽德。說白了就是一系列的庫或者工具集合几莽,而且這些工具是作為我們優(yōu)質(zhì)應(yīng)用的指南,相當于
官方推薦
做法宅静。
google
推出這個系列的原因是什么章蚣?
- 規(guī)范開發(fā)者更快更好的開發(fā)出優(yōu)質(zhì)應(yīng)用。一直以來姨夹,
Android開發(fā)
都充斥了大量的不規(guī)范的操作和重復(fù)代碼纤垂,比如生命周期的管理,開發(fā)過程的重復(fù)磷账,項目架構(gòu)的選擇等等峭沦。所以Google
為了規(guī)范開發(fā)行為,就推出這套指南逃糟,旨在讓開發(fā)者們能夠更好吼鱼,更快蓬豁,更規(guī)范
地開發(fā)出優(yōu)質(zhì)應(yīng)用。
當然菇肃,這兩年的實踐也確實證明了Jetpack
做到了它介紹的那樣地粪,便捷,快速琐谤,優(yōu)質(zhì)蟆技。所以我們作為開發(fā)者還是應(yīng)該早點應(yīng)用到這些工具,提高自己的開發(fā)效率
斗忌,也規(guī)范我們自己的開發(fā)行為质礼。下面我們就一起了解下Jetpack
的所有工具指南。GOGOGO织阳!
先來一張官網(wǎng)的總攬圖:
(溫馨提示??
本文嚴格按照下圖順序?qū)M件進行分析眶蕉,有需要的可以從目錄進入或者直接搜索查看)
Jetpack-基礎(chǔ)組件
Android KTX
Android KTX 是包含在 Android Jetpack 及其他 Android 庫中的一組 Kotlin 擴展程序。KTX 擴展程序可以為 Jetpack陈哑、Android 平臺及其他 API 提供簡潔的慣用 Kotlin 代碼妻坝。為此,這些擴展程序利用了多種 Kotlin 語言功能
所以Android KTX
就是基于kotlin
特性而擴展的一些庫惊窖,方便開發(fā)使用刽宪。
舉??:
現(xiàn)在有個需求,讓兩個Set數(shù)組
的數(shù)據(jù)相加界酒,賦值給新的Set數(shù)組
圣拄。正常情況下實現(xiàn)功能:
val arraySet1 = LinkedHashSet<Int>()
arraySet1.add(1)
arraySet1.add(2)
arraySet1.add(3)
val arraySet2 = LinkedHashSet<Int>()
arraySet2.add(4)
arraySet2.add(5)
arraySet2.add(6)
val combinedArraySet1 = LinkedHashSet<Int>()
combinedArraySet1.addAll(arraySet1)
combinedArraySet1.addAll(arraySet2)
這代碼真是又臭又長???,沒關(guān)系毁欣,引入Collection KTX
擴展庫再實現(xiàn)試試:
dependencies {
implementation "androidx.collection:collection-ktx:1.1.0"
}
// Combine 2 ArraySets into 1.
val combinedArraySet = arraySetOf(1, 2, 3) + arraySetOf(4, 5, 6)
就是這么簡單庇谆,用到kotlin
的擴展函數(shù)擴展屬性,擴展了集合相關(guān)的功能凭疮,簡化了代碼饭耳。
由于kotlin
的各種特性,也就促成了一系列的擴展庫执解,還包括有Fragment KTX寞肖,Lifecycle KTX
等等。
AppCompat
不知道大家發(fā)現(xiàn)沒衰腌,原來Activity繼承的Activity類都被要求改成繼承AppCompatActivity
類新蟆。這個AppCompatActivity類就屬于AppCompat
庫,主要包含對Material Design界面實現(xiàn)的支持右蕊,相類似的還包括ActionBar琼稻,AppCompatDialog和ShareActionProvider
,一共四個關(guān)鍵類饶囚。
那么AppCompatActivity類到底對比Activity類又什么區(qū)別呢帕翻?
-
AppCompatActivity
鸠补,類似于原來的ActionBarActivity,一個帶標題欄的Activity熊咽。具體就是帶Toolbar的Activity莫鸭。
這里還有個ShareActionProvider
大家可能用得比較少,這個類是用于在菜單欄集成分享功能横殴。
通過setShareIntent(Intent intent)
方法可以在Menu里設(shè)置你要分享的內(nèi)容。具體用法可以參考官網(wǎng)說明卿拴。
Auto
讓您在編寫應(yīng)用時無需擔心特定于車輛的硬件差異(如屏幕分辨率衫仑、軟件界面、旋鈕和觸摸式控件)堕花。用戶可以通過手機上的 Android Auto 應(yīng)用訪問您的應(yīng)用文狱。或者缘挽,當連接到兼容車輛時瞄崇,運行 Android 5.0(或更高版本)的手持設(shè)備上的應(yīng)用可以與通過 Android Auto 投射到車輛的應(yīng)用進行通信。
Android Auto
壕曼,這個大家估計有點陌生苏研。但是說到 CarPlay大家是不是很熟悉呢?沒錯腮郊,Android Auto
是Google出的車機手機互聯(lián)方案摹蘑。國內(nèi)銷售的汽車大多數(shù)沒有搭載谷歌的Android Auto墻太高,觸及不到)轧飞,所以我們接觸的很少大渤。但是國外還是應(yīng)用比較廣泛的掸绞。
所以這一模塊就是用于開發(fā)Android Auto
相關(guān)應(yīng)用的,比如音樂播放APP集漾,即時通信APP之類,可以與車載系統(tǒng)通信诗芜。
怎么讓你的應(yīng)用支持Android Auto?
//添加
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<automotiveApp>
<uses name="media"/>
</automotiveApp>
然后就可以進行相關(guān)開發(fā)了横蜒。怎么測試呢斗幼?總不能讓我去汽車里面測試吧谋逻。次询。
放心送巡,官方提供了模擬器—Android Auto Desktop Head Unit emulator
(簡稱DHU)蔽介,在SDK Tools
里面可以下載犀呼。
如果你感興趣律胀,可以去官網(wǎng)文檔了解更多罪佳。
檢測
使用 Jetpack 基準庫,您可以在 Android Studio 中快速對 Kotlin 或 Java 代碼進行基準化分析。該庫會處理預(yù)熱,衡量代碼性能,并將基準化分析結(jié)果輸出到 Android Studio 控制臺。
這個模塊說的是一個測試性能的庫—Benchmark
,其實就是測試耗時時間,所以我們可以用來測試UI性能
,圖片加載性能等等。現(xiàn)在我們來實現(xiàn)一個測試圖片加載性能的??:
為了方便我們直接創(chuàng)建一個Benchmark模塊,右鍵New > Module >Benchmark Module
。
這樣就會幫我們導入好庫了躯泰,然后我們在androidTest—java
目錄下創(chuàng)建我們的測試用例類BitmapBenchmark客叉,并添加兩個測試用例方法卵慰。
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.0.0'
private const val JETPACK = "images/test001.jpg"
@LargeTest
@RunWith(AndroidJUnit4::class)
class BitmapBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var bitmap: Bitmap
@Before
fun setUp() {
val inputStream = context.assets.open(JETPACK)
bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
}
@Test
fun bitmapGetPixelBenchmark() {
val pixels = IntArray(100) { it }
benchmarkRule.measureRepeated {
pixels.map { bitmap.getPixel(it, 0) }
}
}
//測試100像素圖像繪制耗時
@Test
fun bitmapGetPixelsBenchmark() {
val pixels = IntArray(100) { it }
benchmarkRule.measureRepeated {
bitmap.getPixels(pixels, 0, 100, 0, 0, 100, 1)
}
}
}
然后右鍵BitmapBenchmark
類運行鲤嫡,注意需要在真機運行,控制臺打印出兩個方法的耗時
Started running tests
benchmark: 2,086 ns BitmapBenchmark.bitmapGetPixelsBenchmark
benchmark: 70,902 ns BitmapBenchmark.bitmapGetPixelBenchmark
Tests ran to completion.
這就是Benchmark
庫的簡單使用,我理解benchmark
這個模塊是在單元測試的基礎(chǔ)上可以提供更多性能測試的功能,比如執(zhí)行時間等。但是實際使用的話好像大家都用的比較少?以后會多嘗試看看,如果有懂的老鐵也可以評論區(qū)科普下??。
多dex處理
這個應(yīng)該大家都很熟悉腐芍,65536方法
數(shù)限制颠蕴。由于 65536 等于64 X 1024,因此這一限制稱為“64K 引用限制”。意思就是單個DEX 文件
內(nèi)引用的方法總數(shù)限制為65536,超過這個方法數(shù)就要打包成多個dex捅伤。
解決辦法:
-
Android5.0
以下,需要添加MultiDex支持庫。具體做法就是引入庫,啟用MultiDex,修改Application。 -
Android5.0
以上劝贸,默認啟動MultiDex疙剑,不需要導入庫。
問題來了?為什么5.0以上就默認支持這個功能了呢?
-
Android 5.0
之前的平臺版本使用Dalvik運行時執(zhí)行應(yīng)用代碼,Dalvik 將應(yīng)用限制為每個 APK 只能使用一個 classes.dex 字節(jié)碼文件凤优,為了繞過這一限制,只有我們手動添加MultiDex支持庫。 -
Android 5.0
及更高版本使用名為 ART 的運行時,它本身支持從APK 文件加載多個 DEX 文件。ART在應(yīng)用安裝時執(zhí)行預(yù)編譯爱沟,掃描classesN.dex文件钝尸,并將它們編譯成單個.oat 文件猪叙,以供Android設(shè)備執(zhí)行。
安全
Security 庫提供了與讀取和寫入靜態(tài)數(shù)據(jù)以及密鑰創(chuàng)建和驗證相關(guān)的安全最佳做法實現(xiàn)方法。
這里的安全指的是數(shù)據(jù)安全,涉及到的庫為Security 庫
家浇,具體就是安全讀寫文件以及安全設(shè)置共享偏好SharedPreferences莺琳。
不知道大家以前加密文件都是怎么做的,我是把數(shù)據(jù)加密后再寫入文件的寡具,現(xiàn)在用Security
庫就會方便很多厦坛。
首先代碼導入
dependencies {
implementation "androidx.security:security-crypto:1.0.0-alpha02"
}
Security 庫
主要包含兩大類:
1)EncryptedFile
讀寫一個加密文件污桦,生成EncryptedFile
之后,正常打開文件是亂碼情況坝撑,也就是加密了,需要
EncryptedFile相關(guān)API才能讀取。看看怎么實現(xiàn)讀寫的吧与涡!
// 寫入數(shù)據(jù)
fun writeData(context: Context, directory: File) {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
File(directory, fileToRead),
context,
masterKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
val fileContent = "MY SUPER-SECRET INFORMATION"
.toByteArray(StandardCharsets.UTF_8)
encryptedFile.openFileOutput().apply {
write(fileContent)
flush()
close()
}
}
// 讀取數(shù)據(jù)
fun readData(context: Context, directory: File) {
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
File(directory, fileToRead),
context,
masterKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
val inputStream = encryptedFile.openFileInput()
val byteArrayOutputStream = ByteArrayOutputStream()
var nextByte: Int = inputStream.read()
while (nextByte != -1) {
byteArrayOutputStream.write(nextByte)
nextByte = inputStream.read()
}
val plaintext: ByteArray = byteArrayOutputStream.toByteArray()
}
2)EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences
.create(
fileName,
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val sharedPrefsEditor = sharedPreferences.edit()
測試
測試應(yīng)用在Android項目中是必不可缺的步驟儒飒,包括功能測試,集成測試,單元測試
在岂。這里主要說的是通過代碼的形式編寫測試用例,測試應(yīng)用的的穩(wěn)定性骄恶,完整性等等。
具體體現(xiàn)在Android Studio中有兩個測試目錄:
-
androidTest目錄
應(yīng)包含在真實或虛擬設(shè)備上運行的測試蜕该。 -
test 目錄
應(yīng)包含在本地計算機上運行的測試绢淀,如單元測試。
具體測試的編寫可以看看這個官方項目學習:testing-samples楞抡。
TV
Android TV
應(yīng)用在國內(nèi)還是應(yīng)用比較廣泛的竞慢,市場上大部分電視都是Android系統(tǒng),支持APK安裝,包括華為鴻蒙系統(tǒng)也支持APK安裝了粟关。所以我們手機上的應(yīng)用基本可以直接安裝到電視上遮晚,只是UI焦點等方面需要改進糜颠。
以下從四個方面簡單說下TV應(yīng)用的配置汹族,分別是配置恢恼,硬件,按鍵和測試
闰围。
1)配置
首先元旬,在Androidmanifest.xml里面聲明Activity的時候榴徐,如果你想兼容TV版和手機版,可以設(shè)置不同的啟動Activity法绵,主要表現(xiàn)為設(shè)置android.intent.category.LEANBACK_LAUNCHER
過濾器:
//手機啟動Activity
<activity
android:name="com.example.android.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
//TV啟動Activity
<activity
android:name="com.example.android.TvActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Leanback">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
2)硬件
硬件主要包括如何判斷當前運行環(huán)境是TV環(huán)境箕速,以及檢查TV硬件的某些功能是否存在。
//判斷當前運行環(huán)境是TV環(huán)境
val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
Log.d(TAG, "Running on a TV Device")
} else {
Log.d(TAG, "Running on a non-TV Device")
}
//檢查TV硬件的某些功能是否存在
// Check if android.hardware.touchscreen feature is available.
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
Log.d("HardwareFeatureTest", "Device has a touch screen.")
}
3) 按鍵
TV中的界面事件主要包括:
BUTTON_B朋譬、BACK 返回
BUTTON_SELECT盐茎、BUTTON_A、ENTER徙赢、DPAD_CENTER字柠、KEYCODE_NUMPAD_ENTER 選擇
DPAD_UP、DPAD_DOWN狡赐、DPAD_LEFT窑业、DPAD_RIGHT 導航
按鍵配置包括:
nextFocusDown 定義當用戶向下導航時下一個獲得焦點的視圖。
nextFocusLeft 定義當用戶向左導航時下一個獲得焦點的視圖枕屉。
nextFocusRight 定義當用戶向右導航時下一個獲得焦點的視圖常柄。
nextFocusUp 定義當用戶向上導航時下一個獲得焦點的視圖。
<TextView android:id="@+id/Category1"
android:nextFocusDown="@+id/Category2"\>
4)測試
同樣搀擂,TV端APP的測試可以直接通過TV模擬器測試西潘,在AVD Manager
里面創(chuàng)建新的TV 模擬機即可。
Wear OS by Google
Google的手表系統(tǒng)哨颂,同樣是使用Android開發(fā)喷市。國內(nèi)好像沒有基于Wear OS
的手表,而且據(jù)我所知威恼,國外的WearOS設(shè)備也很少了品姓,被WatchOS
全面打敗,連Google旗下的App Nest都不支持WearOS了箫措。所以這部分我們了解下就行腹备,有興趣的可以去看看官方Demo
Jetpack-架構(gòu)組件
這個模塊的組件就是專門為MVVM
框架服務(wù)的,但是每個庫都是可以單獨使用的斤蔓,也是jetpack中比較重要的一大模塊植酥。
簡單說下MVVM
,Model—View—ViewModel附迷。
-
Model層
主要指數(shù)據(jù)惧互,比如服務(wù)器數(shù)據(jù),本地數(shù)據(jù)庫數(shù)據(jù)喇伯,所以網(wǎng)絡(luò)操作和數(shù)據(jù)庫讀取就是這一層喊儡,只保存數(shù)據(jù)。 -
View層
主要指UI相關(guān)稻据,比如xml布局文件艾猜,Activity界面顯示 -
ViewModel層
是MVVM的核心,連接view和model捻悯,需要將model的數(shù)據(jù)展示到view上匆赃,以及view上的操作數(shù)據(jù)反映轉(zhuǎn)化到model層,所以就相當于一個雙向綁定今缚。
所以就需要算柳,databinding進行數(shù)據(jù)的綁定,單向或者雙向姓言。viewmodel進行數(shù)據(jù)管理瞬项,綁定view和數(shù)據(jù)。lifecycle進行生命周期管理何荚。LiveData進行數(shù)據(jù)的及時反饋囱淋。
迫不及待了吧,跟隨我一起看看每個庫的神奇之處餐塘。
數(shù)據(jù)綁定
數(shù)據(jù)綁定庫是一種支持庫妥衣,借助該庫,您可以使用聲明性格式(而非程序化地)將布局中的界面組件綁定到應(yīng)用中的數(shù)據(jù)源戒傻。
主要指的就是數(shù)據(jù)綁定庫DataBinding
税手,下面從六個方面具體介紹下
配置應(yīng)用使用數(shù)據(jù)綁定:
android {
...
dataBinding {
enabled = true
}
}
1)布局和綁定表達式
通過數(shù)據(jù)綁定,我們可以讓xml布局文件中的view與數(shù)據(jù)對象進行綁定和賦值稠鼻,并且可以借助表達式語言編寫表達式來處理視圖分派的事件冈止。舉個??:
//布局 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"/>
</layout>
//實體類User
data class User(val name: String)
//Activity賦值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Bob")
}
通過@{}
符號,可以在布局中使用數(shù)據(jù)對象候齿,并且可以通過DataBindingUtil獲取賦值對象熙暴。并且@{}
里面的表達式語言支持多種運算符,包括算術(shù)運算符慌盯,邏輯運算符等等周霉。
2)可觀察的數(shù)據(jù)對象
可觀察性是指一個對象將其數(shù)據(jù)變化告知其他對象的能力。通過數(shù)據(jù)綁定庫亚皂,您可以讓對象俱箱、字段或集合變?yōu)榭捎^察。
比如上文剛說到的User類灭必,我們將name屬性改成可觀察對象狞谱,
data class User(val name: ObservableField<String>)
val userName = ObservableField<String>()
userName.set("Bob")
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User(userName)
然后綁定到布局中乃摹,這時候這個User的name
屬性就是被觀察對象了,如果userName
改變跟衅,布局里面的TextView
顯示數(shù)據(jù)也會跟著改變孵睬,這就是可觀察數(shù)據(jù)對象。
3)生成的綁定類
剛才我們獲取綁定布局是通過DataBindingUtil.setContentView
方法生成ActivityMainBinding對象并綁定布局伶跷。那么ActivityMainBinding類是怎么生成的呢掰读?只要你的布局用layout
屬性包圍,編譯后就會自動生成綁定類叭莫,類名稱基于布局文件的名稱蹈集,它會轉(zhuǎn)換為 Pascal
大小寫形式并在末尾添加 Binding 后綴。
正常創(chuàng)建綁定對象是通過如下寫法:
//Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
}
//Fragment
@Nullable
fun onCreateView( inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_layout, container, false)
return mDataBinding.getRoot()
}
4)綁定適配器
適配器這里指的是布局中的屬性設(shè)置雇初,android:text="@{user.name}"
表達式為例拢肆,庫會查找接受user.getName()
所返回類型的setText(arg)
方法。
重要的是靖诗,我們可以自定義這個適配器了善榛,也就是布局里面的屬性我們可以隨便定義它的名字和作用。來個??
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String) {
Picasso.get().load(url).into(view)
}
<ImageView app:imageUrl="@{venue.imageUrl}" />
在類中定義一個外部可以訪問的方法loadImage
呻畸,注釋@BindingAdapter
里面的屬性為你需要定義的屬性名稱移盆,這里設(shè)置的是imageUrl。所以在布局中就可以使用app:imageUrl
伤为,并傳值為String類型咒循,系統(tǒng)就會找到這個適配器方法并執(zhí)行。
5)將布局視圖綁定到架構(gòu)組件
這一塊就是實際應(yīng)用了绞愚,和jetpack其他組件相結(jié)合使用叙甸,形成完整的MVVM
分層架構(gòu)。
// Obtain the ViewModel component.
val userModel: UserViewModel by viewModels()
// Inflate view and obtain an instance of the binding class.
val binding: ActivityDatabindingMvvmBinding =
DataBindingUtil.setContentView(this, R.layout.activity_databinding_mvvm)
// Assign the component to a property in the binding class.
binding.viewmodel = userModel
<data>
<variable
name="viewmodel"
type="com.panda.jetpackdemo.dataBinding.UserViewModel" />
</data>
class UserViewModel : ViewModel() {
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
init {
currentName.value="zzz"
}
}
6)雙向數(shù)據(jù)綁定
剛才我們介紹的都是單向綁定位衩,也就是布局中view綁定了數(shù)據(jù)對象裆蒸,那么如何讓數(shù)據(jù)對象也對view產(chǎn)生綁定呢?也就是view改變
的時候數(shù)據(jù)對象也能接收到訊息糖驴,形成雙向綁定
僚祷。
很簡單,比如一個EditText贮缕,需求是EditText改變的時候辙谜,user對象name數(shù)據(jù)也會跟著改變,只需要把之前的"@{}"改成"@={}"
//布局 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.name}"/>
</layout>
很簡單吧感昼,同樣装哆,這個雙向綁定功能也是支持自定義的。來個??
object SwipeRefreshLayoutBinding {
//方法1,數(shù)據(jù)綁定到view
@JvmStatic
@BindingAdapter("app:bind_refreshing")
fun setSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout,newValue: Boolean) {
if (swipeRefreshLayout.isRefreshing != newValue)
swipeRefreshLayout.isRefreshing = newValue
}
//方法1蜕琴,view改變會通知bind_refreshingChanged萍桌,并且從該方法獲取view的數(shù)據(jù)
@JvmStatic
@InverseBindingAdapter(attribute = "app:bind_refreshing",event = "app:bind_refreshingChanged")
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =swipeRefreshLayout.isRefreshing
//方法3,view如何改變來影響數(shù)據(jù)內(nèi)容
@JvmStatic
@BindingAdapter("app:bind_refreshingChanged",requireAll = false)
fun setOnRefreshListener(swipeRefreshLayout: SwipeRefreshLayout,bindingListener: InverseBindingListener?) {
if (bindingListener != null)
swipeRefreshLayout.setOnRefreshListener {
bindingListener.onChange()
}
}
}
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_refreshing="@={viewModel.refreshing }">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
簡單說明下凌简,首先通過bind_refreshing
屬性梗夸,將數(shù)據(jù)viewModel.refreshing
綁定到view上,這樣數(shù)據(jù)變化号醉,view也會跟著變化。然后view變化的時候辛块,通過InverseBindingAdapter
注釋畔派,會調(diào)用bind_refreshingChanged
事件,而bind_refreshingChanged事件告訴了我們view什么時候會進行數(shù)據(jù)的修改润绵,在這個案例中也就是swipeRefreshLayout下滑的時候會導致數(shù)據(jù)進行改變线椰,于是數(shù)據(jù)對象會從isSwipeRefreshLayoutRefreshing
方法獲取到最新的數(shù)值,也就是從view更新過來的數(shù)據(jù)尘盼。
這里要注意的一個點是憨愉,雙向綁定要考慮到死循環(huán)問題,當View被改變卿捎,數(shù)據(jù)對象對應(yīng)發(fā)生更新配紫,同時,這個更新又回通知View層去刷新UI午阵,然后view被改變又會導致數(shù)據(jù)對象更新躺孝,無限循環(huán)下去了。所以防止死循環(huán)的做法就是判斷view的數(shù)據(jù)狀態(tài)底桂,當發(fā)生改變的時候才去更新view植袍。
Lifecycles
生命周期感知型組件可執(zhí)行操作來響應(yīng)另一個組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化。這些組件有助于您寫出更有條理且往往更精簡的代碼籽懦,這樣的代碼更易于維護于个。
Lifecycles
,稱為生命周期感知型組件暮顺,可以感知和響應(yīng)另一個組件(如 Activity 和 Fragment)的生命周期狀態(tài)的變化厅篓。
可能有人會疑惑了,生命周期就那幾個捶码,我為啥還要導入一個庫呢贷笛?有了庫難道就不用寫生命周期了嗎,有什么好處呢宙项?
舉個??乏苦,讓你感受下。
首先導入庫,可以根據(jù)實際項目情況導入
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
//.......
現(xiàn)在有一個定位監(jiān)聽器汇荐,需要在Activity
啟動的時候開啟洞就,銷毀的時候關(guān)閉。正常代碼如下:
class BindingActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
myLocationListener.start()
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
}
internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit
) {
fun start() {
// connect to system location service
}
fun stop() {
// disconnect from system location service
}
}
}
乍一看也沒什么問題是吧掀淘,但是如果需要管理生命周期的類一多旬蟋,是不是就不好管理了。所有的類都要在Activity里面管理革娄,還容易漏掉倾贰。
所以解決辦法就是實現(xiàn)解耦
,讓需要管理生命周期的類自己管理
拦惋,這樣Activity也不會遺漏和臃腫了匆浙。上代碼:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
lifecycle.addObserver(myLocationListener)
}
internal class MyLocationListener (
private val context: Context,
private val callback: (Location) -> Unit
): LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
// disconnect if connected
}
}
很簡單吧,只要實現(xiàn)LifecycleObserver
接口厕妖,就可以用注釋的方式執(zhí)行每個生命周期要執(zhí)行的方法首尼。然后在Activity里面addObserver
綁定即可。
同樣的言秸,Lifecycle
也支持自定義生命周期软能,只要繼承LifecycleOwner即可,然后通過markState
方法設(shè)定自己類的生命周期举畸,舉個??
class BindingActivity : AppCompatActivity(), LifecycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.markState(Lifecycle.State.CREATED)
}
public override fun onStart() {
super.onStart()
lifecycleRegistry.markState(Lifecycle.State.STARTED)
}
}
LiveData
LiveData 是一種可觀察的數(shù)據(jù)存儲器類查排。與常規(guī)的可觀察類不同,LiveData 具有生命周期感知能力抄沮,意指它遵循其他應(yīng)用組件(如 Activity雹嗦、Fragment 或 Service)的生命周期。這種感知能力可確保 LiveData 僅更新處于活躍生命周期狀態(tài)的應(yīng)用組件觀察者合是。
LiveData
是一種可觀察的數(shù)據(jù)存儲器類了罪。
等等,這個介紹好像似曾相識聪全?對泊藕,前面說數(shù)據(jù)綁定的時候就有一個可觀察的數(shù)據(jù)對象ObservableField
。那兩者有什么區(qū)別呢难礼?
1)LiveData
具有生命周期感知能力娃圆,可以感知到Activity等的生命周期。這樣有什么好處呢蛾茉?很常見的一點就是可以減少內(nèi)存泄漏和崩潰情況了呀讼呢,想想以前你的項目中針對網(wǎng)絡(luò)接口返回數(shù)據(jù)的時候都要判斷當前界面是否銷毀,現(xiàn)在LiveData就幫你解決了這個問題谦炬。
具體為什么能解決崩潰和泄漏問題呢悦屏?
-
不會發(fā)生內(nèi)存泄漏
觀察者會綁定到 Lifecycle 對象节沦,并在其關(guān)聯(lián)的生命周期遭到銷毀后進行自我清理。 -
不會因 Activity 停止而導致崩潰
如果觀察者的生命周期處于非活躍狀態(tài)(如返回棧中的 Activity)础爬,則它不會接收任何 LiveData 事件甫贯。 -
自動判斷生命周期并回調(diào)方法
如果觀察者的生命周期處于 STARTED 或 RESUMED狀態(tài),則 LiveData 會認為該觀察者處于活躍狀態(tài)看蚜,就會調(diào)用onActive方法叫搁,否則,如果 LiveData 對象沒有任何活躍觀察者時供炎,會調(diào)用 onInactive()方法渴逻。
2) LiveData更新數(shù)據(jù)更靈活,不一定是改變數(shù)據(jù)音诫,而是調(diào)用方法(postValue或者setValue)
的方式進行UI更新或者其他操作惨奕。
好了。還是舉個??更直觀的看看吧:
//導入庫:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
public class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val myPriceListener: LiveData<BigDecimal> = StockLiveData("")
myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// 監(jiān)聽livedata的數(shù)據(jù)變化纽竣,如果調(diào)用了setValue或者postValue會調(diào)用該onChanged方法
//更新UI數(shù)據(jù)或者其他處理
})
}
}
這是一個股票數(shù)據(jù)對象,StockManager
為股票管理器茧泪,如果該對象有活躍觀察者時蜓氨,就去監(jiān)聽股票市場的情況,如果沒有活躍觀察者時队伟,就可以斷開監(jiān)聽穴吹。
當監(jiān)聽到股票信息變化,該股票數(shù)據(jù)對象就會通過setValue
方法進行數(shù)據(jù)更新嗜侮,反應(yīng)到觀察者的onChanged方法港令。這里要注意的是setValue
方法只能在主線程調(diào)用,而postValue
則是在其他線程調(diào)用锈颗。
當Fragment
這個觀察者生命周期發(fā)生變化時顷霹,LiveData
就會移除這個觀察者,不再發(fā)送消息击吱,所以也就避免崩潰問題淋淀。
Navigation
導航
Navigation 組件旨在用于具有一個主 Activity 和多個 Fragment 目的地的應(yīng)用。主 Activity 與導航圖相關(guān)聯(lián)覆醇,且包含一個負責根據(jù)需要交換目的地的 NavHostFragment朵纷。在具有多個 Activity 目的地的應(yīng)用中,每個 Activity 均擁有其自己的導航圖永脓。
所以說白了袍辞,Navigation
就是一個Fragment
的管理框架。
怎么實現(xiàn)常摧?創(chuàng)建Activity搅吁,F(xiàn)ragment,進行連接。
1)導入庫
def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
2)創(chuàng)建3個Fragment和一個Activity
3)創(chuàng)建res/navigation/my_nav.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/myFragment1"
tools:ignore="UnusedNavigation">
<fragment
android:id="@+id/myFragment1"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/myFragment2" />
</fragment>
<fragment
android:id="@+id/myFragment2"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/myFragment3" />
</fragment>
<fragment
android:id="@+id/myFragment3"
android:name="com.example.studynote.blog.jetpack.navigation.MyFragment1"
android:label="fragment_blank"
tools:layout="@layout/fragmetn_my_1" >
</fragment>
</navigation>
在res文件夾下新建navigation
目錄似芝,并新建my_nav.xml
文件那婉。配置好每個Fragment,其中:
-
app:startDestination
屬性代表一開始顯示的fragment -
android:name
屬性代表對應(yīng)的Fragment路徑 -
action
代表該Fragment存在的跳轉(zhuǎn)事件党瓮,比如myFragment1可以跳轉(zhuǎn)myFragment2详炬。
- 修改Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/my_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到,Activity的布局文件就是一個fragment控件寞奸,name為NavHostFragment呛谜,navGraph
為剛才新建的mynavigation文件。
5)配置完了之后枪萄,就可以設(shè)置具體的跳轉(zhuǎn)邏輯了隐岛。
override fun onClick(v: View) {
//不帶參數(shù)
v.findNavController().navigate(R.id.action_blankFragment_to_blankFragment2)
//帶參數(shù)
var bundle = bundleOf("amount" to amount)
v.findNavController().navigate(R.id.confirmationAction, bundle)
}
//接收數(shù)據(jù)
tv.text = arguments?.getString("amount")
需要注意的是,跳轉(zhuǎn)這塊官方建議用Safe Args
的Gradle 插件瓷翻,該插件可以生成簡單的 object 和 builder
類聚凹,以便以類型安全的方式瀏覽和訪問任何關(guān)聯(lián)的參數(shù)。這里就不細說了齐帚,感興趣的可以去官網(wǎng)看看
Room
Room 持久性庫在 SQLite 的基礎(chǔ)上提供了一個抽象層妒牙,讓用戶能夠在充分利用 SQLite 的強大功能的同時,獲享更強健的數(shù)據(jù)庫訪問機制对妄。
所以Room
就是一個數(shù)據(jù)庫框架湘今。問題來了,市面上那么多數(shù)據(jù)庫組件剪菱,比如ormLite摩瞎,greendao
等等,為什么google還要出一個room孝常,有什么優(yōu)勢呢旗们?
-
性能優(yōu)勢,一次數(shù)據(jù)庫操作主要包括:構(gòu)造sql語句—編譯語句—傳入?yún)?shù)—執(zhí)行操作构灸。
ORMLite
主要在獲取參數(shù)屬性值的時候蚪拦,是通過反射獲取的,所以速度較慢冻押。GreenDao
在構(gòu)造sql語句的時候是通過代碼拼接驰贷,所以較慢。Room
是通過接口方法的注解生成sql語句洛巢,也就是編譯成字節(jié)碼的時候就生成了sql語句括袒,所以運行起來較快。 -
支持jetpack其他組件(比如LiveData稿茉,Paging)以及RxJava锹锰,這就好比借助了當前所在的優(yōu)勢環(huán)境芥炭,就能給你帶來一些得天獨厚的優(yōu)勢。當然實際使用起來也確實要方便很多恃慧,比如
liveData
結(jié)合园蝠,就能在數(shù)據(jù)查詢后進行自動UI更新。
既然Room這么優(yōu)秀,那就用起來吧。
Room的接入主要有三大點:DataBase弓乙、Entity抗愁、Dao
讯榕。分別對應(yīng)數(shù)據(jù)庫,表和數(shù)據(jù)訪問。
1)首先導入庫:
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
}
2)建立數(shù)據(jù)庫類,聲明數(shù)據(jù)庫表成員易遣,數(shù)據(jù)庫名稱,數(shù)據(jù)庫版本嫌佑,單例等等
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDb : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDb? = null
@Synchronized
fun get(context: Context): UserDb {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
UserDb::class.java, "StudentDatabase").build()
}
return instance!!
}
}
}
3)建表豆茫,可以設(shè)置主鍵,外鍵屋摇,索引揩魂,自增等等
@Entity
data class User(@PrimaryKey(autoGenerate = true) val id: Int,
val name: String)
4)Dao,數(shù)據(jù)操作
@Dao
interface UserDao {
@Query("SELECT * FROM User")
fun getAllUser(): DataSource.Factory<Int, User>
@Query("SELECT * FROM User")
fun getAllUser2(): LiveData<List<User>>
@Query("SELECT * from user")
fun getAllUser3(): Flowable<List<User>>
@Insert
fun insert(users: List<User>)
}
然后就可以進行數(shù)據(jù)庫操作了摊册,很簡單吧肤京。
官方文檔
Demo代碼地址
Paging
分頁庫可幫助您一次加載和顯示一小塊數(shù)據(jù)颊艳。按需載入部分數(shù)據(jù)會減少網(wǎng)絡(luò)帶寬和系統(tǒng)資源的使用量茅特。
所以Paging
就是一個分頁庫,主要用于Recycleview列表展示棋枕。下面我就結(jié)合Room說說Paging的用法白修。
使用Paging主要注意兩個類:PagedList和PagedListAdapter
。
1)PagedList
用于加載應(yīng)用數(shù)據(jù)塊重斑,綁定數(shù)據(jù)列表兵睛,設(shè)置數(shù)據(jù)頁等。結(jié)合上述Room
的Demo我繼續(xù)寫了一個UserModel
進行數(shù)據(jù)管理:
class UserModel(app: Application) : AndroidViewModel(app) {
val dao = UserDb.get(app).userDao()
var idNum = 1
companion object {
private const val PAGE_SIZE = 10
}
//初始化PagedList
val users = LivePagedListBuilder(
dao.getAllUser(), PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setEnablePlaceholders(true)
.build()
).build()
//插入用戶
fun insert() = ioThread {
dao.insert(newTenUser())
}
//獲取新的10個用戶
fun newTenUser(): ArrayList<User> {
var newUsers = ArrayList<User>()
for (index in 1..10) {
newUsers.add(User(0, "bob${++idNum}"))
}
return newUsers
}
}
2)PagedListAdapter
使用Recycleview必要要用到adatper窥浪,所以這里需要綁定一個繼承自PagedListAdapter
的adapter:
class UserAdapter : PagedListAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bindTo(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder =
UserViewHolder(parent)
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
oldItem == newItem
}
}
class UserViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)) {
private val tv1 = itemView.findViewById<TextView>(R.id.name)
var user: User? = null
fun bindTo(user: User?) {
this.user = user
tv1.text = user?.name
}
}
}
這里還用到了DiffUtil.ItemCallback
類祖很,用于比較數(shù)據(jù),進行數(shù)據(jù)更新用漾脂。
ok假颇,數(shù)據(jù)源,adapter都設(shè)置好了骨稿,接下來就是監(jiān)聽數(shù)據(jù)笨鸡,刷新數(shù)據(jù)就可以了
// 監(jiān)聽users數(shù)據(jù)姜钳,數(shù)據(jù)改變調(diào)用submitList方法
viewModel.users.observe(this, Observer(adapter::submitList))
對,就是這么一句形耗,監(jiān)聽PagedList
哥桥,并且在它改變的時候調(diào)用PagedListAdapter的submitList
方法。
這分層夠爽吧激涤,其實這也就是paging或者說jetpack給我們項目帶來的優(yōu)勢拟糕,層層解耦,adapter都不用維護list數(shù)據(jù)源了昔期。
ViewModel
ViewModel 類旨在以注重生命周期的方式存儲和管理界面相關(guān)的數(shù)據(jù)已卸。ViewModel 類讓數(shù)據(jù)可在發(fā)生屏幕旋轉(zhuǎn)等配置更改后繼續(xù)留存。
終于說到ViewModel
了硼一,其實之前的demo都用了好多遍了累澡,ViewModel
主要是從界面控制器邏輯中分離出視圖數(shù)據(jù),為什么要這么做呢般贼?主要為了解決兩大問題:
- 以前Activity中如果被系統(tǒng)銷毀或者需要重新創(chuàng)建的時候愧哟,頁面臨時性數(shù)據(jù)都會丟失,需要通過
onSaveInstanceState()
方法保存哼蛆,onCreate方法中讀取蕊梧。而且數(shù)據(jù)量一大就更加不方便了。 - 在Activity中腮介,難免有些異步調(diào)用肥矢,所以就會容易導致界面銷毀時候,這些調(diào)用還存在叠洗。那就會發(fā)生內(nèi)存泄漏或者直接崩潰甘改。
所以ViewModel
誕生了,還是解耦灭抑,我把數(shù)據(jù)單獨拿出來管理十艾,還加上生命周期,那不就可以解決這些問題了嗎腾节。而且當所有者 Activity 完全銷毀之后忘嫉,ViewModel
會調(diào)用其onCleared()
方法,以便清理資源案腺。
接下來舉個??庆冕,看看ViewModel具體是怎么使用的:
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
class SharedViewModel : ViewModel() {
var userData = MutableLiveData<User>()
fun select(item: User) {
userData.value = item
}
override fun onCleared() {
super.onCleared()
}
}
class MyFragment1 : Fragment() {
private lateinit var btn: Button
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
btn.setOnClickListener{
model?.select(User(0,"bob"))
}
}
}
class MyFragment2 : Fragment() {
private lateinit var btn: Button
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val model=activity?.let { ViewModelProvider(it).get(SharedViewModel::class.java) }
model?.userData?.observe(viewLifecycleOwner, Observer<User> { item ->
// Update the UI
})
}
}
Fragment中,獲取到viewmodel
的實例劈榨,然后進行數(shù)據(jù)監(jiān)聽等操作访递。等等,你能發(fā)現(xiàn)什么不鞋既?
對了力九,數(shù)據(jù)通信耍铜。不同的 Fragment 可以使用其父Activity共享ViewModel
來進行數(shù)據(jù)的通信,厲害吧跌前。還有很多其他的用法棕兼,去項目中慢慢發(fā)現(xiàn)吧!
WorkManager
使用 WorkManager API 可以輕松地調(diào)度即使在應(yīng)用退出或設(shè)備重啟時仍應(yīng)運行的可延遲異步任務(wù)抵乓。
聽聽這個介紹就很神奇了伴挚,應(yīng)用退出和設(shè)備重啟都能自動運行?通過廣播灾炭?那數(shù)據(jù)又是怎么保存的呢茎芋?聽說還可以執(zhí)行周期性異步任務(wù),順序鏈式調(diào)用哦蜈出!接下來一一解密
- 關(guān)于應(yīng)用退出和設(shè)備重啟
如果APP正在運行田弥,WorkManager
會在APP進程中起一個新線程來運行任務(wù);如果APP沒有運行铡原,WorkManager
會選擇一個合適的方式來調(diào)度后臺任務(wù)--根據(jù)系統(tǒng)級別和APP狀態(tài)偷厦,WorkManager可能會使用JobScheduler,F(xiàn)ireBase JobDispatcher
或者AlarmManager
燕刻。 - 關(guān)于數(shù)據(jù)保存
WorkManager
創(chuàng)建的任務(wù)數(shù)據(jù)都會保存到數(shù)據(jù)庫只泼,用的是Room
框架。然后重啟等時間段都會去數(shù)據(jù)庫尋找需要安排執(zhí)行的任務(wù)卵洗,然后判斷約束條件
请唱,滿足即可執(zhí)行。
一般這個API應(yīng)用到什么場景呢过蹂?想想十绑,可靠運行,還可以周期異步榴啸。
對了孽惰,發(fā)送日志晚岭∨赣。可以通過WorkManager
設(shè)定周期任務(wù),每天執(zhí)行一次發(fā)送日志的任務(wù)坦报。而且能夠保證你的任務(wù)可靠運行库说,一定可以上傳到,當然也是支持監(jiān)聽任務(wù)結(jié)果等片择。??:
1)導入庫
dependencies {
def work_version = "2.3.4"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
}
2) 新建任務(wù)類潜的,繼承Worker
,重寫doWork
方法字管,返回任務(wù)結(jié)果啰挪。
class UploadLogcatWork(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
if (isUploadLogcatSuc()) {
return Result.success()
} else if (isNeedRetry()){
return Result.retry()
}
return Result.failure()
}
fun isUploadLogcatSuc(): Boolean {
var isSuc: Boolean = false
return isSuc
}
fun isNeedRetry(): Boolean {
var isSuc: Boolean = false
return isSuc
}
}
3)最后就是設(shè)定約束(是否需要網(wǎng)絡(luò)信不,是否支持低電量,是否支持充電執(zhí)行亡呵,延遲等等)抽活,執(zhí)行任務(wù)(單次任務(wù)或者循環(huán)周期任務(wù))
//設(shè)定約束
val constraints =
Constraints.Builder()
//網(wǎng)絡(luò)鏈接的時候使用
.setRequiredNetworkType(NetworkType.CONNECTED)
//是否在設(shè)備空閑的時候執(zhí)行
.setRequiresDeviceIdle(false)
//是否在低電量的時候執(zhí)行
.setRequiresBatteryNotLow(true)
//是否在內(nèi)存不足的時候執(zhí)行
.setRequiresStorageNotLow(true)
//是否時充電的時候執(zhí)行
.setRequiresCharging(true)
//延遲執(zhí)行
.setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
.build()
//設(shè)定循環(huán)任務(wù)
val uploadRequest =
PeriodicWorkRequestBuilder<UploadLogcatWork>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.addTag("uploadTag")
.build()
//執(zhí)行
WorkManager.getInstance(applicationContext).enqueue(uploadRequest)
//監(jiān)聽執(zhí)行結(jié)果
WorkManager.getInstance(this)
// .getWorkInfosByTagLiveData("uploadTag") //通過tag拿到work
.getWorkInfoByIdLiveData(uploadRequest.id) //通過id拿到work
.observe(this, Observer {
it?.apply {
when (this.state) {
WorkInfo.State.BLOCKED -> println("BLOCKED")
WorkInfo.State.CANCELLED -> println("CANCELLED")
WorkInfo.State.RUNNING -> println("RUNNING")
WorkInfo.State.ENQUEUED -> println("ENQUEUED")
WorkInfo.State.FAILED -> println("FAILED")
WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
else -> println("else status ${this.state}")
}
}
})
4)另外還支持任務(wù)取消,任務(wù)鏈式順序調(diào)用等
//取消
fun cancelWork(){
WorkManager.getInstance(applicationContext).cancelAllWorkByTag("uploadTag")
}
fun startLineWork(){
//圖片濾鏡1
val filter1 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片濾鏡2
val filter2 = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片壓縮
val compress = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
//圖片上傳
val upload = OneTimeWorkRequestBuilder<UploadLogcatWork>()
.build()
WorkManager.getInstance(applicationContext)
.beginWith(listOf(filter1, filter2))
.then(compress)
.then(upload)
.enqueue()
}
Jetpack-行為組件
CameraX
CameraX 是一個 Jetpack 支持庫锰什,旨在幫助您簡化相機應(yīng)用的開發(fā)工作下硕。它提供一致且易于使用的 API Surface,適用于大多數(shù) Android 設(shè)備汁胆,并可向后兼容至 Android 5.0(API 級別 21)梭姓。
雖然它利用的是 camera2 的功能,但使用的是更為簡單且基于用例的方法嫩码,該方法具有生命周期感知能力誉尖。它還解決了設(shè)備兼容性問題,因此您無需在代碼庫中添加設(shè)備專屬代碼铸题。這些功能減少了將相機功能添加到應(yīng)用時需要編寫的代碼量释牺。
想必大家都了解過Camera API
和Camera2 API
,總結(jié)就是兩個字回挽,不好用没咙。哈哈,自我感覺千劈,在我印象中祭刚,我要照相拍一張照片,不是應(yīng)該直接調(diào)用一句代碼可以完成嗎墙牌。但是用之前的API涡驮,我需要去管理相機實例,設(shè)置SufraceView相關(guān)的各種東西喜滨,還有預(yù)覽尺寸和圖像尺寸捉捅,處理設(shè)置各種監(jiān)聽等等,頭已暈虽风。
可能是官方聽到了我的抱怨棒口,于是CameraX
來了,CameraX是基于camera2
進行了封裝辜膝,給我們提供了更簡單的解決方案來解決我們之前的困境无牵。??來了
// CameraX core library using the camera2 implementation
def camerax_version = "1.0.0-beta06"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha13"
// If you want to additionally use the CameraX Extensions library
implementation "androidx.camera:camera-extensions:1.0.0-alpha13"
<uses-permission android:name="android.permission.CAMERA" />
//初始化相機
private fun initCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
try {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
//圖片拍攝用例
mImageCapture = ImageCapture.Builder()
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
.build()
//配置參數(shù)(后置攝像頭等)
// Choose the camera by requiring a lens facing
val cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()
//指定要與相機關(guān)聯(lián)的生命周期,該生命周期會告知 CameraX 何時配置相機拍攝會話并確保相機狀態(tài)隨生命周期的轉(zhuǎn)換相應(yīng)地更改厂抖。
val camera: Camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
mImageCapture
)
//相機預(yù)覽
preview.setSurfaceProvider(view_finder.createSurfaceProvider())
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
}
//拍照并保存
fun takePhoto(view: View?) {
if (mImageCapture != null) {
val outputFileOptions: OutputFileOptions = OutputFileOptions.Builder(cretaeFile()).build()
//拍照
mImageCapture?.takePicture(
outputFileOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(@NonNull outputFileResults: OutputFileResults) {
//保存成功
Log.e(TAG, "success")
}
override fun onError(@NonNull exception: ImageCaptureException) {
//保存失敗
Log.e(TAG, "fail")
}
})
}
}
使用起來挺方便吧茎毁,而且可以綁定當前activity的生命周期,這就涉及到另外一個組件Lifecycle
了忱辅,通過一次綁定事件七蜘,就可以使相機狀態(tài)隨生命周期的轉(zhuǎn)換相應(yīng)地更改谭溉。
另外要注意的是先獲取相機權(quán)限哦。
下載管理器
DownloadManager下載管理器是一個處理長時間運行的HTTP下載的系統(tǒng)服務(wù)橡卤∫怪唬客戶端可以請求將URI下載到特定的目標文件。下載管理器將在后臺執(zhí)行下載蒜魄,負責HTTP交互扔亥,并在失敗或跨連接更改和系統(tǒng)重啟后重試下載。
DownloadManager
谈为,大家應(yīng)該都很熟悉吧旅挤,android2.3
就開通提供的API,很方便就可以下載文件伞鲫,包括可以設(shè)置是否通知顯示粘茄,下載文件夾名,文件名秕脓,下載進度狀態(tài)查詢等等柒瓣。??來
class DownloadActivity : AppCompatActivity() {
private var mDownId: Long = 0
private var mDownloadManager: DownloadManager? = null
private val observer: DownloadContentObserver = DownloadContentObserver()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
//配置下載參數(shù),enqueue開始下載
fun download(url: String) {
mDownloadManager =
this.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url))
// 設(shè)置文件夾文件名
request.setDestinationInExternalPublicDir("lz_download", "test.apk")
// 設(shè)置允許的網(wǎng)絡(luò)類型
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
// 文件類型
request.setMimeType("application/zip")
// 設(shè)置通知是否顯示
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
//設(shè)置通知欄標題
request.setTitle("apk download")
//設(shè)置通知欄內(nèi)容
request.setDescription("*** apk")
mDownId = mDownloadManager!!.enqueue(request)
contentResolver.registerContentObserver(mDownloadManager!!.getUriForDownloadedFile(mDownId), true, observer)
}
//通過ContentProvider查詢下載情況
fun queryDownloadStatus(){
val query = DownloadManager.Query()
//通過下載的id查找
//通過下載的id查找
query.setFilterById(mDownId)
val cursor: Cursor = mDownloadManager!!.query(query)
if (cursor.moveToFirst()) {
// 已下載字節(jié)數(shù)
val downloadBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
// 總字節(jié)數(shù)
val allBytes= cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
// 狀態(tài)
when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {
DownloadManager.STATUS_PAUSED -> {
}
DownloadManager.STATUS_PENDING -> {
}
DownloadManager.STATUS_RUNNING -> {
}
DownloadManager.STATUS_SUCCESSFUL -> {
cursor.close()
}
DownloadManager.STATUS_FAILED -> {
cursor.close()
}
}
}
}
//取消下載吠架,刪除文件
fun unDownLoad(view: View?) {
mDownloadManager!!.remove(mDownId)
}
override fun onDestroy() {
super.onDestroy()
contentResolver.unregisterContentObserver(observer)
}
//監(jiān)聽下載情況
inner class DownloadContentObserver : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
queryDownloadStatus()
}
}
}
demo應(yīng)該寫的很清楚了芙贫,要注意的就是保存下載id,后續(xù)取消下載傍药,查詢下載進度狀態(tài)都是通過這個id來查詢磺平。監(jiān)聽下載進度主要是通過觀察getUriForDownloadedFile
方法返回的uri,觀察這個uri指向的數(shù)據(jù)庫變化來獲取進度拐辽。
媒體和播放
Android 多媒體框架支持播放各種常見媒體類型拣挪,以便您輕松地將音頻、視頻和圖片集成到應(yīng)用中俱诸。
這里媒體和播放指的是音頻視頻相關(guān)內(nèi)容菠劝,主要涉及到兩個相關(guān)類:
MediaPlayer
ExoPlayer
MediaPlayer
不用說了,應(yīng)該所有人都用過吧睁搭,待會就順便提一嘴赶诊。
ExoPlayer
是一個單獨的庫,也是google開源的媒體播放器項目介袜,聽說是Youtube APP所使用的播放器甫何,所以他的功能也是要比MediaPlayer
強大出吹,支持各種自定義遇伞,可以與IJKPlayer
媲美,只是使用起來比較復(fù)雜捶牢。
1)MediaPlayer
//播放本地文件
var mediaPlayer: MediaPlayer? = MediaPlayer.create(this, R.raw.test_media)
mediaPlayer?.start()
//設(shè)置播放不息屏 配合權(quán)限WAKE_LOCK使用
mediaPlayer?.setScreenOnWhilePlaying(true)
//播放本地本地可用的 URI
val myUri: Uri = Uri.EMPTY
val mediaPlayer2: MediaPlayer? = MediaPlayer().apply {
setAudioStreamType(AudioManager.STREAM_MUSIC)
setDataSource(applicationContext, myUri)
prepare()
start()
}
//播放網(wǎng)絡(luò)文件
val url = "http://........"
val mediaPlayer3: MediaPlayer? = MediaPlayer().apply {
setAudioStreamType(AudioManager.STREAM_MUSIC)
setDataSource(url)
prepare()
start()
}
//釋放
mediaPlayer?.release()
mediaPlayer = null
2)ExoPlayer
compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
var player: SimpleExoPlayer ?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exoplayer)
//初始化
player = SimpleExoPlayer.Builder(this).build()
video_view.player = player
player?.playWhenReady = true
//設(shè)置播放資源
val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
this,
Util.getUserAgent(this, "yourApplicationName")
)
val uri: Uri = Uri.EMPTY
val videoSource: MediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(uri)
player?.prepare(videoSource)
}
private fun releasePlayer() {
//釋放
player?.release()
player = null
}
好像也不復(fù)雜鸠珠?哈哈巍耗,更強大的功能需要你去發(fā)現(xiàn)。
通知
通知是指 Android 在應(yīng)用的界面之外顯示的消息渐排,旨在向用戶提供提醒炬太、來自他人的通信信息或應(yīng)用中的其他實時信息。用戶可以點按通知來打開應(yīng)用驯耻,也可以直接在通知中執(zhí)行某項操作亲族。
這個應(yīng)該都了解,直接上個??
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "mychannel"
val descriptionText = "for test"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun showNotification(){
val intent = Intent(this, SettingActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Set the intent that will fire when the user taps the notification
.setContentIntent(pendingIntent)
.setAutoCancel(true)
with(NotificationManagerCompat.from(this)) {
notify(1, builder.build())
}
}
權(quán)限
權(quán)限的作用是保護 Android 用戶的隱私可缚。Android 應(yīng)用必須請求權(quán)限才能訪問敏感的用戶數(shù)據(jù)(例如聯(lián)系人和短信)以及某些系統(tǒng)功能(例如相機和互聯(lián)網(wǎng))霎迫。系統(tǒng)可能會自動授予權(quán)限,也可能會提示用戶批準請求帘靡,具體取決于訪問的功能知给。
權(quán)限大家應(yīng)該也都很熟悉了。
- 危險權(quán)限描姚。6.0以后使用危險權(quán)限需要申請涩赢,推薦RxPermissions庫
- 可選硬件功能的權(quán)限。 對于使用硬件的應(yīng)用轩勘,比如使用了相機筒扒,如果你想讓
Google Play
允許將你的應(yīng)用安裝在沒有該功能的設(shè)備上,就要配置硬件功能的權(quán)限為不必須的:<uses-feature android:name="android.hardware.camera" android:required="false" /> - 自定義權(quán)限绊寻。這個可能有些同學沒接觸過霎肯,我們知道,如果我們設(shè)置Activity的
exported
屬性為true榛斯,別人就能通過包名和Activity名訪問我們的Activty观游,那如果我們又不想讓所有人都能訪問我這個Activty呢?可以通過自定義權(quán)限
實現(xiàn)。??來
//應(yīng)用A
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.myapp" >
<permission
android:name="com.test.myapp.permission.DEADLY_ACTIVITY"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous" />
<activity
android:name="MainActivity"
android:exported="true"
android:permission="com.test.myapp.permission.DEADLY_ACTIVITY">
</activity>
</manifest>
//應(yīng)用B
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.otherapp" >
<uses-permission android:name="com.test.myapp.permission.DEADLY_ACTIVITY" />
</manifest>
偏好設(shè)置
建議使用 AndroidX Preference Library 將用戶可配置設(shè)置集成至您的應(yīng)用中驮俗。此庫管理界面懂缕,并與存儲空間交互,因此您只需定義用戶可以配置的單獨設(shè)置王凑。此庫自帶 Material 主題搪柑,可在不同的設(shè)備和操作系統(tǒng)版本之間提供一致的用戶體驗。
開始看到這個標題我是懵逼的索烹,設(shè)置工碾?我的設(shè)置頁官方都可以幫我寫了?然后我就去研究了Preference庫
百姓,嘿渊额,還真是,如果你的App本身就是Material風格
,就可以直接用這個了旬迹。但是也正是由于風格固定火惊,在實際多樣的APP中應(yīng)用比較少。
來個??
implementation 'androidx.preference:preference:1.1.0-alpha04'
//res-xml-setting.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="notifications_category"
app:title="Notifications">
<SwitchPreferenceCompat
app:key="notifications"
app:title="Enable message notifications" />
</PreferenceCategory>
<PreferenceCategory
app:key="help_category"
app:title="Help">
<Preference
app:key="feedback"
app:summary="Report technical issues or suggest new features"
app:title="Send feedback" />
<Preference
app:key="webpage"
app:title="View webpage">
<intent
android:action="android.intent.action.VIEW"
android:data="http://www.baidu.com" />
</Preference>
</PreferenceCategory>
</PreferenceScreen>
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.setting, rootKey)
val feedbackPreference: Preference? = findPreference("feedback")
feedbackPreference?.setOnPreferenceClickListener {
Toast.makeText(context,"hello Setting",Toast.LENGTH_SHORT).show()
true
}
}
}
class SettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_setting)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_container, SettingFragment())
.commit()
}
}
首先新建xml
文件奔垦,也就相當于設(shè)置頁的布局了屹耐,包括那些分類,那些選項椿猎,以及選項的功能惶岭。
然后新建fragment
繼承自PreferenceFragmentCompat
,這里就可以綁定xml文件犯眠,并且可以設(shè)置點擊事件俗他。
最后將fragment加到Activity即可。??
來張效果圖看看
共享
Android 應(yīng)用的一大優(yōu)點是它們能夠互相通信和集成阔逼。如果某一功能并非應(yīng)用的核心兆衅,而且已存在于另一個應(yīng)用中,為何要重新開發(fā)它嗜浮?
這里的共享主要指的是應(yīng)用間的共享
羡亩,比如發(fā)郵件功能,打開網(wǎng)頁功能危融,這些我們都可以直接調(diào)用系統(tǒng)應(yīng)用或者其他三方應(yīng)用來幫助我們完成這些功能畏铆,這也就是共享的意義。
//發(fā)送方
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
//接收方
<activity android:name=".ui.MyActivity" >
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
切片
切片是界面模板吉殃,可以在 Google 搜索應(yīng)用中以及 Google 助理中等其他位置顯示您應(yīng)用中的豐富而動態(tài)的互動內(nèi)容辞居。切片支持全屏應(yīng)用體驗之外的互動,可以幫助用戶更快地執(zhí)行任務(wù)蛋勺。您可以將切片構(gòu)建成為應(yīng)用操作的增強功能瓦灶。
這個介紹確實有點模糊,但是說到Slice
你會不會有點印象抱完?2018年Google I/0宣布推出新的界面操作Action & Slice
贼陶。而這個Slice就是這里說的切片。他能做什么呢巧娱?可以讓使用者能快速使用到 app 里的某個特定功能碉怔。只要開發(fā)者導入 Slice 功能,使用者在使用搜尋禁添、Google Play 商店撮胧、Google Assitant或其他內(nèi)建功能時都會出現(xiàn) Slice
的操作建議。
說白了就是你的應(yīng)用一些功能可以在其他的應(yīng)用顯示和操作老翘。
所以芹啥,如果你的應(yīng)用發(fā)布在GooglePlay
的話锻离,還是可以了解學習下Slice相關(guān)內(nèi)容,畢竟是Google為了應(yīng)用輕便性做出的又一步實驗叁征。
怎么開發(fā)這個功能呢纳账?很簡單逛薇,只需要一步捺疼,右鍵New—other—Slice Provider
就可以了。
slice庫永罚,provider和SliceProvider類都配置好了啤呼,方便吧。貼下代碼:
<provider
android:name=".slice.MySliceProvider"
android:authorities="com.panda.jetpackdemo.slice"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.app.slice.category.SLICE" />
<data
android:host="panda.com"
android:pathPrefix="/"
android:scheme="http" />
</intent-filter>
</provider>
class MySliceProvider : SliceProvider() {
/**
* Construct the Slice and bind data if available.
* 切片匹配
*/
override fun onBindSlice(sliceUri: Uri): Slice? {
val context = context ?: return null
val activityAction = createActivityAction() ?: return null
return if (sliceUri.path.equals("/hello") ) {
Log.e("lz6","222")
ListBuilder(context, sliceUri, ListBuilder.INFINITY)
.addRow(
ListBuilder.RowBuilder()
.setTitle("Hello World")
.setPrimaryAction(activityAction)
)
.build()
} else {
// Error: Path not found.
ListBuilder(context, sliceUri, ListBuilder.INFINITY)
.addRow(
ListBuilder.RowBuilder()
.setTitle("URI not found.")
.setPrimaryAction(activityAction)
)
.build()
}
}
//切片點擊事件
private fun createActivityAction(): SliceAction? {
return SliceAction.create(
PendingIntent.getActivity(
context, 0, Intent(context, SettingActivity::class.java), 0
),
IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground),
ListBuilder.ICON_IMAGE,
"Open App"
)
}
}
如上就是切片的重要代碼呢袱,其中onBindSlice
是用來匹配uri的官扣,比如上述如果uri為/hello就顯示一個ListBuilder。createActivityAction
方法則是響應(yīng)切片點擊事件的羞福。
可以看到在AndroidManifest.xml中是通過provider
配置的惕蹄,所以這個切片的原理就是通過ContentProvider
形式,讓外部可以訪問這個provider治专,然后響應(yīng)相關(guān)事件或者顯示相關(guān)的view卖陵。
好了,接下來就是測試切片使用了张峰,完整的切片URI是slice-content://{authorities}/{action}
泪蔫,所以這里對應(yīng)的就是slice-content://com.panda.jetpackdemo.slice/hello
。
又在哪里可以使用呢喘批?官方提供了一個可供測試的app—slice-viewer撩荣。
下載下來后,配置好URI饶深,就會提示要訪問某某應(yīng)用的切片權(quán)限提示餐曹,點擊確定就可以看到切片內(nèi)容了(注意最好使用模擬器測試,真機有可能無法彈出切片權(quán)限彈窗)敌厘。如下圖凸主,點擊hello就可以跳轉(zhuǎn)到我們之前createActivityAction
方法里面設(shè)置的Activity了。
Jetpack-界面組件
動畫和過渡
當界面因響應(yīng)用戶操作而發(fā)生變化時额湘,您應(yīng)為布局過渡添加動畫卿吐。這些動畫可向用戶提供有關(guān)其操作的反饋,并有助于讓用戶始終關(guān)注界面锋华。
動畫也是老生常談的內(nèi)容了嗡官。說到動畫,我們都會想到幀動畫毯焕,屬性動畫衍腥,補間動畫
等等磺樱。今天我們從不一樣的角度歸類一些那些你熟悉又不熟悉的動畫。
1)為位圖添加動畫
-
AnimationDrawable
婆咸。接連加載一系列可繪制資源以創(chuàng)建動畫竹捉。即屬性動畫,通過設(shè)置每幀的圖像尚骄,形成動畫块差。 -
AnimatedVectorDrawable
。為矢量可繪制對象的屬性添加動畫效果,例如旋轉(zhuǎn)或更改路徑數(shù)據(jù)以將其變?yōu)槠渌麍D片。
其中主要講下AnimatedVectorDrawable遇绞,VectorDrawable
是為了支持SVG而生,SVG 是可縮放矢量圖形鹉动,用xml代碼描繪圖像。下面舉個??
//res-drawable-vectordrawable.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="64dp"
android:width="64dp"
android:viewportHeight="600"
android:viewportWidth="600">
<group
android:name="rotationGroup"
android:pivotX="300.0"
android:pivotY="300.0"
android:rotation="45.0" >
<path
android:name="v"
android:fillColor="#000000"
android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
</group>
</vector>
//res-animator-path_morph.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="3000"
android:propertyName="pathData"
android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z"
android:valueType="pathType" />
</set>
//res-animator-rotation.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="6000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360" />
//利用上面兩個動畫文件和一個SVG圖像宏邮,生成animated-vector可執(zhí)行動畫
//res-drawable-animatiorvectordrawable.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/vectordrawable" >
<target
android:name="rotationGroup"
android:animation="@animator/rotation" />
<target
android:name="v"
android:animation="@animator/path_morph" />
</animated-vector>
//布局文件activity_vector.xml
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:srcCompat="@drawable/animatorvectordrawable"
app:layout_constraintTop_toTopOf="parent"
/>
//activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vector)
imageView.setOnClickListener {
(imageView.drawable as Animatable).start()
}
}
ok泽示,運行后,點擊圖像蜜氨,就會發(fā)現(xiàn)一個繞圈的同時又會自變的動畫了械筛,感覺有點像地球自轉(zhuǎn)和公轉(zhuǎn),感興趣的同學可以自己實現(xiàn)下记劝。
2)為界面可見性和動作添加動畫
這一部分主要就是屬性動畫变姨。屬性動畫的原理就是在一段時間內(nèi)更新 View 對象的屬性,并隨著屬性的變化不斷地重新繪制視圖厌丑。也就是ValueAnimator
定欧,以及在此技術(shù)上衍生的ViewPropertyAnimator
和 ObjectAnimator
。主要運用到控件本身的基礎(chǔ)動畫以及自定義view動畫怒竿。
3)基于物理特性的動作
這部分可以讓動畫應(yīng)盡可能運用現(xiàn)實世界的物理定律砍鸠,以使其看起來更自然。比如彈簧動畫和投擲動畫耕驰。這里舉個彈簧動畫的??
def dynamicanimation_version = "1.0.0"
implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
val springForce = SpringForce(0.0f)
.setDampingRatio(0f) //設(shè)置阻尼
.setStiffness(0.5f) //設(shè)置剛度
imageView2.setOnClickListener {
SpringAnimation(imageView2, DynamicAnimation.TRANSLATION_Y).apply {
spring = springForce
setStartVelocity(500f) //設(shè)置速度
start()
}
}
4)為布局更改添加動畫
借助 Android 的過渡框架爷辱,您只需提供起始布局和結(jié)束布局
,即可為界面中的各種運動添加動畫效果朦肘。也就是說我們只需要提供兩個場景饭弓,代表動畫前后,然后就可以自動生成動畫了媒抠。要注意的是弟断,兩個場景其實在一個頁面中。
//兩個場景的布局
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/scene_root">
<include layout="@layout/a_scene" />
</FrameLayout>
//場景一
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="26sp"
android:id="@+id/text_view1"
android:text="Text Line 1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="26sp"
android:id="@+id/text_view2"
android:text="Text Line 2" />
</LinearLayout>
//場景二
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scene_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_view2"
android:textSize="22sp"
android:text="Text Line 2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:id="@+id/text_view1"
android:text="Text Line 1" />
</LinearLayout>
//獲取場景趴生,開始場景間的動畫阀趴,從場景一變化為場景二
val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)
titletv.setOnClickListener {
TransitionManager.go(anotherScene)
}
5)在 Activity
之間添加動畫
剛才是同一頁面不同場景之間的動畫昏翰,如果是不同頁面呢?也就是不同的Activity之間的動畫呢刘急?更簡單了哈哈棚菊,可以在style
中設(shè)置具體的動畫,也可以直接設(shè)置過渡動畫叔汁,還可以設(shè)置共享控件
完成過渡動畫统求。
//樣式中定義動畫
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
//設(shè)置過渡動畫,可以在兩個布局中設(shè)置共享控件攻柠,android:transitionName="robot"
val intent = Intent(this, Activity2::class.java)
// create the transition animation - the images in the layouts
// of both activities are defined with android:transitionName="robot"
val options = ActivityOptions
.makeSceneTransitionAnimation(this, androidRobotView, "robot")
// start the new activity
startActivity(intent, options.toBundle())
表情符號
EmojiCompat 支持庫旨在讓 Android 設(shè)備及時兼容最新的表情符號球订。它可防止您的應(yīng)用以 ? 的形式顯示缺少的表情符號字符后裸,該符號表示您的設(shè)備沒有用于顯示文字的相應(yīng)字體瑰钮。通過使用 EmojiCompat 支持庫,您的應(yīng)用用戶無需等到 Android OS 更新即可獲取最新的表情符號微驶。
這一模塊就是為了兼容性提供的一個庫:EmojiCompat
浪谴,通過CharSequence文本中的 emoji 對應(yīng)的unicode 編碼
來識別 emoji 表情,將他們替換成EmojiSpans因苹,最后呈現(xiàn) emoji 表情符號苟耻。
//導入庫
implementation "com.android.support:support-emoji:28.0.0"
//初始化
EmojiCompat.Config config = new BundledEmojiCompatConfig(this);
EmojiCompat.init(config);
//替換組件
<android.support.text.emoji.widget.EmojiTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Fragment
Fragment 表示 FragmentActivity 中的行為或界面的一部分。您可以在一個 Activity 中組合多個片段扶檐,從而構(gòu)建多窗格界面凶杖,并在多個 Activity 中重復(fù)使用某個片段。您可以將片段視為 Activity 的模塊化組成部分款筑,它具有自己的生命周期智蝠,能接收自己的輸入事件,并且您可以在 Activity 運行時添加或移除片段(這有點像可以在不同 Activity 中重復(fù)使用的“子 Activity”)奈梳。
片段必須始終托管在 Activity 中杈湾,其生命周期直接受宿主 Activity 生命周期的影響。
我確實沒想到fragment
也被歸入到j(luò)etpack了攘须,哈哈漆撞,這里我就貼一篇我覺得寫得好的文章,雖然文章比較老了于宙,但是可以幫你更深理解Fragment
浮驳。
當然官方也發(fā)布了Fragment的管理框架——Navigation
,感興趣的在本文搜索下即可捞魁。
布局
布局可定義應(yīng)用中的界面結(jié)構(gòu)(例如 Activity 的界面結(jié)構(gòu))至会。布局中的所有元素均使用 View 和 ViewGroup 對象的層次結(jié)構(gòu)進行構(gòu)建。View 通常繪制用戶可查看并進行交互的內(nèi)容署驻。然而奋献,ViewGroup 是不可見容器健霹,用于定義 View 和其他 ViewGroup 對象的布局結(jié)構(gòu)
布局部分主要注意下比較新的兩個布局ConstraintLayout
和MotionLayout
。
-
ConstraintLayout
現(xiàn)在用的已經(jīng)很多了瓶蚂,確實很好用糖埋,特別是復(fù)雜的大型布局,與RelativeLayout屬關(guān)系布局窃这,但是更加靈活瞳别,也可以配合Android Studio的布局編輯器使用,具體用法還是比較多的杭攻,貼上官網(wǎng)鏈接祟敛。 -
MotionLayout
是一種布局類型,可幫助您管理應(yīng)用中的運動和微件動畫兆解。MotionLayout是ConstraintLayout
的子類馆铁,在其豐富的布局功能基礎(chǔ)之上構(gòu)建而成。
所以MotionLayout
就是帶動畫的ConstraintLayout唄锅睛,這里舉個??看看效果:
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta8'
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_01"
tools:showPaths="true">
<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/colorAccent"
android:text="Button" />
</androidx.constraintlayout.motion.widget.MotionLayout>
//scene_01.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<OnSwipe
motion:touchAnchorId="@+id/button"
motion:touchAnchorSide="right"
motion:dragDirection="dragRight" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" >
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#D81B60" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" >
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#9999FF" />
</Constraint>
</ConstraintSet>
</MotionScene>
運行效果如下:
主要是通過app:layoutDescription="@xml/scene_01"
設(shè)定動畫場景埠巨,然后在scene_01場景中就可以設(shè)置起始和結(jié)束位置,動畫屬性现拒,就可以完成對動畫的設(shè)置了辣垒。是不是有點自定義view
那味了,關(guān)鍵這個只需要布局一個xml文件就可以了印蔬!還不試試勋桶?
調(diào)色板
出色的視覺設(shè)計是應(yīng)用成功的關(guān)鍵所在,而配色方案是設(shè)計的主要組成部分侥猬。調(diào)色板庫是一個支持庫例驹,用于從圖片中提取突出顏色,幫助您創(chuàng)建具有視覺吸引力的應(yīng)用陵究。
沒想到吧眠饮,Android還有官方的調(diào)色板庫—Palette
。那到底這個調(diào)色板能做什么呢铜邮?主要用來分析圖片中的色彩特性
仪召。比如圖片中的暗色,亮色松蒜,鮮艷顏色扔茅,柔和色,文字顏色秸苗,主色調(diào)召娜,等等。
implementation 'com.android.support:palette-v7:28.0.0'
//同步分析圖片并獲取實例
fun createPaletteSync(bitmap: Bitmap): Palette = Palette.from(bitmap).generate()
//異步分析圖片并獲取實例
fun createPaletteAsync(bitmap: Bitmap) {
Palette.from(bitmap).generate { palette ->
// Use generated instance
val mutedColor = palette!!.getMutedColor(Color.BLUE)
//主色調(diào)
val rgb: Int? = palette?.vibrantSwatch?.rgb
//文字顏色
val bodyTextColor: Int? = palette?.vibrantSwatch?.bodyTextColor
//標題的顏色
val titleTextColor: Int? = palette?.vibrantSwatch?.titleTextColor
}
}
總結(jié)
終于告一段落了惊楼,大家吃??應(yīng)該吃飽了吧哈哈玖瘸。
希望這篇文章能讓不怎么熟悉Jetpack
的同學多了解了解秸讹。
當然,這還遠遠不夠雅倒,在我看來璃诀,本文更像是一個科普文
,只是告訴了大家jetpack大家庭有哪些成員蔑匣,有什么用處劣欢。實際項目中,我們還需要建立MVVM
的思想裁良,深刻了解每個組件的設(shè)計意義凿将,靈活運用組件。如果大家感興趣价脾,后面我會完整做一個MVVM的項目牧抵,并通過文章的形式記錄整個過程。(附件也有一個項目是官方的Jetpack實踐項目
)
最后希望大家都能通過jetpack
構(gòu)建高質(zhì)量彼棍,簡易并優(yōu)質(zhì)的項目架構(gòu)灭忠,從而解放生產(chǎn)力膳算,成為效率達人
座硕。
附件:
Jetpack實踐官方Demo—Sunflower
文章相關(guān)所有Demo
你的一個??,就是我分享的動力??涕蜂。