Jetpack常用組件使用

更多文章可以訪問我的博客Aengus | Blog

Jetpack是Google官方推出的一套Android庫,它幫助開發(fā)者更方便、更快速的開發(fā)出穩(wěn)健性極佳的軟件例隆,簡化開發(fā)流程與提高效率愈诚。Jetpack庫常用的如下幾個組件,它們都可以單獨使用或者組合使用:

組建名稱 介紹
Android KTX Kotlin擴展程序床未,包括擴展函數(shù)、擴展屬性振坚、協(xié)程等
AppCompat 提供向后兼容性的Android組件
WorkManager 管理應用退出或設備重啟時仍應運行等可延遲異步任務
Room Sqlite數(shù)據(jù)庫持久化抽象層
ViewModel 以注重生命周期的方式存儲和管理界面相關的數(shù)據(jù)
LiveData 可觀察到數(shù)據(jù)存儲器類薇搁,在發(fā)生改變時自動更新UI

Android KTX

Android KTX分為多個模塊,每個模塊都含有一個或多個軟件包渡八。Android KTX包含一個核心模塊啃洋,這個模塊為通用框架提供Kotlin擴展程序。如果要在項目中使用核心模塊屎鳍,需要在build.gradle中聲明以下依賴:

dependencies {
    implementation "androidx.core:core-ktx:1.3.0"
}

Android KTX中還含有下面幾個模塊宏娄,它們的依賴聲明如下:

dependencies {
    // Collection KTX
    implementation "androidx.collection:collection-ktx:1.1.0"
    // Fragment KTX
    implementation "androidx.fragment:fragment-ktx:1.2.5"
    // Lifecycle KTX
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    // LiveData KTX
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    // Room KTX
    implementation "androidx.room:room-ktx:2.2.5"
    // SQLite KTX
    implementation "androidx.sqlite:sqlite-ktx:2.1.0"
    // ViewModel KTX
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    // WorkManager KTX
    implementation "androidx.work:work-runtime-ktx:2.3.4"
    // Navigation KTX
    implementation "androidx.navigation:navigation-runtime-ktx:2.3.0-rc01"
    implementation "androidx.navigation:navigation-fragment-ktx:2.3.0-rc01"
    implementation "androidx.navigation:navigation-ui-ktx:2.3.0-rc01"
    // Palette KTX
    implementation "androidx.palette:palette-ktx:1.0.0"
    // Reactive KTX
    implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:2.2.0"
}

ViewModel

當程序退出或者屏幕發(fā)生旋轉導致軟件方向發(fā)生變化時,ActivityFragment都會被銷毀或者重新創(chuàng)建逮壁,此時存儲在其中的任何臨時性頁面相關的數(shù)據(jù)都會丟失孵坚,雖然可以通過Bundle對此類數(shù)據(jù)進行存儲,但是這種方法僅僅適合可以序列化再反序列化的少量數(shù)據(jù)窥淆,而不適合數(shù)量較大的數(shù)據(jù)卖宠。此外,Activity或者Fragment常常需要異步調用(如先需要聯(lián)網(wǎng)下載資源后再顯示在頁面中)祖乳,這些調用可能需要一些時間才能返回結果逗堵。ActivityFragment如果還負責加載數(shù)據(jù),對用戶操作進行響應或處理系統(tǒng)通信眷昆,會使得類越發(fā)膨脹蜒秤,導致后續(xù)的維護及其困難汁咏,所以有必要將界面與數(shù)據(jù)控制分離。

使用ViewModel與LiveData等具有生命周期功能等組件時作媚,可以直接在build.gradle中添加以下依賴:

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// 例子中還使用了協(xié)程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

ViewModel類主要負責為界面準備數(shù)據(jù)攘滩,在界面發(fā)生重新創(chuàng)建時會自動保留ViewModel對象,以便它們存儲的數(shù)據(jù)立即可供下一個ActivityFragment使用纸泡。ViewModel創(chuàng)建如下所示:

data class User(val name: String = "")

class UserViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }
    fun getUserList() = users
    private fun loadUsers() {
        // 加載用戶
    }
}

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      
        val model: UserViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
        // 或者用 val model: UserViewModel by viewModels() 這是activity-ktx的Kotlin屬性委派
        model.getUserList().observe(this, Observer { users ->
            // 更新界面
            textView.text = "${it[0].name} - ${it[1].name} - ${it[2].name}"
        })
    }
}

