Data Binding 学习笔记

Data Binding 学习笔记

在Android系统中使用Java编写逻辑代码,而使用XML表示界面View,从而使得控制逻辑与显示界面分离,实现一定程度的解耦。但是,界面显示部分与控制逻辑还是有一定的数据和事件的联系,在通常的编码中是在Java逻辑代码中获取XML中的控件,并在代码中为其设置属性以及事件监听器等方式实现控制界面View显示的目的。

Data Binding则是通过在XML文件中引入对象变量,使用变量控制XML各个控件的属性以及事件,并在代码中为每个XML文件生成ViewDataBinding对象,从而实现代码对XML的更好控制。逻辑对界面的控制无非包括控件属性和控件响应事件两个方面,分别对应着数据和事件两个方面,下面从数据绑定,事件绑定以及双向数据绑定三个方面介绍Data Binding的相关内容。

数据绑定

首先介绍一下DataBinding的基本用法,在Android Studio 1.3之后,可以在module的build.gradle文件中直接进行如下配置:

    //module的build.gradle
    android {
        ....
        dataBinding {
               enabled = true
        }
    }

Data Binding 是在XML文件中引入对象变量,XML文件的形式有所改变,下面为官方文档中的实例:

    <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="user" type="com.recluseguo.recluseandroid.data.User"/>
    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"/>
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}"/>
    </LinearLayout>
</layout>

XML 文件根标签为<layout>,在根标签内部包括两个子标签,<data>和原来界面XML根标签。在<data>标签中可以使用<varable>引入变量,name属性是在该XML文件中该变量的名称,type则为该变量所对应Java类的全限定名。在原来的界面XML文件中,可以通过@{}的方式使用该变量,大括号内为变量的引入。在<data>标签内部还有另外两个可用子标签本文后面细节部分会有介绍。

在XML文件中使用的对象变量所属的类的定义如下:
package com.recluseguo.recluseandroid.data;

public class User {

    private String firstName;
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

Data Binding 使用变量对象获取对象属性从而设置控件属性时可以通过两种方法,一是本例中采用的getter方法,另外一种就是讲属性设置为public也可以实现对对象属性的访问。

最后就是在Android的控制逻辑Java代码中获取XML对应的ViewDataBinding对象,Data Binding Library会为XML文件生成一个类,该类实例化的对象可以用在Java代码中控制界面中中的View,类的名称由XML的文件名决定,如activity_main.xml文件生成的类为ActivityMainBinding,这个类是继承自ViewDataBinding。该类会为在<data>标签里的每一个<varable>声明的变量添加getter和setter方法,因此可以控制XML中的界面属性。实例化这个类的对象的方法有两种方式:

    //使用生成类的静态方法inflate()
    ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflator, viewGroup, isAttach);
    //使用DataBindingUtil的静态方法inflate()
    ActivityMainBinding binding = DataBindingUtil.inflate(layoutInflator, R.layout.activity_main, viewGroup, isAttach);

在Fragment,ListView以及RecyclerView中,使用DataBinding可以通过此方法获取binding对象,然后通过binding.getRoot()方法获得根View即可,而在activity中封装了setContentView()方法,所以有一下简便方法使用DataBinding

    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

    User user = new User("recluse", "guo");
    binding.setUser(user);

通过binding对象的各个getter和setter方法就可以控制各个变量。如本例中通过setUser()方法为其设置对象变量。

通过上述三个步骤就实现了控制逻辑代码对界面属性的控制,相比于之前的通过findViewById获取控件,然后使用View控件属性的setter方法设置控件属性的方式,唯一优势就是可以使用对象变量的多个属性域设置多个控件属性,但这只是我所理解的数据绑定部分的优势(可能还会有其他优势吧),data binding的优势并不只是如此,后面还有事件绑定以及双向的数据绑定部分,更能凸显data binding的优势。

事件绑定

由于XML是一种标记语言,其逻辑控制能力很弱,所以Android系统中界面的事件处理通常是在Java代码中获取控件,并为控件绑定监听器的方式处理控件的点击等事件。同时,也可以为一些有设置了默认的clickable的控件设置onclick属性,为控件指定点击事件的响应方法,并在对应的activity或fragment中实现该方法。同样data binding也可是实现该功能,并且更进一步。

