是時候更新手里的武器了—Jetpack最全簡析

前言

Android Jetpack想必大家都耳熟能詳了手幢,Android KTXLiveData拱雏,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.jpg

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等等。

官方文檔
Demo代碼地址

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ū)科普下??。

官方文檔
Demo代碼地址

多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()
    

官方文檔
Demo代碼地址

測試

測試應(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植袍。

官方文檔
Demo代碼地址

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)
    }
}    

官方文檔
Demo代碼地址

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ā)送消息击吱,所以也就避免崩潰問題淋淀。

官方文檔
Demo代碼地址

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详炬。
  1. 修改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)看看

官方文檔
Demo代碼地址

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ù)源了昔期。

官方文檔
Demo代碼地址

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)吧!

官方文檔
Demo代碼地址

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()
    }

官方文檔
Demo代碼地址

Jetpack-行為組件

CameraX

CameraX 是一個 Jetpack 支持庫锰什,旨在幫助您簡化相機應(yīng)用的開發(fā)工作下硕。它提供一致且易于使用的 API Surface,適用于大多數(shù) Android 設(shè)備汁胆,并可向后兼容至 Android 5.0(API 級別 21)梭姓。
雖然它利用的是 camera2 的功能,但使用的是更為簡單且基于用例的方法嫩码,該方法具有生命周期感知能力誉尖。它還解決了設(shè)備兼容性問題,因此您無需在代碼庫中添加設(shè)備專屬代碼铸题。這些功能減少了將相機功能添加到應(yīng)用時需要編寫的代碼量释牺。

想必大家都了解過Camera APICamera2 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)限哦。

官方文檔
Demo代碼地址

下載管理器

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ù)庫變化來獲取進度拐辽。

官方文檔
Demo代碼地址

媒體和播放

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)。

官方文檔
Demo代碼地址

通知

通知是指 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即可。??

來張效果圖看看


jetpack-setting.jpg

官方文檔
Demo代碼地址

共享

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了。

slice.jpg

官方文檔
Demo代碼地址

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ù)上衍生的ViewPropertyAnimatorObjectAnimator。主要運用到控件本身的基礎(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())

官方文檔
Demo代碼地址

表情符號

EmojiCompat 支持庫旨在讓 Android 設(shè)備及時兼容最新的表情符號球订。它可防止您的應(yīng)用以 ? 的形式顯示缺少的表情符號字符后裸,該符號表示您的設(shè)備沒有用于顯示文字的相應(yīng)字體瑰钮。通過使用 EmojiCompat 支持庫,您的應(yīng)用用戶無需等到 Android OS 更新即可獲取最新的表情符號微驶。

這一模塊就是為了兼容性提供的一個庫:EmojiCompat浪谴,通過CharSequence文本中的 emoji 對應(yīng)的unicode 編碼來識別 emoji 表情,將他們替換成EmojiSpans因苹,最后呈現(xiàn) emoji 表情符號苟耻。

emoji.png
//導入庫
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)

布局部分主要注意下比較新的兩個布局ConstraintLayoutMotionLayout

  • 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>

運行效果如下:


motionlayout.gif

主要是通過app:layoutDescription="@xml/scene_01"設(shè)定動畫場景埠巨,然后在scene_01場景中就可以設(shè)置起始和結(jié)束位置,動畫屬性现拒,就可以完成對動畫的設(shè)置了辣垒。是不是有點自定義view那味了,關(guān)鍵這個只需要布局一個xml文件就可以了印蔬!還不試試勋桶?

官方文檔
Demo代碼地址

調(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 
        }
    }
    

官方文檔
Demo代碼地址

總結(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


你的一個??,就是我分享的動力??涕蜂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末华匾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子机隙,更是在濱河造成了極大的恐慌蜘拉,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件有鹿,死亡現(xiàn)場離奇詭異旭旭,居然都是意外死亡,警方通過查閱死者的電腦和手機葱跋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門持寄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娱俺,你說我怎么就攤上這事稍味。” “怎么了荠卷?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵模庐,是天一觀的道長。 經(jīng)常有香客問我油宜,道長掂碱,這世上最難降的妖魔是什么怜姿? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮疼燥,結(jié)果婚禮上社牲,老公的妹妹穿的比我還像新娘。我一直安慰自己悴了,他們只是感情好搏恤,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著湃交,像睡著了一般熟空。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搞莺,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天息罗,我揣著相機與錄音,去河邊找鬼才沧。 笑死迈喉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的温圆。 我是一名探鬼主播挨摸,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼岁歉!你這毒婦竟也來了得运?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤锅移,失蹤者是張志新(化名)和其女友劉穎熔掺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體非剃,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡置逻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了备绽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片券坞。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疯坤,靈堂內(nèi)的尸體忽然破棺而出报慕,到底是詐尸還是另有隱情,我是刑警寧澤压怠,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布眠冈,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜗顽。R本人自食惡果不足惜布卡,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雇盖。 院中可真熱鬧忿等,春花似錦、人聲如沸崔挖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狸相。三九已至薛匪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間脓鹃,已是汗流浹背逸尖。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瘸右,地道東北人娇跟。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像太颤,于是被迫代替她去往敵國和親苞俘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353