Jeptpack Compose 官网教程学习笔记(一)

Jetpack Composed

Jetpack Composed创建Jetpack Compose项目基础知识可组合函数预览功能背景颜色设置重复使用可组合项布局Compose状态状态提升列表保留状态添加动画效果设置应用的样式和主题

用于构建原生Android界面的新工具包。可以简化并加快Android上的界面开发,帮助使用更少的代码、强大的工具和直观的Kotlin API,快速打造生动而精彩的应用,使用了声明式编程范式,在前端主流框架中【如:Vue、React】声明式界面设计普遍存在,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计

优势

  • 更少的代码

    • 使用更少的代码实现更多的功能,并且可以避免各种bug,从而使得代码简洁且易于维护
  • 直观

    • 只需描述界面,Compose会负责处理剩余的工作。应用状态变化时,界面会自动更新
  • 加快应用开发

    • 兼容现有的所有代码,方便随时随地采用。借助实时预览和全面的Android Studio支持,实现快速迭代
  • 功能强大

    • 凭借对Android平台API的直接访问和对于Material Design、深色主题、动画等的内置支持,创建精美的应用

声明性编程范式

长期以来,Android 视图层次结构一直可以表示为界面控件树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById()等函数遍历树,并通过调用button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap)等方法更改节点。这些方法会改变控件的内部状态。

手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以意外的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护复杂性会随着需要更新的视图数量而增长。

在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。

重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减轻这一成本,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。

参考资料

创建Jetpack Compose项目

在使用Jetpack Composed时需要更新AndroidStudio,此处学习使用版本如下:

Android Studio Bumblebee | 2021.1.1 Patch 2

新建项目选择Empty Compose Activty

image-20220508110549005.png

build文件差异

project build.gradle

buildscript {
    ext {
        //规定Jetpack Compose版本
        compose_version = '1.0.1'
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.1.2' apply false
    id 'com.android.library' version '7.1.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.5.21' apply false
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

与普通的kotlin应用项目的build.gradle文件相比就增加:

buildscript {
   ext {
       compose_version = '1.0.1'
   }
}
...
task clean(type: Delete) {
   delete rootProject.buildDir
}

app build.gradle文件

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    ...
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {
    //默认添加的依赖
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

从添加的依赖文件中可以看出jetpack compose中内置了协程

image-20220508110409678.png

基础知识

可组合函数

Jetpack Compose是围绕可组合函数构建的。这些函数可以让您以程序化方式定义应用的界面,只需描述应用界面外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素,将其附加到父项等)

可组合函数是带有 @Composable 注解的常规函数。这类函数自身可以调用其他 @Composable 函数

@Composable
private fun Greeting(name: String) {
   Text(text = "Hello $name!")
}

其中Text 是由库提供的可组合函数

@Composable
fun Text(
    ...
){...}

在使用 Compose 时,Activities 仍然是 Android 应用的入口点,通过setContent()定义布局,但与传统使用xml文件不同,我们可以在该函数中调用可组合函数

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text(text = "Fucking World")
        }
    }
}

在此项目中导入与 Jetpack Compose 相关的类时,请从以下位置导入:

  • androidx.compose.*(针对编译器和运行时类)
  • androidx.compose.ui.*(针对界面工具包和库)

Jetpack Compose不支持android.support.*

预览功能

若要使用Android Studio预览,通过 @Preview 注解标记所有无参数可组合函数或采用默认参数的函数

@Composable
fun Greeting(name: String) {
    Text(text = "Fucking $name!")
}

@Preview
@Composable
fun NoParamPreview() {
    Greeting("World")
}

@Preview
@Composable
fun DefaultParamPreview(name:String="World") {
    Greeting(name)
}
image-20220508113519093.png

@Preview注解还有很多参数:

具体的可以看源码中的注释,在此处标注出常用的参数及其作用

@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(
   AnnotationTarget.FUNCTION
)
@Repeatable
annotation class Preview(
   //预览的显示名称,没有指定就是方法名
   val name: String = "",
   //组名,进行分组时使用做到仅显示其中一个或多个
   val group: String = "",
   //渲染带注释的@Composable 时使用的 API 级别
   @IntRange(from = 1) val apiLevel: Int = -1,
   //设置渲染窗口的宽度和高度
   val widthDp: Int = -1,
   val heightDp: Int = -1,
   val locale: String = "",
   @FloatRange(from = 0.01) val fontScale: Float = 1f,
   //是否显示设备的状态栏和操作栏
   val showSystemUi: Boolean = false,
   //是否显示背景
   val showBackground: Boolean = false,
   //背景颜色,若showBackground为false则不生效
   val backgroundColor: Long = 0,
   @UiMode val uiMode: Int = 0,
   @Device val device: String = Devices.DEFAULT
)