在data binding中对事件的绑定有两种方式,第一种叫Method References,这个与原来的设置onclick属性并没有太大差别,如下为实例:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="handlers" type="com.example.MyHandlers"/>
           <variable name="user" type="com.recluse.User"/>
       </data>
       <LinearLayout
           ....

           <Button
            android:text="click"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handler::onClickFriend}"/>
       </LinearLayout>
    </layout>
public class MyHandler {
    public void onClickFriend(View view){
        Log.d("recluse", "on click friend");
    }
}

    //MainActivity.java中设置handler对象变量
    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    User user = new User("recluse", "guo");
    MyHandler handler = new MyHandler();
    binding.setUser(user);
    binding.setHandler(handler);

在上述实例中设置了一个handler的对象变量,并在onClick属性中设置处理点击事件的方法,语法类似于C++中对方法的引用,使用双冒号。这种方式与之前的在activity中实现点击方法并无不同,这里定义的方法还是要与OnClickListener#onClick()方法的定义形式一致,即参数列表相同,否则报错,这里只不过可以将方法定义在任意的类中,并在XML文件中以对象变量的形式引入,并使用对象的方法而已。如果我们想在点击事件的处理中传入其他参数,比如我们想点击控件是改变User对象,将user变量传入,这时这种方式就无能为力了,因为不能改变参数列表。于是可以实现下面这种事件绑定的第二种方式。在官方文档中叫Listener Bindings。

首先看一下实例改为如下形式:

<?xml version="1.0" encoding="utf-8"?>
<layout>
...

        <Button
            android:text="click"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{(view)->handler.onClickFriend(view, user)}"/>
        </LinearLayout>
</layout>
    //MyHandler.java
    public void onClickFriend(View view, User user){
        user.setFirstName("qian");
        Log.d("recluse", "view id is " + view.getId());
        Log.d("recluse", "on click friend and the user's last name is " + user.getLastName());
    }

如上述实例中,在onClick属性中使用lambda表达式,对于参数列表,即->之前的那个括号,只可以使用两种形式,一是空,二是与属性所对应的定义方法参数列表相同,本例中为onClick,则需要一个view, 后面的方法中使用该view,这里代表这个button(可以通过打印出的id验证),如果后面的方法调用中不使用这个view,则可以使用第一个形式,即空参数列表。这里虽然限制lambda表达式的参数列表形式,但是后面的函数调用则可以任意的自定义函数参数列表了,如本例中传入了user变量对象,通过输出打印的last name可以验证我们传入的user对象就是原来在activity中定义的user,但是这里存在一个问题,我们设置了first name,但是界面并没有随之改变,除非我们再次调用ActivityMainBinding.setUser(user)。如果我们想要在设置的对象变量,如本例中的user, 发生变化时界面也可以随之改变,就可以使用data binding提供的双向绑定功能。

双向绑定

data binding的双向绑定是通过让POJO(plain old java object)类实现Observable接口,该接口中定义了添加和移除PropertyChangedListener接口的方法,通过监听器的方式监听对象状态的变化并通知界面View改变对应属性。由于过程较为复杂,data binding为我们提供了BaseObservable类,POJO类只需要继承BaseObservable,并在属性对应的getter方法上添加@Bindable注解以及setter方法中添加notifyPropertyChanged(BR.propertyName)方法即可,实例如下:

    public class User extends BaseObservable{

        private String firstName;
        private String lastName;

        public User(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
        @Bindable
        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
            notifyPropertyChanged(BR.firstName);
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    }

如代码中所示,当我们给属性对应的getter方法添加@Bindable注解之后,就会在BR类中生成一个该属性对应的入口,如BR.firstName,将该参数传入notifyPropertyChanged()方法,就可以在该属性发生变化时及时更新界面View的控件属性。此时我们点击按钮就可以改变firstName对应的TextView了。

此外,如果我们的POJO类由于框架限制不能继承BaseObservable, 而实现Observable接口并实现所有的监听和通知逻辑又十分繁琐和复杂。考虑到这种情况,data binding还为我们提供了更为方便的ObservableField类,以及Java中基本数据类型所对应的便捷类,如ObservableBoolean, ObservableInt等一系列类。

因此User类可以改成如下方式:

public class User{

    public ObservableInt age;
    public ObservableField<String> firstName;
    public ObservableField<String> lastName;
    public User(int age, String firstName, String lastName){
        this.age = new ObservableInt(age);
        this.firstName = new ObservableField<>(firstName);
        this.lastName = new ObservableField<>(lastName);
    }
}

XML的代码如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="user" type="com.recluseguo.recluseandroid.data.User"/>
        <variable
            name="handler"
            type="com.recluseguo.recluseandroid.data.MyHandler"/>
    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"/>
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}"/>

        <Button
            android:id="@+id/click_bt"
            android:text="click"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{(view)->handler.onClickFriend(view, user)}"/>
    </LinearLayout>