需要注意的是ViewModel中不能引用視圖漂问、Lifecycle或可能存儲對Activity上下文的引用的任何類,這是因為ViewModel的生命周期比試圖或者LifecycleOwners(在這個例子中是我們傳給ViewModelProvides.of()函數(shù)的對象)更長女揭,所以一旦含有視圖等等引用蚤假,可能會導致內存泄露。ViewModel生命周期如下:

ViewModel生命周期

上面的UserViewModel中并沒有構造參數(shù)吧兔,如果我們想要為ViewModel的構造傳入?yún)?shù)的話磷仰,可以實現(xiàn)我們自己的Factory,并將其作為參數(shù)傳入到ViewModelProviders.of()中就可以境蔼,對類進行以下改造:

class UserViewModel(private val usersCache: List<User>) : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }
    fun getUserList() = users
    private fun loadUsers() {
        // 模擬加載用戶
        GlobalScope.launch(Dispatchers.Main) {
            withContext(Dispatchers.IO) {
                delay(1000L)
            }
            users.value = listOf(User("a"), User("b"), User("c"))
        }
    }
}
// 實現(xiàn)自己的ViewModelFactory
class UserViewModelFactory(private val users: List<User>) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UserViewModel(users) as T
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val cache = listOf(User("x"), User("y"), User("z"))
        val model: UserViewModel = ViewModelProviders.of(this, UserViewModelFactory(cache)).get(UserViewModel::class.java)
        model.getUserList().observe(this, Observer {
            textView.text = "${it[0].name} - ${it[1].name} - ${it[2].name}"
        })
    }
}

這個例子的效果是界面首先顯示x - y - z扭弧,經過一秒后變?yōu)閍 - b - c幻碱。

ViewModelProviders.of()在新版本中已被廢棄唆迁,可以直接使用ViewModelProvider()獲取葵硕。

如果ViewModel需要Application上下文,那么可以繼承AndroidViewModel吴藻,它接收Application作為參數(shù)瞒爬。

LiveData

LiveData是一種可觀察的數(shù)據(jù)存儲類,它和普通的可觀察類的不同之處在于它擁有生命周期感知能力调缨,這種能力可以確保LiveData僅更新處于活躍生命周期狀態(tài)(即處于STARTEDRESUMED狀態(tài))的應用組件觀察者疮鲫。

使用LiveData的步驟如下:

  1. 創(chuàng)建LiveData實例來存儲某種數(shù)據(jù)吆你,通常放在ViewModel中弦叶;
  2. 創(chuàng)建可定義onChanged()方法的Observer對象,該方法會在LiveData對象存儲的數(shù)據(jù)發(fā)生變化時被調用妇多,通過是在Activity中或者Fragment中創(chuàng)建Observer對象伤哺;
  3. 使用LiveData.observe()方法將第二步創(chuàng)建的Observer對象附加到LiveData對象上,并傳入一個LifecycleOwner(通常是ActivityFragment)者祖;

當更新存儲在LiveData中的數(shù)據(jù)時立莉,它會自動觸發(fā)所有已經注冊的觀察者(只要這個觀察者處于活躍狀態(tài))進行更新∑呶剩可以看一下上面ViewModel中的例子蜓耻。

大部分情況下,在組件的onCreate()方法中開始觀察LiveData對象是比較正確的方法械巡,這樣做不僅可以確保系統(tǒng)不會從ActivityFragementonResume()方法進行冗余調用刹淌,還可以確保ActivityFragement變?yōu)榛钴S狀態(tài)后具有可以立刻要顯示的數(shù)據(jù)饶氏。

LiveData規(guī)范的用法如下:

class UserViewModel(userCache: List<User>) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    
    val users: LiveData<List<User>>
        get() = _users
    // 或者
    fun getUserList(): LiveData<List<User>> = _users
}

它與上面的例子不同之處在于ViewModel僅僅對外暴露了不可變的LiveData,而不是MutableLiveData有勾,這樣可以保證View層無法直接對LiveData進行修改疹启,而只能通過ViewModel中的方法進行數(shù)據(jù)更新。