背景颜色设置

如果只能干巴巴的使用Android原生控件,UI界面绝对不会好看。那么如何对控件进行修饰呢?此处以背景颜色设置举例

1.通过Modifier修饰符修改

大多数 Compose 界面元素(例如 SurfaceText)都接受可选的 modifier 参数。修饰符会指示界面元素如何在其父级布局中放置、显示或表现

@Composable
fun GreetingWithModify(name: String = "World") {
    Text(
        text = "Fucking $name!",
        modifier = Modifier
            .background(Purple200)
            .padding(16.dp)
    )
}

相当于xml文件中修改background值,只不过是通过Modifier进行设置

Modifier中可以设置控件的通用属性,如:padding、background、width、height等

2.通过布局设置颜色

@Composable
fun GreetingWithSurface(name:String="World") {
    Surface(color = Purple200) {
        Text(text = "Fucking $name!")
    }
}

这种方式相当于父控件设置背景颜色,若子控件没有设置背景颜色就会延用父控件的背景颜色

<FrameLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/purple_200">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fucking World!" />
</FrameLayout>

其中SurfaceMaterial Design相关概念,会在之后介绍。通过查看源码可以知道Surface也是通过Modifier设置背景颜色的

Purple200是项目创建时默认生成的值,位置为包名.ui.theme.Color.kt

image-20220508141545945.png

重复使用可组合项

添加到界面的组件越多,创建的嵌套层级就越多。如果函数变得非常大,可能会影响可读性。通过创建可重用的小型组件,可以轻松构建应用中所用界面元素的库。每个组件对应于屏幕的一个部分,可以单独修改

布局

既然我们知道如何去创建一个控件,那么如何将控件有序的摆放呢?就像是 xml 中的Linear Layout一样

在 Compose 中有三个基本标准布局元素: ColumnRowBox 可组合项

fbd450e8eab10338.png

Column可以视为android:orientation="vertical"LinearLayout

Row可以视为android:orientation="horizontal"LinearLayout

Box可以视为FrameLayout,对控件进行重叠摆放

例:

@Preview(group = "1.3", widthDp = 320, showBackground = true, backgroundColor = 0xFFFFFF)
@Composable
fun ColumnPreview() {
    ColumnGreeting()
}

@Composable
fun ColumnGreeting() {
    Column(
        modifier = Modifier
            //相当于外边距,margin
            .padding(12.dp)
            .background(Teal200)
            .fillMaxWidth()
            //相当于内边距,padding
            .padding(12.dp)
    ) {
        listOf("world", "World", "WORLD").forEach {
            RowGreetingContent(it)
        }
    }
}

@Composable
fun RowGreetingContent(name: String) {
    Row(modifier = Modifier.background(Purple200)) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(horizontal = 8.dp)
        ) {
            Text(text = "Hello", modifier = Modifier)
            Text(text = name, modifier = Modifier)
        }
        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier.weight(1f)
        ) {
            Text(text = "Button")
        }
    }
}
image-20220508144843783.png

Modifier中没有margin属性,margin效果的实现就需要通过padding的执行顺序实现

Compose状态

之前实现的控件都是静态的,无法响应事件与用户进行交互。那么该如何让控件响应时间呢?比如实现以下效果:
[图片上传失败...(image-45f5c8-1652016292431)]
很显然在响应事件前,我们应该设置变量用来记录事件状态类型

@Composable
fun ColumnStateContent(name: String) {
    //错误
    var isExpand = false
    Row(
        modifier = Modifier
            .padding(4.dp)
            .background(Purple200)
            .padding(16.dp)
    ) {
        Column(modifier = Modifier.weight(1f)) {
            Text(text = "Fucking,")
            Text(text = name)
        }
        OutlinedButton(onClick = { isExpand = !isExpand }) {
            Text(text = if (isExpand) "show less" else "show more")
        }
    }
}

然而这种方式无法按预期发挥作用,为 isExpand 变量设置不同的值不会使 Compose 将其检测为状态更改,因此不会产生任何效果。

更改此变量不会触发重组的原因是 Compose 并未跟踪此更改。此外,每次调用 ColumnStateContent 时,都会将该变量重置为 false。

如需向可组合项添加内部状态,您可以使用 mutableStateOf 函数,该函数可让 Compose 重组读取该 State 的函数。