</layout>

MyHandler.java的代码如下:

public class MyHandler {
    public void onClickFriend(View view, User user){
        user.firstName.set("qian");
        Log.d("recluse", "view id is " + view.getId());
        Log.d("recluse", "on click friend and the user's last name is " + user.lastName.get());
    }
}

从代码中可以看出我们使用ObservableField以及对应的便捷类等,对于对象状态的监听以及通知界面类操作不需要我们去考虑,很方便地实现了双向保定,此时每个属性都变成了Observable的一个子类,我们可以通过其set(T)和get()方法设置或获取属性值,如MyHandler中处理的那样,XML文件生成的对应ViewDataBinding类中获取对象属性也是使用相同方法,因此我们需要将对象变量的属性设置为public,如本例中firstName是public ObservableField<String>, XML文件中设置的@{user.firstName},对应ActivityMainBinding类中设置该属性时会调用user.firstName.get()方法获取文本数据。
此外,对年龄的设置中可以看出在@{}大括号内部我们可以使用简单的Java表达式对变量做处理,这样在XML这种标记语言的环境下也可以执行一定的逻辑控制操作了,这样有些类似于JSP页面中的脚本。后面介绍data binding的一些语法细节中也可以看出其他与JSP相似的地方。

这里我们是使用一个POJO类封装关于一个类的多个属性,data binding同时还为我们提供了更为方面的组织一系列属性的方式,就是Observable的第三种形式,Observable Collection。Collection包括ObservableArrayList<T>和ObservableArrayMap<K,V>两种形式,从名字也可以看出前者顺序地保存一系列属性,并可以通过序号访问,后者则保存一个键值对系列,通过键可以访问对应值,以此作为属性设置。具体用法从官方实例中就可以很明白的看懂:

使用ObservableArrayList

    ObservableArrayList<Object> user = new ObservableArrayList<>();
    user.add("Google");
    user.add("Inc.");
    user.add(17);
    <data>
        <import type="android.databinding.ObservableList"/>
        <variable name="user" type="ObservableList<Object>"/>
    </data>
    …
    <TextView
       android:text='@{user[0]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    <TextView
       android:text='@{String.valueOf(1 + (Integer)user[2])}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

使用ObservableArrayMap

    ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
    user.put("firstName", "Google");
    user.put("lastName", "Inc.");
    user.put("age", 17);

对应的XML文件

    <data>
         <import type="android.databinding.ObservableMap"/>
         <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
       android:text='@{user["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    <TextView
       android:text='@{String.valueOf(1 + (Integer)user["age"])}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

这里的例子很容易看懂,不过有个新的知识点是<data>标签的子标签<import>,它负责引入在XML文件中使用的类,后面语法细节部分会讲述。这里需要注意的是ObservableMap是一个接口,它继承自Map,并且在内部定义了与Observable相同的接口方法, ObservableArrayMap是它的一个实现类,所以在代码中直接使用ObservableArrayMap即可以实现一些简单的一系列双向绑定的属性的组织。同样的,ObservableArrayList也是实现了ObservableList接口的类,用于实现简单的一系列双向绑定属性的组织。其内部具体的实现源码暂时不做分析。

所以总结双向数据绑定有三种实现方式,一是继承BaseObservable,并对属性getter方法使用@Bind注解,而且不要忘记在setter方法中调用notifyProperChanged()方法,传入参数是BR类中生成的该属性的入口;第二种方式就是对象中直接定义ObservableField<T>属性,需要定义为public,不再需要getter和setter,通过ObservableField的set()和get()方法完成属性的设置和获取,泛型参数T代表属性的类型,对应基本数据类型有一系列的便捷类与之对应。第三种就是最方便的方式,就是使用Observable Collection的形式组织一系列的属性。

