compose--附带效应、传统项目集成、导航

该文章将是compose基础系列中最后一篇,附带效应是这篇文章的重点,其余补充内容为如何在传统xml中集成composecompose导航的使用

一、附带效应

有了前面的了解,我们知道compose中是由State状态发生改变来使得可组函数发生重组,状态的改变应该是在可组合函数作用域中,但有时我们需要它发生在别的作用域,如定时弹出一个消息,这就需要附带效应出场了,compose定义了一系列附带效应API,来运用在可组合函数作用域内外,发生状态改变的不同场景

1.LaunchedEffect

LaunchedEffect我们之前就已经使用过了,特别是在低级别动画时,LaunchedEffect用于安全地调用挂起函数,本质就是启动一个协程,LaunchedEffect的调用需要在可组合函数作用域内

LaunchedEffect的执行分为以下三种,优先级由上到下:

  • 当发生重组时LaunchedEffect退出组合,将取消协程
  • 当发生重组时如果LaunchedEffect使用的同一个key,只会执行第一次,如果上次LaunchedEffect没执行结束,不重新执行
  • 当发生重组时如果LaunchedEffect使用的不同的key,并且上次LaunchedEffect没执行结束,则取消上次执行,启动新的协程执行该次任务

例子:

@Preview
@Composable
fun MyLaunchEffect() {
    var state by remember { mutableStateOf(false) }
    var count by remember { mutableStateOf(0) }

    if (state) {
        // key为Unit唯一值
        LaunchedEffect(Unit) {
            delay(3000)
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state = !state }
    ) {
        Text("执行了${count}次")
    }
}

先是点击两下的效果,由于statefalse时,没有LaunchedEffect的代码块,此时LaunchedEffect会取消:

稍微改变下例子的代码,一旦状态发生改变,那么重复执行LaunchedEffect

@Preview
@Composable
fun MyLaunchEffect2() {
    var state by remember { mutableStateOf(0) }
    var count by remember { mutableStateOf(0) }

    if (state > 0) {
        // key为Unit唯一值
        LaunchedEffect(Unit) {
            delay(3000)
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state++ }
    ) {
        Text("执行了${count}次")
    }
}

点击三下的效果,LaunchedEffectkey唯一,重复触发重组,key唯一时只会执行第一次的LaunchedEffect

改变例子代码,每次执行的key不同:

@Preview
@Composable
fun MyLaunchEffect3() {
    var state by remember { mutableStateOf(0) }
    var count by remember { mutableStateOf(0) }

    if (state > 0) {
        // key为随机值
        LaunchedEffect(UUID.randomUUID()) {
            delay(3000)
            // 置为0,防止不断重组导致一直执行LaunchedEffect
            state = 0
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state++ }
    ) {
        Text("执行了${count}次")
    }
}

效果,取消了之前的LaunchedEffect,隔了3秒后才发生count状态改变:

2.rememberCoroutineScope

rememberCoroutineScope也是使用过的,它返回一个remember的协程作用域,可以在可组合函数外使用,调用几次执行几次

例子:

@Preview
@Composable
fun MyRememberCoroutineScope() {
    val scope = rememberCoroutineScope()
    var count by remember { mutableStateOf(0) }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable {
            scope.launch {
                delay(3000)
                count++;
            }
        }
    ) {
        Text("执行了${count}次")
    }
}

效果:

3.rememberUpdatedState

LaunchedEffect一旦启动,同一个key其内部的方法调用和引用都是final的,即无法更改,如果LaunchedEffect内使用的外部引用可能发生改变,应该使用rememberUpdatedState

3.1 不使用remember

先来看一个例子,我在重组时生成一个随机数,并作为onTimeout()的打印参数,将onTimeout()传给MyRememberUpdatedStateLaunchedEffect内调用onTimeout()打印这个随机数:

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成随机数
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印随机数
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
//    val rememberUpdatedState by rememberUpdatedState(onTimeout)
    val rememberUpdatedState = onTimeout

    // 2.key唯一发生重组,不会重新执行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延迟5s,调用外部传入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