StateMutableState 是两个接口,它们具有特定的值,每当该值发生变化时,它们就会触发界面更新(重组)

但是,不能将mutableStateOf 分配给可组合项中的某个变量,因为重组可能会随时发生,会再次调用可组合项函数ColumnStateContent,会导致isExpand 重置为值为 false 的新可变状态

@Composable
fun ColumnStateContent(name:String) {
    //错误
    var isExpand = mutableStateOf(false)
    ...
}

如需在重组后保留状态,请使用 remember 记住可变状态,然后添加一个依赖于状态的额外变量用于内边距距离修改

@Composable
fun ColumnStateContent(name: String) {
    val isExpand = remember { mutableStateOf(false) }
    val paddingValue = if (isExpand.value) 48.dp else 0.dp
    Row(
        modifier = Modifier
            .padding(4.dp)
            .background(Purple200)
            .padding(16.dp)
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(bottom = paddingValue)
        ) {
            Text(text = "Fucking,")
            Text(text = name)
        }
        OutlinedButton(onClick = { isExpand.value = !isExpand.value }) {
            Text(text = if (isExpand.value) "show less" else "show more")
        }
    }
}

使用remember记录extraPadding,因为该设置依赖于状态,只需执行一个简单的计算即可

通过状态的作用方式,可以看除Compose的重组会重新调用需要修改的Composable函数

状态提升

在可组合函数中,被多个函数读取或修改的状态应位于共同父实体中,此过程称为状态提升

状态提升,可以避免复制状态和引入bug,有助于重复使用可组合项,并大大降低可组合项的测试难度。相反,不需要由可组合项的父级控制的状态则不应该被提升。可信来源属于该状态的创建者和控制者

例:

通过按钮切换显示的界面

@Composable
fun StateHoistScreen() {
    var isShow by remember { mutableStateOf(true) }
    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = { isShow = false }
            ) {
                Text("Continue")
            }
        }
    }
}

isShow 使用的是 by 关键字,而不是=。使用了委托可以无需每次都输入 .value来获取值

但是在父实体中无法获取isShow状态

@Composable
fun MyApp() {
    //编译器:干嘛啊,小老弟,没这玩意!
    if (isShow) {
        StateHoistScreen()
    }else{
        ColumnState()
    }
}

此时就需要提升状态,将isShow状态移至需要访问它的共同父实体中,在本案例中一共有两个可组合函数需要该状态【MyAppStateHoistScreen】,其共同父实体就是MyApp,所以将isShow状态提升到MyApp

同时我们需要和StateHoistScreen共享isShow状态

1.通过传递MutableState实现