至此就完成data binding的基本内容的讲述,包括数据绑定,事件绑定以及事件的双向绑定。下面还有一些具体的语法细节和面临的一些其他问题,下面一部分展开讲述。

语法细节

1. import标签

前面说过在@{}中可以使用一些简单的Java表达式,包括Lambda表达式,如果在这些表达式中使用了某些Java类,并不属于java.lang包,我们都知道在java代码中需要通过import导入包,在data binding中也在XML文件中引入了相同的语法,即<data>标签中的<import>子标签,前面已经用过该标签,type属性即表示引入的Java类,为了避免引入时带来名称冲突,还可以使用alias属性为该类指定别名,如下:

    <import type="android.view.View"/>
    <import type="com.example.real.estate.View"
            alias="Vista"/>

这一点感觉也有点像JSP页面。

2. View with ID

使用data binding,我们可以通过向XML文件中传入变量的形式控制控件的属性,因此大部分情况下,我们不需要再Java代码中获取到某个控件的引用,因此添加ID也就没有太大必要,但是少数情况需要在Java代码中获取某个控件时,我们还是可以为该控件设置ID,但是此时我们不需要findViewById去获取控件引用,而是在XML文件生成的对应ViewDataBinding类中生成一个域与该控件对应,在Java代码中直接使用该控件即可。如下:

<TextView
    android:id="@+id/first_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName}"/>

在XML文件中为TextView设置了Id, 在ActivityMainBinding类中会生成一个域:public final TextView firstName; 则在Java代码中直接使用binding(ActivityMainBinding的实例化对象),binding.firstName可以获取该TextView的引用。

3. 自定义类名

    <data class=".ContactItem">
        ...
    </data>

也可以指定包,即指定其全限定名

    <data class="com.example.ContactItem">
        ...
    </data>

4. includes

在一个XML文件中,包含另外的XML文件中,也可以将某个变量传递到被包含文件中的变量对象,但是包含文件以及被包含文件都需要定义了该变量。
使用方式如下:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:bind="http://schemas.android.com/apk/res-auto">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </LinearLayout>
    </layout>

name.xml和contact.xml文件中也必须定义了user变量,它们两个中的user变量则由该包含XML文件传递,即ActivityMainBinding#set(user), 那么在name.xml和contact.xml文件中也可以使用该user对象了。

5. @{}内的语法

在大括号内通常是引用变量的某个属性或方法,也可以是一个Lamada表达式。总结来说可以是Java的一个表达式,但是不支持new,super,this等关键字,这里只是表达式。大括号内还可以引用资源文件,如@string/str_name等可以在xml文件中使用的引用资源文件的方式都可以使用。

一些问题

我们知道data binding只是帮我们完成一件事件,就是监听对象状态的变化,并且通知对应的界面控件属性发生改变,这一系列动作都是由ViewDataBinding类帮助我们完成的,而它完成这些功能的方式就有一个就是调用对应属性的getter和setter方法。我们自定义的类或者Observable类,即用于XML对象变量的类,所有属性要么是public的要么有其对应的getter方法,即使是ObservableField类的属性,也可以设置为public,并访问该属性的get()方法获取真正想要赋值到View控件上的属性的值,这一步容易理解,而下一步就是设置控件的属性,也很简单就是调用控件View对应属性的setter方法。但是这里就存在一个问题,某些控件的属性设置方法,其方法名不是setXXXX(),这里就会遇到问题,我们的ViewDataBinding类如何设置这些属性。官方文档上给出了两种解决办法:

1. 重命名setter

官方文档给出的实例是ImageView, 它的android:tint属性设置实际上应该调用ImageView#setImageTintList(ColorStateList)方法,而不是setTint(),所以设置重命名的setter,方法如下:

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

这个注解是添加到ImageView当中,而我们不能修改ImageView的代码,所以这种方式对开发者来说作用不大,唯一可以用的地方是在自定义View的时候可以添加重命名的注解,但是自定义View的时候编写正确方式setter就可以了,也没必要使用。Android系统提供的控件,其属性的setter方法基本上都是使用该注解重命名过的,所以可以放心使用data binding。而自定义的View,我们自己定义setter即可。