使用Transformations.map()函數(shù)可以將LiveData中存儲的數(shù)據(jù)應用函數(shù)傳到另外一個LiveData對象中蔼卡,該函數(shù)返回LiveData中存儲的數(shù)據(jù)類型:

val userLiveData: LiveData<List<User>> = UserLiveData()
val userNameLiveData: LiveData<List<String>> = Transformations.map(users) { userList ->
    val userNames = mutableListOf<String>()
    userList.forEach {
        userNames.add(it.name)
    }
    userNames
}

使用Transformations.switchMap()函數(shù)可以將LiveData對象解封并應用函數(shù)喊崖,該函數(shù)參數(shù)必須返回LiveData類型:

val userNameLiveData: LiveData<List<String>> = Transformations.switchMap(users) { userList ->
    val result = MutableLiveData<List<String>>()
    val userNames = mutableListOf<String>()
    userList.forEach {
        userNames.add(it.name)
    }
    result.value = userNames
    result
}

MediatorLiveDataLiveData的子類,允許合并多個LiveData源雇逞。只要任何原始的LiveData源對象發(fā)生更改荤懂,就會觸發(fā) MediatorLiveData 對象的觀察者更新。上面的map()函數(shù)與switchMap()返回的實際上都是MediatorLiveData塘砸。

從接口聲明上來看我們似乎可以改寫map使其功能和switchMap類似势誊,這種思路在同步代碼上是可行的,但是異步代碼下用map代替switchMap會報錯谣蠢。下面是一個完整的例子粟耻,使用Retrofit2庫進行網(wǎng)絡請求并將結果顯示在頁面上:

依賴如下:

implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'

ViewModel類文件:

data class Update(val version: String = "1.0.0",
                  @SerializedName("download_url") val downloadUrl: String = "",
                  @SerializedName("update_content") val updateContent: String = "",
                  @SerializedName("have_update") val haveUpdate: String = "0")

class UpdateViewModel(cache: Update) : ViewModel(){
    private var currVersion: MutableLiveData<Update> = MutableLiveData(cache)
    var click = false

    val latestVersion: LiveData<Update> = Transformations.switchMap(currVersion) {
         if (click) {
             Repository.checkUpdate(it.version)
         } else {
             MutableLiveData(cache)
         }
    }

    fun getLatestVersion(version: Update) {
        click = true
        currVersion.value = version
    }
}

class UpdateViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UpdateViewModel(Update()) as T
    }

}

object Repository {
    fun checkUpdate(version: String): LiveData<Update> {
        val liveData = MutableLiveData<Update>()
        GlobalScope.launch(Dispatchers.Main) {
            val deferred = async(Dispatchers.IO) {
                val retrofit = Retrofit.Builder()
                    .baseUrl("https://allpass.aengus.top/api/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                val appService = retrofit.create(AppService::class.java)
                appService.getAll(version).await()
            }
            liveData.value = deferred.await()
        }
        return liveData
}

頁面文件:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val model: UpdateViewModel = ViewModelProvider(this, UpdateViewModelFactory()).get(UpdateViewModel::class.java)
        model.latestVersion.observe(this, Observer {
            textView.text = "${it.version}"
        })

        get_button.setOnClickListener {
            model.getLatestVersion(Update("1.0.0"))
        }
    }
}

軟件運行時會在頁面顯示“1.0.0”,點擊按鈕后變?yōu)椤?.2.2”眉踱。

Room

Room是在SQLite的基礎上的抽象層挤忙,Room使用起來非常像Mybatis,通常的使用方式包含創(chuàng)建數(shù)據(jù)類(與數(shù)據(jù)庫表一一對應)谈喳,創(chuàng)建DAO編寫SQL語句册烈,創(chuàng)建數(shù)據(jù)庫管理DAO,創(chuàng)建Repository控制數(shù)據(jù)庫婿禽。用法如下:

一赏僧、創(chuàng)建數(shù)據(jù)類。下面注解中的tableNamename都可以省略扭倾,若省略則默認和類名與屬性名相同淀零;

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "username") val username: String = "",
    @ColumnInfo(name = "password") val password: String = ""
)

二、創(chuàng)建DAO膛壹。

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>> // Room內有對LiveData的支持
    
    @Insert
    fun insert(user: User)
    
    @Insert
    fun insertAll(vararg users: User)
    
    @Update
    fun update(user: User)
    
    @Delete
    fun delete(user: User)
}