@Composable
fun StateHoistScreen(isShow:MutableState<Boolean>) {
    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = {
                    isShow.value=false
                }
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
fun MyApp() {
    val isShow = remember { mutableStateOf(true) }
    if (isShow.value) {
        StateHoistScreen (isShow)
    } else {
        ColumnState()
    }
}

不推荐这种写法,因为这会导致业务逻辑在各个可组合函数中分布,代码逻辑混乱不利于维护。同时也将业务固定,不利于可组合函数的可重用性

2.通过向下传递回调

@Composable
fun StateHoistScreen(onClick: () -> Unit) {
    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onClick
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
fun MyApp() {
    var isShow by remember { mutableStateOf(true) }
    if (isShow) {
        StateHoistScreen {
            isShow = false
        }
    } else {
        ColumnState()
    }
}

推荐向下传递回调方式,可以提高该可组合项的可重用性,并防止状态被其他可组合项更改

这种方式的使用类似于onClickListener回调

列表

Android应用中列表的使用非常常见,那么对于多条数据Compse如何实现的呢?

@Composable
fun PluralGreeting() {
    Column {
        (0..1000).forEach {
            Greeting(name = "$it")
        }
    }
}

这种方式很显然不可取,即使屏幕上放不下这些问候语,仍会去加载这些控件导致应用启动很慢,甚至在模拟器中直接卡死。并且这种方式创建的列表不能进行滚动

如何让列表进行滚动呢?可以通过Modifier.verticalScroll设置滚动状态

@Composable
fun PluralGreeting() {
    val rememberScrollState = rememberScrollState()
    Column(modifier = Modifier.verticalScroll(rememberScrollState)) {
        (0..1000).forEach {
            Greeting(name = "$it")
        }
    }
}

但是加载控件速度慢的缺点还是没有解决

为了显示可滚动列和加载速度考虑,Compose提供了LazyColumnLazyColumn 只会渲染屏幕上可见的内容,从而在渲染大型列表时提升效率

LazyColumnLazyRow 相当于 Android View 中的 RecyclerView

LazyColumn API 会在其作用域内提供一个 items 元素,并在该元素中编写各项内容的渲染逻辑:

@Composable
fun LazyColumnPluralGreeting() {
    LazyColumn{
        items(items = (0..1000).toList()) { index ->
            Greeting(name = "$index")
        }
    }
}

LazyColumn 不会像 RecyclerView 一样回收其子级。它会在您滚动它时发出新的可组合项,并保持高效运行,因为与实例化 Android Views 相比,发出可组合项的成本相对较低

保留状态

运行应用,旋转屏幕,更改为深色模式等情况会重启整个Activity,而remember 函数仅在可组合项包含在组合中时起作用,重启Activity的情况会导致所有状态丢失。

我们可以使用 rememberSaveable,而不使用 remember。这会保存每个在配置更改(如旋转)和进程终止后保留下来的状态。

添加动画效果

在 Compose 中,有多种方式可以为界面添加动画效果:从用于添加简单动画的高阶 API 到用于实现完全控制和复杂过渡的低阶方法,具体的动画会在之后进行介绍

使用 animateDpAsState 可组合项。该可组合项会返回一个 State 对象,该对象的 value 会被动画持续更新,直到动画播放完毕。该可组合项需要一个类型为 Dp 的“目标值”

@Composable
fun MyApp() {
    LazyColumn {
        items(items = (0..1000).toList()) { index ->
            AnimationGreeting(name = "$index")
        }
    }
}

@Composable
fun AnimationGreeting(name: String) {
    var isExpand by remember { mutableStateOf(false) }
    //animateDpAsState返回State<DP>类型
    val paddingValue by animateDpAsState(if (isExpand) 48.dp else 0.dp)

    Row(
        modifier = Modifier
            .padding(4.dp)
            .background(Purple200)
            .padding(16.dp)
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(bottom = paddingValue)
        ) {
            Text(text = "Fucking,")
            Text(text = name)
        }
        OutlinedButton(onClick = { isExpand = !isExpand }) {
            Text(text = if (isExpand) "show less" else "show more")
        }
    }
}

如果您展开第 1 项内容,然后滚动到第一项内容离开屏幕,再返回到第 1 项内容,您会发现第 1 项内容已恢复为原始尺寸。如果需要,您可以使用 rememberSaveable 保存此数据

设置应用的样式和主题

如果您打开 ui/Theme.kt 文件,您会看到 [应用名]Theme 在其实现中使用了 MaterialTheme

@Composable
fun StudyOfJetpackComposeTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

MaterialTheme 是一个可组合函数,体现了 Material Design 规范中的样式设置原则。样式设置信息会逐级向下传递到位于其 content 内的组件,这些组件会读取该信息来设置自身的样式。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            StudyOfJetpackComposeTheme{
                AnimationPreview()
            }
        }
    }
}

由于 [应用名]ThemeMaterialTheme 包围在其内部,因此 MyApp 会使用该主题中定义的属性来设置样式。从任何后代可组合项中都可以检索 MaterialTheme 的三个属性:colorstypographyshapes

@Composable
fun ThemeText() {
    StudyOfJetpackComposeTheme {
        Text(
            text = "Fucking World",
            style = MaterialTheme.typography.h4,
            color = MaterialTheme.colors.onSurface
        )
    }
}

上例中的 Text 可组合项会设置新的 TextStyle。您可以创建自己的 TextStyle,也可以使用 MaterialTheme.typography 检索主题定义的样式(首选)。此结构支持您访问 Material 定义的文本样式,例如 h1-h6body1,body2captionsubtitle1

通常来说,最好是将颜色、形状和字体样式放在 MaterialTheme 中。例如,如果对颜色进行硬编码,将会很难实现深色模式,并且需要进行大量修正工作,而这很容易造成错误。

Compose 还可以基于现有的颜色或样式进行设置,通过copy 函数修改预定义的样式,只会修改传入的参数类型的值,其余值不变

@Composable
fun ThemeText() {
    Text(
        text = "Fucking World",
        style = MaterialTheme.typography.h4.copy(fontWeight = FontWeight.Bold),
        color = MaterialTheme.colors.onSurface.copy(alpha = .2f)
    )
}

除此之外在预览中可以设置显示的模式

@Preview(
    showBackground = true,
    widthDp = 320,
    //设置为深色主题
    uiMode = UI_MODE_NIGHT_YES
)
@Composable
fun DefaultPreview() {
    StudyOfJetpackComposeTheme {
        ThemeText()
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容