2. 使用BindingAdapter

由于我们对于系统提供的控件无法改变,而有些属性是没有对应setter方法,如官方文档的举例中说,paddingLeft属性对应着setPadding()方法,该方法是设置四个方向的padding, 并没有单个方法的设置方法,此时我们使用data binding想要设置这类型的属性,我们就需要自己定义方法做处理,同时又不需要修改系统控件的代码,data binding为我们提供了这种方式,即@BindingAdapter注解。如下实例:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}

该方法是定义在任意一个类中的静态方法,比如我们可以定义一个BindingAdapterUtil.java类,在其中定义该静态方法,只要有@BindingAdpter()注解以及其中参数参数表示的XML中的属性,data binding就可以找到该静态方法并调用,该方法的参数列表是该控件view和注解参数中所表示的控件属性值共同组成。@BindingAdapter()注解可以包含多个属性,即参数可以是一个String数组,其中包括若干属性,对应静态方法则需要一个view参数,后面跟着String数组表示属性的对应属性值,共同组成方法的参数列表。这些属性值是由data binding传入的,至于值是什么,则是由XML文件中该属性使用的对象变量的绑定决定的。

通过这种方式,我们就可以自己定义方法,供data binding在给控件的属性设置属性值调用setter方法时调用,从而避免了调用控件View的属性对应setter方法。

既然我们可以为data binding指定在设置属性时的方法,那么我们就可以为控件设置任意的属性,即使该控件没有的属性也是可以的,只不过是在XML文件中我们使用自定义的命名空间,并在自定义的@BindingAdapter方法中对这些属性做处理,完成对控件的设置即可。比如官方的实例,我们想要通过Picasso加载图片,就可以使用如下方式:

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

这里在@BindingAdapter注解中,使用了bind的命名空间,其实bind关键字是指对于XML中的属性的处理忽略命名空间,只要是imageUrl和error属性,其值是由data binding绑定的,都经过该静态方法做处理,包括自定义的命名空间以及android命名空间。这里由于ImageView并没有imageUrl和error属性,所以需要使用自定义的命名空间,如果使用android:imageUrl 是会报错的。

接下来时类型匹配问题,之前说@{}可以是一个Java表达式,那么表达式则会返回一个Java对象,data binding会调用对应属性的setter方法,并将该Java对象作为参数传入,这里就存在一个类型匹配问题,系统首先会将java对象自动转型为参数类型,如果转型失败则会发生错误。考虑之前说的@BindingAdapter,我们可以想到一个解决方法,就是自定义静态方法,使得data binding不再调用setter方法,只要我们自定义的静态方法可以接受这个java对象的类型,并在方法中做转换处理并设置控件正确的属性即可。(官方文档上并没有如此说明,只是想到可以这样实现,可能会有一些其他问题,比如由于是设置的android命名空间的属性,有可能其他控件的该属性使用data binding时也会调用该静态方法,造成不必要的麻烦,具体还没有深入分析)

对于类型匹配问题,data binding 为我们提供了另外一个注解,@BindingConversion, 如下实例:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

由于background需要的类型是drawable, 而data binding的表达式返回的明显是int类型,因此需要我们自己定义一个conversion,这个静态方法也是可以定义在任意的类中,只需要@BindingConversion注解即可

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

这样在data binding在调用setter方法时,会调用该转换方法。
除此以外还有事件绑定的部分,每个监听器只能有一个方法,否则需要拆成两个等问题,暂时没有用到还没有去细致的学习,另外还有一些问题,比如在定义了@BindingAdapter({"bind:property", XXXX})的静态方法后,会拦截所有的名称为property的setter方法调用,而改为调用此方法,控件较多时会不会发生混乱,定义多个@BindingConversion时,data binding是如何查找正确的方法的,仅仅是根据参数类型和返回值类型做出判断吗,如果定义两个方法不同名,但是参数类型和返回值类型完全一样,但是需要不同逻辑,会不会发生混乱。等以后再做研究,基本用法暂且介绍到这里。

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

推荐阅读更多精彩内容