我点击多次,这次的效果直接看日志即可:

可以看到最后打印的结果,是第一次生成的随机数

3.2 使用remember

我们尝试使用remember,将onTimeout作为State状态并记住,并以onTimeout作为key使得每次onTimeout发生改变,触发值的更新:

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成随机数
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印随机数
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
//    val rememberUpdatedState by rememberUpdatedState(onTimeout)
    val rememberUpdatedState by remember(onTimeout) { mutableStateOf(onTimeout) }
//    val rememberUpdatedState = onTimeout

    // 2.key唯一发生重组,不会重新执行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延迟5s,调用外部传入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

打印的结果,依然是第一次生成的随机数:

3.3 使用rememberUpdatedState

rememberUpdatedState可以始终保持最新的值,从而改变LaunchedEffect运行时的引用的值

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成随机数
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印随机数
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
    val rememberUpdatedState by rememberUpdatedState(onTimeout)
//    val rememberUpdatedState by remember{ mutableStateOf(onTimeout) }
//    val rememberUpdatedState = onTimeout

    // 2.key唯一发生重组,不会重新执行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延迟5s,调用外部传入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

打印结果:

原理:首先我们知道remember相当于创建了一个静态变量,如果不指定key,只会初始化一次,重复调用remember并不会更新引用,指定key时,当key发生变化,则会更新引用
LaunchedEffect运行时会复制引用,新建变量指向传入的引用,所以此时无论外部变量的引用发生如何改变,并不会改变LaunchedEffect内部变量的引用
rememberUpdatedStateremember的基础上做了更新值处理,每次调用到rememberUpdatedState时,将值更新,也就是引用的值的更新,此时不管外部变量还是LaunchedEffect内部变量的值引用都会发生变化,LaunchedEffect调用的自然就是最新的方法了,下面是rememberUpdatedState的源码:

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

4.DisposableEffect

DisposableEffect可以在key变化和移除时做一些善后工作,需实现onDispose

例子:

@Preview
@Composable
fun MyDisposableEffect() {
    var state by remember { mutableStateOf(false) }
    var text by remember { mutableStateOf("click") }
    val scope = rememberCoroutineScope()

    if (state) {
        // 重组或移除时会调用onDispose
        DisposableEffect(Unit) {
            val job = scope.launch { 
                delay(3000)
                text = "点了"
            }

            onDispose {
                job.cancel()
                text = "取消了"
            }
        }
    }

    Button(onClick = { state = !state }) {
        Text(text)
    }
}

效果,在3s内点击了两次,导致重组时移除DisposableEffect而触发onDispose

5.SideEffect

SideEffect会在可组合函数重组完成时调用,可以进行用户行为分析、日志记录等操作

例子:

@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MySideEffect() {
    var enable by remember { mutableStateOf(false) }

    Column {
        AnimatedVisibility(
            visible = enable,
            enter = scaleIn(tween(2000)),
            exit = scaleOut(tween(2000))
        ) {
            MySideEffectText("hello world")
        }

        Button(onClick = { enable = !enable }) {
            Text("click")
        }
    }
}

@Composable
fun MySideEffectText(text: String) {
    SideEffect {
        Log.i("SideEffect", "重组完成")
    }

    Text(text)
}

效果,如果组件重组完成了,连续点击导致动画重复执行,则不会触发重组:

6.produceState

produceState 会启动一个协程,并返回一个State对象,用来将非 Compose 状态转换为 Compose 状态,即执行一些耗时操作,如网络请求,并将结果作为State对象返回

例子:

@Preview
@Composable
fun MyProduceState() {
    var visiable by remember { mutableStateOf(false) }

    Column {
        if (visiable)
            Text(load().value)

        Button(onClick = { visiable = !visiable }) {
            Text("load")
        }
    }
}