三驾中、創(chuàng)建數(shù)據(jù)庫類。version指定數(shù)據(jù)庫版本模聋,將來升級時基于此肩民。

@Database(entities = [User::class], verson = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null
        
        fun getDatabase(context: Context, scope: CoroutineScope): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    name = "users_demo"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

四、創(chuàng)建Repository链方。

class UserRepository(private val userDao: UserDao) {
    val userList = userDao.getAllUsers()
    
    fun insert(user: User) = userDao.insert(user)
    
    fun update(user: User) = userDao.update(user)
    
    fun delete(user: User) = userDao.delete(user)
}

五持痰、使用。下面的AndroidViewModel在前面說過祟蚀。

class UserViewModel(application) : AndroidViewModel(application) {
    private val repository: UserRepository
    val usersLiveData: LiveData<User>
    
    init {
        val userDao = UserDatabase.getDatabase(application, viewModelScope).userDao()
        repository = UserRepository(userDao)
        userLiveData = repository.userList
    }
    
    fun insert(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.insert(user)
    }
    
    fun update(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.update(user)
    }
    
    fun delete(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.delete(user)
    }
}

Room數(shù)據(jù)庫升級的步驟是:首先需要更改數(shù)據(jù)庫版本工窍,然后定義一個Migration對象占调,并在databaseBuilder()后運行:

@Database(entities = [User::class], verson = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    val MIGRATION_1_2: Migration = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE users ADD COLUMN age INT")
        }
    }
    
    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null
        
        fun getDatabase(context: Context, scope: CoroutineScope): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    name = "users_demo"
                )
                .addMigrations(MIGRATION_1_2)   // 在此使用Migration對象
                .build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

利用同樣的方法可以定義從2-3,3-4移剪,或者1-4的數(shù)據(jù)庫版本遷移究珊。

除了addMigrations(vararg migrations: Migration)方法,還有fallbackToDestructiveMigrationFrom(varargs startVersions: Int)fallbackToDestructiveMigration()纵苛,這兩個函數(shù)允許Room當要求的數(shù)據(jù)庫版本與實際的數(shù)據(jù)庫版本不同時破壞性的重新創(chuàng)建數(shù)據(jù)庫表剿涮,不同之處在于前者可以從指定版本號的數(shù)據(jù)庫表進行遷移。

WorkManager

WorkManager可以用來管理即使在應用退出后或者設備重啟時仍運行的任務攻人,支持一次性或周期性任務取试,并可以添加網(wǎng)絡可用性或充電狀態(tài)等約束,遵循省電模式等功能怀吻。WorkManager不適合應用結束后進行安全終止(如應用數(shù)據(jù)保存)的后臺工作瞬浓,也不適合需要立刻執(zhí)行的工作

要使用WorkManager蓬坡,需要在build.gradle中添加如下依賴:

implementation "androidx.work:work-runtime:2.3.4"   // Java
implementation "androidx.work:work-runtime-ktx:2.3.4"  // Kotlin + coroutines

使用WorkManager猿棉,常常有如下幾個步驟:

一、創(chuàng)建任務Worker屑咳,重寫doWork()函數(shù)返回Result萨赁,失敗使用Result.failure(),需要重試使用Result.retry()兆龙。

class MyWorker(appContext: Context, workerParams: WorkerParams) : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 做一些工作
        return Result.success() // 任務成功
    }
}

二杖爽、配置運行任務的方式和時間。

val myWorkRequest = OneTimeWorkRquestBuilder<MyWorker>() // 一次性的
val anotherWorkRequest = PeriodicWorkRequest<MyWorker>() // 周期性的

PeriodicWorkRequest()會不斷運行直到任務被取消紫皇,更精細的操作慰安,需要使用PeriodicWorkRequestBuilder()可以定義的最短重復時間間隔為15分鐘聪铺。示例如下:

val constraints = Constraints.Builder()
        .setRequiresCharging(true)  // 約束為充電狀態(tài)
        .build()
// 一小時執(zhí)行一次化焕,但是必須在接通電源時運行
val saveRequest = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints).build()

Contraints.Builder()還有以下方法:

// 當一個本地content(Uri)更新時任務是否需要運行
addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean)
// 任務運行是否要求設備處于特定網(wǎng)絡模式
setRequiredNetworkType(networkType: NetworkType)
// 任務運行是否要求設備不處于低電量模式
setRequiredBatteryNotLow(requiresBatteryNotLow: Boolean)
// 任務運行是否要求設備處于空閑時
setRequiredDeviceIdle(requiresDeviceIdle: Boolean)
// 任務運行是否要求設備具有較多的存儲空間
setRequiredStorageNotLow(requiresStorageNotLow: Boolean)
// 任務預定好時,content: Uri第一次發(fā)生改變后的延時
setTriggerContentMaxDelay(duration: Duration)
setTriggerContentMaxDelay(duration: Long, timeUnit: TimeUnit)
// 任務預定好時计寇,content: Uri發(fā)生改變后的延時
setTriggerContentUpdateDelay(duration: Duration)
setTriggerContentUpdateDelay(duration: Long, timeUnit: TimeUnit)

一次性任務和周期性任務都可以使用setInitialDelay(duraiton: Long, timeUnit: TimeUnit)設置初始延遲(注意調用后還需要調用build()才能返回WorkRequest)锣杂。

可以用setBackoffCriteria(backoffPolicy: BackoffPolicy, backoffDelay: Long, timeUnit: TimeUnit)設置退避延遲政策,也就是兩個任務沖突時如需要重試番宁,那么在重試工作前需要等待的最短時間,退避政策有BackoffPolicy.EXPONENTIALBackoffPolicy.LINEAR赖阻,前者代表指數(shù)增長等待時間蝶押,后者代表線性增長等待時間;backoffDelay是等待時間延遲火欧,對于一次性任務和周期性任務分別有它們的MIN_BACK_MILLIS=5*60*1000MAX_BACKOFF_MILLIS=5*60*60*1000棋电。

任務也可以有輸入輸出茎截,輸入輸出都是以鍵值對的形式存儲在Data對象中,示例如下:

val inputData = workDataof("key" to "value")
// 輸入數(shù)據(jù)
val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
            .setInputData(inputData)
            .build()
// 獲取輸出數(shù)據(jù)
class MyWorker(appContext: Context, workerParams: WorkerParams) : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 做一些工作
        return Result.success(outputData) // 任務成功赶盔,獲取輸出數(shù)據(jù)
    }
}

可以使用addTag(tag: String)WorkRequest添加標簽企锌,并使用WorkManager.cancelAllWorkByTag(tag: String)取消特定標簽的任務。

三于未、將任務提交給系統(tǒng)撕攒。

WorkManager.getInstance(myContext).enqueue(myWorkRequest)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烘浦,隨后出現(xiàn)的幾起案子抖坪,更是在濱河造成了極大的恐慌,老刑警劉巖闷叉,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擦俐,死亡現(xiàn)場離奇詭異,居然都是意外死亡握侧,警方通過查閱死者的電腦和手機蚯瞧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來品擎,“玉大人状知,你說我怎么就攤上這事∧醪椋” “怎么了饥悴?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盲再。 經常有香客問我西设,道長,這世上最難降的妖魔是什么答朋? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任贷揽,我火速辦了婚禮,結果婚禮上梦碗,老公的妹妹穿的比我還像新娘禽绪。我一直安慰自己,他們只是感情好洪规,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布印屁。 她就那樣靜靜地躺著,像睡著了一般斩例。 火紅的嫁衣襯著肌膚如雪雄人。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天念赶,我揣著相機與錄音础钠,去河邊找鬼恰力。 笑死,一個胖子當著我的面吹牛旗吁,可吹牛的內容都是我干的踩萎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼很钓,長吁一口氣:“原來是場噩夢啊……” “哼香府!你這毒婦竟也來了?” 一聲冷哼從身側響起履怯,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤回还,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后叹洲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柠硕,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年运提,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝗柔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡民泵,死狀恐怖癣丧,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情栈妆,我是刑警寧澤胁编,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站鳞尔,受9級特大地震影響嬉橙,放射性物質發(fā)生泄漏。R本人自食惡果不足惜寥假,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一市框、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糕韧,春花似錦枫振、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乒疏,卻和暖如春额衙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怕吴。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工窍侧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人转绷。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓伟件,卻偏偏與公主長得像,于是被迫代替她去往敵國和親议经。 傳聞我的和親對象是個殘疾皇子斧账,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345