DataBinding-使用篇

概念

DataBind 就是 基于apt技术,帮我们生成了一些模板代码,这些模板代码大概解决了如下操作:

  1. 控件变量的声明,类似如下:
 @NonNull
 public final TextView tv1;//自动解决了类型匹配的问题,不用担心自己手抖触发类型转换异常了
  1. 控件的查找赋值,相当于自动帮我们完成了类似如下操作:
  tv1 = findViewById(R.id.tv1) //确保了自己脑子卡,忘记给声明的变量赋值,引发空指针
  1. 控制的数据填充操作,也就是其本意数据绑定,类似自动完成了如下操作:
tv1.setText(user.getName())

读前须知

  1. 官网连接:数据绑定库
  2. 本文只讲使用层面的对应解析,不涉及原理流程之类,这点将在下一篇完善
  3. 本文主要基于官网的使用实例,集合kapt生成的相关代码,来吃透用法背后的真实面纱(源码)
  4. 本文不涉及配置,基础引用等,需要有一定的DataBinding使用经验,但是只用但是不知道为啥这么用的大佬

按照官网的顺序一点一点来

最常见的TexView的text填充
< android:text="@{viewModel.name}" />

对应的赋值模板代码如下:

//声明数据对应数据变量
java.lang.String viewModelName = null;
//数据变量赋值
viewModelName = viewModel.getName();
//数据绑定View
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv1, viewModelName);
//TextViewBindingAdapter是DataBinding库给我们提供的一个数据绑定适配器
//这个方法翻译过来就是接管了android:text的属性的赋值逻辑,也就是当遇到上面写法时,会通过下面的静态方法进行赋值
//官方提供的了很多,可以参考着进行更多属性赋值功能扩展,这绝对是最好的copy对象
   @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        //下面实现很贴心,对新老内容进行了比较,防止了重复调用view.setText(text);引起的过渡绘制,很值得我们学习哦
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }
带表达式的
< android:visibility="@{viewModel.age > 10 ? View.VISIBLE : View.GONE}" />

对应的赋值代码块如下(上面只是一个简单的表达式,其实还有很多,但是官方并不太建议在布局写太复杂的表达式,这样会搞的布局文件很乱,按照Jetpack的整体思路,数据的逻辑处理,应该的ViewModel中处理,布局里最好是取最终值就好,这里吐槽一点,有些说法是Databind会搞的布局文件很乱,其实是用复杂了而已,人家官方本意其实指向让你进行数据绑定,并不想让你在布局里做太复杂的数据逻辑)

//声明变量,这个名字很长
int viewModelAgeInt10ViewVISIBLEViewGONE = 0;
//变量赋值
viewModelAgeInt10ViewVISIBLEViewGONE = ((viewModelAgeInt10) ? (android.view.View.VISIBLE) : (android.view.View.GONE));
//数据绑定
// ?咦,这个咋没适配器呢。因为Databind,针对属性有对应setXXX方法会默认调用其setXXX方法就好,无需提供Adapter
//那android:text,也有setText()呀,为啥上面有呢,因为官方觉得那个方法太简单了,所以给提供了更优化的方法,以达到优化目的
//也就是说默认的会调用属性对应的setXXX方法,如果有适配器定制的话就调用定制的
this.tv3.setVisibility(viewModelAgeInt10ViewVISIBLEViewGONE);
Null合并运算符
< android:text="@{viewModel.name??viewModel.lastName}" />

对应的赋值代码块

//声明变量
java.lang.String viewModelNameJavaLangObjectNullViewModelLastNameViewModelName = null;
//给变量赋值,本质还是用了我们三元运算符,也就是这个就是个语法糖
viewModelNameJavaLangObjectNullViewModelLastNameViewModelName = ((viewModelNameJavaLangObjectNull) ? (viewModelLastName) : (viewModelName));
//给View填充数据,讲过了不啰嗦
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv2, viewModelNameJavaLangObjectNullViewModelLastNameViewModelName);
视图引用
< android:text="@{tv3.text}" />

适用两个组件取值一致的场景

androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv3, stringValueOfViewModelAge);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv4, stringValueOfViewModelAge);

其实本质是两个组件使用了统一个数据变量

集合
<!-- 要手动导入,主语xml里不支持‘<’符号,要用&lt代替 -->
<import type="java.util.List"/>
<variable
           name="listdata"
           type="List&lt;String>" />