@Composable
fun load(): State<String> {
    return produceState(initialValue = "", producer = {
        delay(2000);

        value = "hi"
    })
}

效果:

7.derivedStateOf

derivedStateOf可以将一个或多个状态对象转变为其他的状态对象,一旦状态发生改变,只会在用到该derivedStateOf状态的地方进行重组

例子,根据传入的list,过滤高亮的元素,并展示到列表中:

val alpha = arrayOf("a", "b", "c", "d", "e", "f", "g", "h")

@Preview
@Composable
fun MyDerivedStateOf() {
    val items = remember { mutableStateListOf<String>() }

    Column {
        Button(onClick = { items.add(alpha[Random.nextInt(alpha.size)]) }) {
            Text("Add")
        }

        DerivedStateOf(items, highPriorityKeywords = listOf("a", "b"))
    }
}

/**
 * 拥有highPriorityKeywords的优先显示
 */
@Composable
fun DerivedStateOf(
    lists: List<String>,
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    // 需要高亮置顶的items
    val highPriorityLists by remember(highPriorityKeywords) {
        derivedStateOf { lists.filter { it in highPriorityKeywords } }
    }

    LazyColumn {
        items(highPriorityLists) { value ->
            Text(value, color = Color.Red)
        }
        items(lists) { value ->
            Text(value)
        }
    }
}

效果:

8.snapshotFlow

snapshotFlow可以将 ComposeState 转为Flow,当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值

@Preview
@Composable
fun MySnapshotFlow() {
    val listState = rememberLazyListState()
    val list = remember {
        mutableListOf<Int>().apply {
            repeat(1000) { index ->
                this += index
            }
        }
    }

    LazyColumn(state = listState) {
        items(list) {
            Text("hi:${it}")
        }
    }

    LaunchedEffect(Unit) {
        snapshotFlow {
            listState.firstVisibleItemIndex
        }.collect { index ->
            Log.i("collect", "${index}")
        }
    }
}

滚动查看日志:

9.重启效应

Compose 中有一些效应(如 LaunchedEffectproduceStateDisposableEffect)会采用可变数量的参数和键来取消运行效应,并使用新的键启动一个新的效应。在实际开发中,灵活运用key是否唯一来使得是否需要重启效应

二、传统项目集成

官方推荐一次性替换整个布局,也可以替换部分布局,本身compose就兼容传统xml的方式,所以在传统的项目上集成compose很容易

1.xml中使用compose

xml中使用ComposeView,表示一个加载compose的控件:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ComposeIntegrateActivity">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="hello android"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/composeView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>

Activity中调用ComposeViewsetContent()方法,并使用compose:

class ComposeIntegrateActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_compose_integrate)

        val composeView = findViewById<ComposeView>(R.id.composeView)
        composeView.setContent {
            MyComposeApplicationTheme {
                MyText1()
            }
        }
    }

    @Composable
    fun MyText1() {
        Text("hi compose")
    }
}

启动效果:

2.fragment中使用

fragment中要多一步绑定View树生命周期:

class BlankFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val disposeOnViewTreeLifecycleDestroyed =
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        val root = inflater.inflate(R.layout.fragment_blank, container, false)
        root.findViewById<ComposeView>(R.id.fragment_composeView).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme() {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
        return root
    }
}

三、导航

compose定义了全新的导航API,下面来开始使用它

1.导入依赖

    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"

2.创建 NavHost

NavHost需要一个navController用于控制导航到那个可组合项,startDestination 初始的可组合项,以及NavGraphBuilder导航关系图

class NaviActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                MyNavi()
            }
        }
    }
}

@Preview
@Composable
fun MyNavi() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") { Home() }
        composable("message") { Message() }
        composable("mine") { Mine() }
    }
}

@Composable
fun Home() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Home")
    }
}

@Composable
fun Message() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Message")
    }
}

@Composable
fun Mine() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Mine")
    }
}

效果:

3.navController