<!-- 使用 -->
<TextView
android:text="@{viewModel.list[1]}"/>

对应代码生成的代码

import java.util.List;
java.util.List<java.lang.String> viewModelList = null;
java.lang.String viewModelList1 = null;
viewModelList = viewModel.getList();
if (viewModelList != null) {
                        // read viewModel.list[1]
                        viewModelList1 = getFromList(viewModelList, 1);
                    }
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView14, viewModelList1);
//这个方法是ViewDatabinding基类提供的方法,与之对应还有其它集合类方法,用于获取集合中某个索引值用
protected static <T> T getFromList(List<T> list, int index) {
        if (list == null || index < 0 || index >= list.size()) {
            return null;
        }
        return list.get(index);
    }
字符串字面量

这个就简单的说下使用,场景上就是咱们的值是在双引号内的,里面如果需要字面常量时不能再用双引号,要用单引号;当然如果外层用单引号内层就可以用双引号了,总之就是不能同时出现两个双引号

<!-- 外单内双 -->
< android:text='@{"写死的值"}' />
<!-- 外双内单 -->
< android:text="@{map[`firstName`]}" />
//到了编译后就是用的死值,不会为其生成变量
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv5, "写死的值");
资源

主要是资源文件的动态话

<string name="content">I am %s , age is %d</string>
< android:text='@{@string/content(viewModel.name,viewModel.age)}' />
java.lang.String tv6AndroidStringContentViewModelNameViewModelAge = null;
//可以看到本质上还是调用了tv6.getResources().getString()的方法
tv6AndroidStringContentViewModelNameViewModelAge = tv6.getResources().getString(R.string.content, viewModelName, viewModelAge);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv6, tv6AndroidStringContentViewModelNameViewModelAge);
方法引用
事件处理的一种方式,官方的解释是:
  • 在表达式中,您可以引用符合监听器方法签名的方法。当表达式求值结果为方法引用时,数据绑定会将方法引用和所有者对象封装到监听器中,并在目标视图上设置该监听器。如果表达式的求值结果为 null,则数据绑定不会创建监听器,而是设置 null 监听器。
  • 事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定 android:onClick 的方式。与 ViewonClick` 特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或其签名不正确,则会收到编译时错误。
  • 方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您希望在事件发生时对表达式求值,则应使用监听器绑定

好难懂是吧,那我们接下来就根据栗子和源码去理解吧

首先定义一个方法引用,如下
class ListenerHandler {
    fun tvOnClick(view: View){
        Toast.makeText(view.context,"aaaa",Toast.LENGTH_LONG).show()
    }
}

这个啥特点呢就是方法的参数与返回值必须与对应事件的参数类型一致,方法名随意,对应到官方的一句话就是”引用符合监听器方法签名的方法“

<variable
            name="clickHandler"
            type="org.geekbang.databindingtest.ListenerHandler" />

来个错误的示范
fun tvOnClick(view: View) --> fun tvOnClick(context: Context);也就是方法签名搞错了,会咋样呢



会直接有个红线,提示错了,也就是不和规范在编译器就不行了
修正过来的写法

android:onClick="@{clickHandler::tvOnClick}"

那么最终编译出来的相关代码是啥呢

//老样子,根据文件里的东东生成一个变量
android.view.View.OnClickListener clickHandlerTvOnClickAndroidViewViewOnClickListener = null;
private OnClickListenerImpl mClickHandlerTvOnClickAndroidViewViewOnClickListener;
if (clickHandler != null) {
  clickHandlerTvOnClickAndroidViewViewOnClickListener = (((mClickHandlerTvOnClickAndroidViewViewOnClickListener == null) 
       ? (mClickHandlerTvOnClickAndroidViewViewOnClickListener = new OnClickListenerImpl()) 
       : mClickHandlerTvOnClickAndroidViewViewOnClickListener).setValue(clickHandler));
}

翻译下就是,如果mXXX==null,则new OnClickListenerImpl(),否则mXXX.setValue(clickHandler)更新下值
先看否则 clickHandler,很明显就是我们在布局文件中的<variable name="clickHandler">
核心还是那个OnClickListenerImpl,看下源码

public static class OnClickListenerImpl implements android.view.View.OnClickListener{
        private org.geekbang.databindingtest.ListenerHandler value;
        public OnClickListenerImpl setValue(org.geekbang.databindingtest.ListenerHandler value) {
            this.value = value;
            return value == null ? null : this;
        }
        @Override
        public void onClick(android.view.View arg0) {
            this.value.tvOnClick(arg0); 
        }
    }

这个类能解释好多官方解释

  1. 这个类和实例是编译时就创建,对应官方的话:方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的
  2. 这个类里持有一个变量org.geekbang.databindingtest.ListenerHandler value,这个变量的类型是我们的自定义的实现的类型,值也很明显就是我们声明的那个clickHandler;这里对应官方的话:数据绑定会将方法引用和所有者对象封装到监听器中
  3. 接口的实现最终调用的是value对应的方法;这也就能解释通为啥定义的方法一定要符合监听器的方法签名了,也就是参数上要对应好,从这里我们可以发现,其实不一定参数类型完全一致,只要是事件方法参数的子类类型就可以了,不过一般设置接口时就会根据依赖倒置规则确定了类型上不能再具体了。

最后肯定是设置值了,这一些列操作编译时相当于都把代码给我们写好了,至此所谓的方法引用方式绑定事件处理就通了

this.tv1.setOnClickListener(clickHandlerTvOnClickAndroidViewViewOnClickListener);
监听器绑定
也是事件处理的一种方式,官方的解释来一波:
  • 这些是在事件发生时进行求值的 lambda 表达式。数据绑定始终会创建一个要在视图上设置的监听器。事件被分派后,监听器会对 lambda 表达式进行求值。
  • 监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许您运行任意数据绑定表达式。
  • 在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有您的返回值必须与监听器的预期返回值相匹配(预期返回值无效除外)

定义,声明,使用

class ListenerHandler {
   fun onClickByInfo(view:View,text:CharSequence){
        Toast.makeText(view.context,text,Toast.LENGTH_LONG).show()
    }
}
<variable
           name="clickHandler"
           type="org.geekbang.databindingtest.ListenerHandler" />
< android:onClick="@{(view)->clickHandler.onClickByInfo(view,viewModel.name)}" />

理一下生成的相关代码,我们倒着看比较好,这里倒着看下

//给tv设置监听,这里的callBack肯定是一个OnClickListener
this.tv2.setOnClickListener(mCallback1);
// 果不其然,直接就是
@Nullable
 private final android.view.View.OnClickListener mCallback1;
// 那他赋值是谁呢,看下面,这个有两个参数,传了this->ActivityMainBinding,还有一个1?,这个1是干啥的。。。现在不清楚
mCallback1 = new org.geekbang.databindingtest.generated.callback.OnClickListener(this, 1);
//看看这个类的实现
package org.geekbang.databindingtest.generated.callback;
public final class OnClickListener implements android.view.View.OnClickListener {
    final Listener mListener;
    final int mSourceId;
    public OnClickListener(Listener listener, int sourceId) {
        mListener = listener;
        mSourceId = sourceId;
    }
    @Override
    public void onClick(android.view.View callbackArg_0) {
        //我们点击按钮时会调这里,这个的具体实现交给了内部接口实例mListener的_internalCallbackOnClick方法去实现了
        mListener._internalCallbackOnClick(mSourceId , callbackArg_0);
    }
    public interface Listener {
        void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0);
    }
}
接下来就看那个接口的实现在哪里,通过开始的赋值也能才到,实现在ActivityMainBinding,那么就看具体实现
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
        boolean clickHandlerJavaLangObjectNull = false;
        java.lang.String viewModelName = null;
        org.geekbang.databindingtest.ListenerHandler clickHandler = mClickHandler;
        org.geekbang.databindingtest.MainViewModel viewModel = mViewModel;
        boolean viewModelJavaLangObjectNull = false;
        clickHandlerJavaLangObjectNull = (clickHandler) != (null);
        if (clickHandlerJavaLangObjectNull) {
            viewModelJavaLangObjectNull = (viewModel) != (null);
            if (viewModelJavaLangObjectNull) {
                viewModelName = viewModel.getName();
                //上面都是变量声明和检测,其实可以看出来就是各种非空逻辑的判断,要确保不出现空指针
               //这里最终调用了我们定义的方法
                clickHandler.onClickByInfo(callbackArg_0, viewModelName);
            }
        }
    }

从上面源码看,相比方法引用其实就是换了下写法,同时支持非签名参数了而已,因为这些代码也都是编译时都设置好了。
那么这两种看都是将原来的onClick的具体实现最终转给了我们自己写的业务块,只是方法参数上监听器比方法引用更加灵活,这里可以推断出场景选择,点击事件不依赖于数据逻辑时用方法引用就好,如果依赖于数据,比如我们的Rv的Item里的点击需要把Item的data带出去,就可以用监听器引用了。

可观察数据对象

这个其实在实际中都以LiveData代替了,这最终是如何运行的,且看下回分解。我们就通过一个简单的LiveData实例看看有啥相关代码。

val descriptionInfo = MutableLiveData("简介")
< android:text="@={viewModel.descriptionInfo}" />

倒着看看相关代码

//赋值
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvDes, viewModelDescriptionInfoGetValue);
//相关变量声明,能看到有两个变量,一个是值,一个是LiveData
androidx.lifecycle.MutableLiveData<java.lang.String> viewModelDescriptionInfo = null;
java.lang.String viewModelDescriptionInfoGetValue = null;
//值肯定通过LiveData获取的
if (viewModelDescriptionInfo != null) {
    viewModelDescriptionInfoGetValue = viewModelDescriptionInfo.getValue();
}
// LiveData通过变量赋值
if (viewModel != null) {
   // read viewModel.descriptionInfo
   viewModelDescriptionInfo = viewModel.getDescriptionInfo();
 }
//赋值后还有个下面的方法,从这里顺下去应该能找出是如何响应LiveData变化的
 updateLiveDataRegistration(1, viewModelDescriptionInfo);
 /**
  * 更新liveData的的注册器
  * 本地字段id
  * LiveData自己,算是一个observable
  */
protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
        mInLiveDataRegisterObserver = true;
        try {
            //最终给了updateRegistration,参数CREATE_LIVE_DATA_LISTENER,这个方法最其实就是经过关联判断,合理的去给LiveData去更新下观察者,就不展开了
            return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
        } finally {
            mInLiveDataRegisterObserver = false;
        }
    }
  //CREATE_LIVE_DATA_LISTENER是啥?可以看到其本质是LiveDataListener,是ViewDataBinding的静态常量。那就瞅一眼,有点多,我们只关注下核心的
//implements Observer是一个观察者
private static class LiveDataListener implements Observer,
            ObservableReference<LiveData<?>> {
        //一个监听相关的数据包装类,将各个参数存到这里面了
        final WeakListener<LiveData<?>> mListener;
            //略...
            mListener = new WeakListener(binder, localFieldId, this, referenceQueue);
           //略...
           //会给LiveData设置监听
           liveData.observe(newOwner, this);
           //略...
        //响应监听变化
        public void onChanged(@Nullable Object o) {
            ViewDataBinding binder = mListener.getBinder();
            if (binder != null) {
                binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);
            }
        }
    }

从上线我们能看到当生性周期状态变化后,会交由ViewDataBinding的handleFieldChange去响应变化,接下来我们跟一下这条线的主要代码

 //如果字段变化后会走requestRebind
boolean result = onFieldChange(mLocalFieldId, object, fieldId);
        if (result) {
            requestRebind();
        }
 //requestRebind是请求重新绑定的意思,最终绕来绕去会走到executeBindings();而executeBindings是执行绑定的意思,最终会回到我们开始的赋值阶段

从上面看,我们的LiveData数据的管理还是有点复杂的,主要是很多预制的东西,本质上还是注册观察者和响应观察者而已,只是将这一套流程模板化了而已

绑定适配器
自己的理解
  • 这个官网讲述的都已经很详细了,这里主要是做做总结,加深下理解,没兴趣的可以直接看官网。
  • 绑定的实质是设置控件的某个属性,如setText是改变文字,setAlpha是改变透明度,那么要改变这个属性可能需要一些逻辑去定制,比如setText,如果新旧值没有变化就没必要设置了,多刷新一次而已,那么适配器的本质作用就是为设置属性提供自定义实现方式,类似于RecycleView中我们将布局填充交给Adapter一样,这里是我们将某个属性的修改交给了一个Adapter而已。
适配器的定制原则
  1. 理论上任何属性都可以定制
  2. 自动选择:有setter方法的都可以直接搞
    • 有属性有对应的setter方法,可以直接用;例如android:gravity="@{vm.gravity}"
    • 没有属性,但是有setter方法,可以通过app:xxx="xxx"方式使用;例如app:scrimColor="@{@color/scrim}"
  3. 指定自定义方法名称:有属性,但是没有对应的setter,但是呢有其它名称的方法可以单独修改这个属性,可以将属性及方法建立关联即可。例如android:tint这个属性,木有setTint方法,但是有setImageTintList方法是用来设置各个属性的,我们通过一下方法进行关键即可。示例:
    @BindingMethods(value = [
            BindingMethod(
                type = android.widget.ImageView::class,
                attribute = "android:tint",
                method = "setImageTintList")])
    
    其实这个有,但是很少,首先一般写法都是用对应setter方法,及时需要关联的,大多数DataBinding框架已经帮我们建立关联了,发现木有时到androidx.databinding.adapters下找找看。
  4. 完全自定义:这个就是既没有默认,也无法1v1关联的场景了;例如我们有android:paddingLeft属性,但没有该属性的sePaddingLeft,有改变的方法setPadding但这个哥们是四个参数是改四个padding值用的,也就是完全驴唇对不上马嘴,就只能自定义了,这也是我们大多数场景了。
    • 这个主要是单参数,组合参数的情况,官网给了明确的示例,并且官方库也提供了很多封装,比着葫芦画瓢就好。注意在Kotlin中比官方更简单点,因为Kotlin可以用扩展函数实现,省了一个参数的声明
    • @BindingAdapter 其实读一下这个注解的源码及注释就一通百通了
对象转换
  • 我们目标上在绑定数据时需要确保与属性的值类型一致,如我们gravity属性需要一个int,我们给一个字符串类型的肯定不合适。但是某些逻辑下我们得到的可能并不是int类型,这就需要转换了。转换的目的是让值能符合属性设置的类型要求。
  • 自动转换:有些属性可以支持多种类型,比如setText,可以是int的resId,可以是String类型,自动转换的理解就是可以根据给定的值类型自动选择调用哪个方法来设置属性。
  • 自定义转换:就是原始值不满足属性值的类型要求;举两个例子,backGround属性需要Drawable,我们给@color/xxx肯性不行,还有一个较为常见的场景,我们一般拿到的时间戳,但是需要显示的是xxxx年xx月xx日的格式。定义方法比较简单,就是在方法上加个@BindingConversion就可以了。咱们用个例子看下
    // 我们有一个user对象
    val user = User("二胖胖",18)
    
    <!-- 给text直接指定了对象了,理论上我们木有setText(user:User)类型的方法的 -->
    < android:text="@{viewModel.user}" />
    
    我们定义个转义器
    @BindingConversion
    fun convertUserToString(user: User): String? {
        return user.toString()
    }
    
    看下最终表现
    androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv9, org.geekbang.databindingtest.ViewExBindingAdaptersKt.convertUserToString(viewModelUser));
    
    也就是编译器会发现类型不匹配会找对应的covert方法来用
双向绑定
  • 首先这个东西看官网得看几遍,个人觉得不是很好理解
  • 其实掌握以下就可以了
    1. 这个的应用场景就是,某个UI依赖一个数据展示,这个UI的内容变化后会改变这个数值;例如EditText,我们初始值依赖一个变量value,随着输入的变化输入框的值会实时改变value的值,翻译过来就是通过简单的写法可以实现如下两个操作:
       editText.text = value
       editText.addTextChangedListener(object : TextWatcher(){
            override fun afterTextChanged(s: Editable?) {
                value = s.toString()
            }
        })
      
    2. 写法的话就是语法糖@=
      < android:text="@={value}" />
      
    3. 系统提供了大多数使用场景双向特性
    4. 想自定义的话,需要掌握几个注解用法 @BindingAdapter @InverseBindingAdapter @InverseBindingMethods
ViewStub
  • 额...没咋用过...也就没深入研究了,但是这个是布局优化的一个点,有兴趣深究下就好
include
  • 本质只是一个DataBinding的包裹,掌握传值就好了,不过这里写演示实例时遇到个坑,就是在约束布局里的宽高用wrapper不行,了解下就好了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容