接下来使用navController来导航到不同的可组合项,下面是官方给出的示例的几种方式:

  • 在导航到“friendslist”并加到返回堆栈中
navController.navigate("friendslist")
  • 在导航到“friendslist”之前,将所有内容从后堆栈中弹出到“home”(不包含home)
navController.navigate("friendslist") {
    popUpTo("home")
}
  • 在导航到“friendslist”之前,从堆栈中弹出所有内容,包括“home”
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }
}
  • 只有当我们还不在“search”时,才能导航到“search”目标地,避免在后堆栈的顶部有多个副本
navController.navigate("search") {
    launchSingleTop = true
}

例子:

我们给App添加上Scaffold,并在底部导航栏进行navController导航的控制

class NaviActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                Scene()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Scene() {
    val navController = rememberNavController()

    Surface(Modifier.background(MaterialTheme.colorScheme.surface)) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text(
                            stringResource(id = R.string.app_name),
                            color = MaterialTheme.colorScheme.onPrimaryContainer
                        )
                    },
                    colors = TopAppBarDefaults.smallTopAppBarColors(
                        containerColor = MaterialTheme.colorScheme.primaryContainer
                    )
                )
            },
            bottomBar = {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(MaterialTheme.colorScheme.primaryContainer)
                        .padding(10.dp),
                    horizontalArrangement = Arrangement.SpaceAround
                ) {
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
                        Icon(
                            Icons.Rounded.Home, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("home") {
                                    launchSingleTop = true
                                    popUpTo("home")
                                }
                            }
                        )
                        Icon(
                            Icons.Rounded.Email, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("message") {
                                    launchSingleTop = true
                                    popUpTo("message")
                                }
                            }
                        )
                        Icon(
                            Icons.Rounded.Face, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("mine") {
                                    launchSingleTop = true
                                    popUpTo("mine")
                                }
                            }
                        )
                    }
                }
            }
        ) { paddings ->
            MyNavi(
                modifier = Modifier.padding(paddings),
                navController = navController,
                startDestination = "home"
            ) {
                composable("home") { Home() }
                composable("message") { Message() }
                composable("mine") { Mine() }
            }
        }
    }
}

@Composable
fun MyNavi(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        builder()
    }
}

@Composable
fun Home() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Home")
    }
}

@Composable
fun Message() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Message")
    }
}

@Composable
fun Mine() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Mine")
    }
}

效果:

4.参数传递

Navigation Compose 还支持在可组合项目的地之间传递参数,方式为Restful风格,这种风格的参数为必填:

MyNavi(
modifier = Modifier.padding(paddings),
navController = navController,
startDestination = "home/b1254"
) {
    composable("home/{userId}") { Home() }
    composable("message/{count}") { Message() }
    composable("mine/{userId}") { Mine() }
}

...

// 导航时带入参数
navController.navigate("mine/a1587")

参数类型默认为字符串,也可以通过navArgument指定参数的类型:

composable(
"home/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) { Home() }

通过 lambda 中提供的NavBackStackEntry中提取这些参数:

composable(
"home/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {navBackStackEntry ->
    navBackStackEntry.arguments?.getString("userId")
    Home()
}

可选参数可以使用:?argName={argName} 来添加:

composable(
"message?count={count}",
arguments = listOf(navArgument("count") {
    type = NavType.IntType
    defaultValue = 0
})
) { Message() }

5.深层链接

深层链接照搬了官方文档:深层链接

如果你想要将特定的网址、操作或 MIME 类型与导航绑定,实现对外提供跳转应用的功能,那么使用深层链接可以很方便的实现这个功能

url为例,通过deepLinksurl进行绑定:

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

manifest中注册配置:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

外部通过PendingIntent进行跳转:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

6.封装导航图

随着业务的越来越复杂,导航图也可能分为模块化,可以在NavHost作用域中使用navigation进行封装:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

使用扩展函数将更好的对模块进行封装:

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

推荐阅读更多精彩内容