從零開始搭建一個(gè)主流項(xiàng)目框架(四)—Kotlin+GreenDao3.2

個(gè)人博客:haichenyi.com赶盔。感謝關(guān)注

??本篇介紹android常用的數(shù)據(jù)庫之一GreenDao的簡單用法企锌,增刪改查∮谖矗基于前面搭的框架撕攒,當(dāng)然陡鹃,你也可以選擇不用《镀海看懂用法之后萍鲸,用起來很方便。GreenDao數(shù)據(jù)庫升級到3.0版本之后api用起來更加方便了擦俐,便于讓開發(fā)人員專注于業(yè)務(wù)邏輯脊阴。我需要額外說明的是,我把之前的項(xiàng)目框架轉(zhuǎn)成了kotlin蚯瞧,不會kotlin語法的同學(xué)嘿期,可以去研究一下。

添加依賴

最終埋合,我們要添加如下代碼备徐,效果圖如下:

初始化數(shù)據(jù)庫.png

第一步

??打開的你根目錄下面的build.gradle文件,也就是項(xiàng)目下面的甚颂,并不是app目錄下面的build.gradle蜜猾。

// In your root build.gradle file:
buildscript {
    repositories {
        jcenter()
        ...//其他你自己的
        mavenCentral() // add repository
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        ...//其他你自己的
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2' // add plugin
    }
}

第二步

??打開你的項(xiàng)目下面的build.gradle文件,也就是你的app目錄下面的西设,之前我們添加依賴的時(shí)候的那個(gè)文件

// In your app projects build.gradle file:
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
 
dependencies {
    compile 'org.greenrobot:greendao:3.2.2' // add library
}

??完成上面兩步瓣铣,辣么,關(guān)于greendao的依賴我們就添加完成了

初始化

??我們首先得有一個(gè)bean類贷揽,這個(gè)bean類對應(yīng)的就是數(shù)據(jù)庫表的表結(jié)構(gòu)棠笑。我這里想說明的是(敲黑板了),看到了很多網(wǎng)上說的什么主鍵id必須用Long類型禽绪,這種說法是不準(zhǔn)確的蓖救,準(zhǔn)確的說,你的主鍵字段名稱印屁,如果是“id”循捺,辣么,你這個(gè)字段“id”雄人,必須用Long類型从橘,如果你換一個(gè)名稱,比方說“myId”础钠,辣么恰力,你就不必用Long類型,這個(gè)問題旗吁,說大不大踩萎,說小,又困擾了我有一會很钓。我這里新建用戶表香府,就需要一個(gè)User的java bean類董栽。如下:

package com.haichenyi.myproject.model.bean;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Property;
import org.greenrobot.greendao.annotation.Transient;

/**
 * Author: 海晨憶
 * Date: 2018/2/24
 * Desc: 用戶表的bean類
 */
@Entity
public class User {
  @Id
  private String name;
  private int sex;
  @Property(nameInDb = "Height")
  private int height;
  private String weight;
  @NotNull
  private int age;
  @Transient
  private String character;
}

??這里我把幾個(gè)常用的注解都寫出來了,說一下這幾個(gè)注解是什么意思

注解 意義
@Entity 用于標(biāo)識這是一個(gè)需要Greendao幫我們生成代碼的bean
@Id 標(biāo)明主鍵企孩,括號里可以指定是否自增
@Property 用于設(shè)置屬性在數(shù)據(jù)庫中的列名(默認(rèn)不寫就是保持一致)
@NotNull 非空
@Transient 標(biāo)識這個(gè)字段是自定義的不會創(chuàng)建到數(shù)據(jù)庫表里

簡單的講一下:

  1. @Entity:標(biāo)識的bean類锭碳,我們在運(yùn)行的時(shí)候,greendao會自動幫我們生成對應(yīng)的表

  2. @Id:標(biāo)識的字段就是這個(gè)表對應(yīng)的主鍵

  3. @Property:標(biāo)識的字段在表中對應(yīng)的那一欄的名稱是后面括號里面的柠硕,這個(gè)表height字段對應(yīng)表中的Height工禾,一般我們直接設(shè)置成默認(rèn)就可以了

  4. @NotNull:標(biāo)識的字段,這個(gè)字段在表中不能為空蝗柔,不然就出錯(cuò)闻葵,所以,在添加數(shù)據(jù)的時(shí)候設(shè)置默認(rèn)值

  5. @Transient:標(biāo)識的字段癣丧,在生成表的時(shí)候不會生成對應(yīng)的字段槽畔。這個(gè)什么時(shí)候用呢?這個(gè)胁编,我一般用作標(biāo)記flag厢钧,比方說,從數(shù)據(jù)庫拿數(shù)據(jù)嬉橙,又不想重新寫一個(gè)bean類早直,就用這個(gè)bean類,RecyclerView市框,填充完數(shù)據(jù)霞扬,item點(diǎn)擊的時(shí)候,狀態(tài)發(fā)生變化枫振,我們要有一個(gè)flag喻圃,就通過修改這個(gè)字段的值,頁面做出相應(yīng)的變化粪滤。

??寫到這里斧拍,我們的bean類也有了,要怎么生成數(shù)據(jù)庫呢杖小?在生成數(shù)據(jù)庫之前肆汹,我們先把項(xiàng)目重新clean一遍,再build一遍予权,看到你剛寫的需要生成表的bean類變成了如下樣子:

package com.haichenyi.myproject.model.bean;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.NotNull;
import org.greenrobot.greendao.annotation.Property;
import org.greenrobot.greendao.annotation.Transient;
import org.greenrobot.greendao.annotation.Generated;

/**
 * Author: 海晨憶
 * Date: 2018/2/24
 * Desc: 用戶表的bean類
 */
@Entity
public class User {
  @Id
  private String name;
  private int sex;
  @Property(nameInDb = "Height")
  private int height;
  private String weight;
  @NotNull
  private int age;
  @Transient
  private String character;
  @Generated(hash = 717717955)
  public User(String name, int sex, int height, String weight, int age) {
      this.name = name;
      this.sex = sex;
      this.height = height;
      this.weight = weight;
      this.age = age;
  }
  @Generated(hash = 586692638)
  public User() {
  }
  public String getName() {
      return this.name;
  }
  public void setName(String name) {
      this.name = name;
  }
  public int getSex() {
      return this.sex;
  }
  public void setSex(int sex) {
      this.sex = sex;
  }
  public int getHeight() {
      return this.height;
  }
  public void setHeight(int height) {
      this.height = height;
  }
  public String getWeight() {
      return this.weight;
  }
  public void setWeight(String weight) {
      this.weight = weight;
  }
  public int getAge() {
      return this.age;
  }
  public void setAge(int age) {
      this.age = age;
  }
}

??如上昂勉,greendao通過注解的方式幫我們自動生成了set/get方法,還有構(gòu)造方法伟件,這就對了,我們不用關(guān)议经,之后我們再執(zhí)行如下代碼生成數(shù)據(jù)庫和表:

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(getApplicationContext(), "haichenyi.db", null);
DaoMaster daoMaster = new DaoMaster(devOpenHelper.getWritableDb());
DaoSession daoSession = daoMaster.newSession();

??通過 DaoMaster 的內(nèi)部類 DevOpenHelper斧账,你可以得到一個(gè)便利的 SQLiteOpenHelper 對象谴返。可能你已經(jīng)注意到了咧织,你并不需要去編寫「CREATE TABLE」這樣的 SQL 語句嗓袱,因?yàn)?greenDAO 已經(jīng)幫你做了。注意:默認(rèn)的 DaoMaster.DevOpenHelper會在數(shù)據(jù)庫升級時(shí)习绢,刪除所有的表渠抹,意味著這將導(dǎo)致數(shù)據(jù)的丟失。所以闪萄,在正式的項(xiàng)目中梧却,你還應(yīng)該做一層封裝,來實(shí)現(xiàn)數(shù)據(jù)庫的安全升級败去。升級的問題放航,我們在后面講,這里我們先把數(shù)據(jù)庫和表先創(chuàng)建了圆裕。

??上面這個(gè)方式是java格式的广鳍,由于,我昨天寫完框架之后吓妆,我把項(xiàng)目轉(zhuǎn)成了kotlin代碼赊时,所以這里有點(diǎn)不一樣,項(xiàng)目我后面會上傳行拢。這里我要說明的是(敲黑板)我用kotlin的時(shí)候祖秒,碰到了一個(gè)問題,當(dāng)我使用greendao的時(shí)候剂陡,他提示我狈涮,無法引入用注解方式生成的類,dagger2也是一樣的鸭栖,我用java代碼寫就沒有問題歌馍,我寫這篇博客的時(shí)候,目前還沒有找到解決的辦法晕鹊。

greendao的問題.png

??我用了另外一種方式松却,采用跟之前網(wǎng)絡(luò)請求一樣的設(shè)計(jì)模式——裝飾者模式。我這里就不多做說明了溅话。我貼出我的代碼晓锻。

SqlHelper

package com.haichenyi.myproject.model.sql;

/**
 * Author: 海晨憶
 * Date: 2018/2/24
 * Desc:
 */
public interface SqlHelper {
  void onAdd();

  void onDelete();

  void onUpdate();

  void onSelect();
}

??這里定義增刪改查4個(gè)方法,用于測試這4個(gè)功能

SqlImpl

package com.haichenyi.myproject.model.sql;

import com.haichenyi.myproject.base.MyApplication;
import com.haichenyi.myproject.model.bean.DaoMaster;
import com.haichenyi.myproject.model.bean.DaoSession;
import com.haichenyi.myproject.model.bean.UserDao;
import com.haichenyi.myproject.utils.ToastUtils;

/**
 * Author: 海晨憶
 * Date: 2018/2/24
 * Desc:
 */
public class SqlImpl implements SqlHelper {
  private final UserDao userDao;

  /**
   * 初始化Sql Dao.
   *
   * @param application {@link MyApplication}
   */
  public SqlImpl(MyApplication application) {
    SqlOpenHelper helper = new SqlOpenHelper(application, "haichenyi.db");
    DaoSession daoSession = new DaoMaster(helper.getWritableDb()).newSession();
    userDao = daoSession.getUserDao();
  }

  @Override
  public void onAdd() {
    ToastUtils.Companion.showTipMsg("增加數(shù)據(jù)");
  }

  @Override
  public void onDelete() {
    ToastUtils.Companion.showTipMsg("刪除數(shù)據(jù)");
  }

  @Override
  public void onUpdate() {
    ToastUtils.Companion.showTipMsg("更新數(shù)據(jù)");
  }

  @Override
  public void onSelect() {
    ToastUtils.Companion.showTipMsg("查詢數(shù)據(jù)");
  }
}

??功能實(shí)現(xiàn)類飞几,看到他的構(gòu)造方法里面砚哆,第二個(gè)參數(shù)就是我們的數(shù)據(jù)庫名稱,后面通過getWritableDb()獲取的是可寫的數(shù)據(jù)庫屑墨,可寫就肯定可讀躁锁。然后就是接口的實(shí)現(xiàn)類了纷铣,這里就是具體的增刪改查功能的實(shí)現(xiàn)類,我這里在對應(yīng)的方法里面就寫了Toast战转,增刪改查具體怎么寫后面再說

SqlOpenHelper

package com.haichenyi.myproject.model.sql;

import android.content.Context;

import com.haichenyi.myproject.model.bean.DaoMaster;
import com.haichenyi.myproject.model.bean.UserDao;

import org.greenrobot.greendao.database.Database;

/**
 * Author: 海晨憶
 * Date: 2018/2/24
 * Desc:
 */
public class SqlOpenHelper extends DaoMaster.OpenHelper {
  public SqlOpenHelper(Context context, String name) {
    super(context, name);
  }

  @SuppressWarnings("unchecked")
  @Override
  public void onUpgrade(Database db, int oldVersion, int newVersion) {
    super.onUpgrade(db, oldVersion, newVersion);

    MigrationHelper.migrate(db, new MigrationHelper.ReCreateAllTableListener() {
      @Override
      public void onCreateAllTables(Database db, boolean ifNotExists) {
        DaoMaster.createAllTables(db, ifNotExists);
      }

      @Override
      public void onDropAllTables(Database db, boolean ifExists) {
        DaoMaster.dropAllTables(db, ifExists);
      }
    }, UserDao.class);
  }
}

??這個(gè)類用于管理數(shù)據(jù)庫的表對應(yīng)的字段發(fā)生變化的時(shí)候搜立,數(shù)據(jù)庫需要進(jìn)行的版本更新,連上下面那個(gè)類槐秧,都是用于版本數(shù)據(jù)庫版本更新的啄踊,防止數(shù)據(jù)丟失。怎么寫呢刁标?看到最后面的?UserDao.class?了嗎颠通?這個(gè)就是我們需要更新的表,你哪個(gè)表需要更新命雀,直接寫在后面就可以了蒜哀,這個(gè)是可以一次傳多個(gè)表的,并不是一次只能傳一個(gè)

MigrationHelper

package com.haichenyi.myproject.model.sql;

import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;

import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.database.StandardDatabase;
import org.greenrobot.greendao.internal.DaoConfig;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Author: 海晨憶.
 * Date: 2018/2/24
 * Desc:
 */
public final class MigrationHelper {
  public static boolean DEBUG = false;
  private static String TAG = "MigrationHelper";
  private static final String SQLITE_MASTER = "sqlite_master";
  private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";

  private static WeakReference<ReCreateAllTableListener> weakListener;

  public interface ReCreateAllTableListener {
    void onCreateAllTables(Database db, boolean ifNotExists);

    void onDropAllTables(Database db, boolean ifExists);
  }

  public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    printLog("【The Old Database Version】" + db.getVersion());
    Database database = new StandardDatabase(db);
    migrate(database, daoClasses);
  }

  public static void migrate(SQLiteDatabase db, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    weakListener = new WeakReference<>(listener);
    migrate(db, daoClasses);
  }

  public static void migrate(Database database, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    weakListener = new WeakReference<>(listener);
    migrate(database, daoClasses);
  }

  public static void migrate(Database database, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    printLog("【Generate temp table】start");
    generateTempTables(database, daoClasses);
    printLog("【Generate temp table】complete");

    ReCreateAllTableListener listener = null;
    if (weakListener != null) {
      listener = weakListener.get();
    }

    if (listener != null) {
      listener.onDropAllTables(database, true);
      printLog("【Drop all table by listener】");
      listener.onCreateAllTables(database, false);
      printLog("【Create all table by listener】");
    } else {
      dropAllTables(database, true, daoClasses);
      createAllTables(database, false, daoClasses);
    }
    printLog("【Restore data】start");
    restoreData(database, daoClasses);
    printLog("【Restore data】complete");
  }

  private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    for (int i = 0; i < daoClasses.length; i++) {
      String tempTableName = null;

      DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
      String tableName = daoConfig.tablename;
      if (!isTableExists(db, false, tableName)) {
        printLog("【New Table】" + tableName);
        continue;
      }
      try {
        tempTableName = daoConfig.tablename.concat("_TEMP");
        StringBuilder dropTableStringBuilder = new StringBuilder();
        dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";");
        db.execSQL(dropTableStringBuilder.toString());

        StringBuilder insertTableStringBuilder = new StringBuilder();
        insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
        insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
        db.execSQL(insertTableStringBuilder.toString());
        printLog("【Table】" + tableName + "\n ---Columns-->" + getColumnsStr(daoConfig));
        printLog("【Generate temp table】" + tempTableName);
      } catch (SQLException e) {
        Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e);
      }
    }
  }

  private static boolean isTableExists(Database db, boolean isTemp, String tableName) {
    if (db == null || TextUtils.isEmpty(tableName)) {
      return false;
    }
    String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER;
    String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?";
    Cursor cursor = null;
    int count = 0;
    try {
      cursor = db.rawQuery(sql, new String[]{"table", tableName});
      if (cursor == null || !cursor.moveToFirst()) {
        return false;
      }
      count = cursor.getInt(0);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cursor != null)
        cursor.close();
    }
    return count > 0;
  }


  private static String getColumnsStr(DaoConfig daoConfig) {
    if (daoConfig == null) {
      return "no columns";
    }
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < daoConfig.allColumns.length; i++) {
      builder.append(daoConfig.allColumns[i]);
      builder.append(",");
    }
    if (builder.length() > 0) {
      builder.deleteCharAt(builder.length() - 1);
    }
    return builder.toString();
  }


  private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
    reflectMethod(db, "dropTable", ifExists, daoClasses);
    printLog("【Drop all table by reflect】");
  }

  private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
    reflectMethod(db, "createTable", ifNotExists, daoClasses);
    printLog("【Create all table by reflect】");
  }

  /**
   * dao class already define the sql exec method, so just invoke it
   */
  private static void reflectMethod(Database db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
    if (daoClasses.length < 1) {
      return;
    }
    try {
      for (Class cls : daoClasses) {
        Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class);
        method.invoke(null, db, isExists);
      }
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }

  private static void restoreData(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
    for (int i = 0; i < daoClasses.length; i++) {
      DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
      String tableName = daoConfig.tablename;
      String tempTableName = daoConfig.tablename.concat("_TEMP");

      if (!isTableExists(db, true, tempTableName)) {
        continue;
      }

      try {
        // get all columns from tempTable, take careful to use the columns list
        List<String> columns = getColumns(db, tempTableName);
        ArrayList<String> properties = new ArrayList<>(columns.size());
        for (int j = 0; j < daoConfig.properties.length; j++) {
          String columnName = daoConfig.properties[j].columnName;
          if (columns.contains(columnName)) {
            properties.add("`" + columnName + "`");
          }
        }
        if (properties.size() > 0) {
          final String columnSQL = TextUtils.join(",", properties);

          StringBuilder insertTableStringBuilder = new StringBuilder();
          insertTableStringBuilder.append("REPLACE INTO ").append(tableName).append(" (");
          insertTableStringBuilder.append(columnSQL);
          insertTableStringBuilder.append(") SELECT ");
          insertTableStringBuilder.append(columnSQL);
          insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
          db.execSQL(insertTableStringBuilder.toString());
          printLog("【Restore data】 to " + tableName);
        }
        StringBuilder dropTableStringBuilder = new StringBuilder();
        dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
        db.execSQL(dropTableStringBuilder.toString());
        printLog("【Drop temp table】" + tempTableName);
      } catch (SQLException e) {
        Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e);
      }
    }
  }

  private static List<String> getColumns(Database db, String tableName) {
    List<String> columns = null;
    Cursor cursor = null;
    try {
      cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 0", null);
      if (null != cursor && cursor.getColumnCount() > 0) {
        columns = Arrays.asList(cursor.getColumnNames());
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cursor != null)
        cursor.close();
      if (null == columns)
        columns = new ArrayList<>();
    }
    return columns;
  }

  private static void printLog(String info) {
    if (DEBUG) {
      Log.d(TAG, info);
    }
  }
}

??這個(gè)類是工具類吏砂,拿過去用就好了撵儿,還有就是,應(yīng)用怎么判斷是否需要版本更新呢狐血?打開你的app下面的build.grade淀歇,在根結(jié)點(diǎn)下面添加如下代碼:

greendao {
    schemaVersion 1
}

每當(dāng)你發(fā)布新版本的時(shí)候,把這個(gè)版本號+1即可匈织。

??當(dāng)然浪默,我門這里依然是用的dagger生成的全局單例,所以缀匕,你還需要在你的AppModule下面添加如下代碼:

  @Provides
  @Singleton
  SqlHelper provideSqlHelper() {
    return new SqlImpl(application);
  }

??記得把項(xiàng)目重新clean一遍纳决,build一遍,重新跑項(xiàng)目的時(shí)候乡小,找到你的數(shù)據(jù)庫阔加。data-data-你的應(yīng)用包名-databases-haichenyi.db,這個(gè)就是我們的數(shù)據(jù)庫满钟。找個(gè)Sqlite可視化工具打開胜榔,你會看到如下結(jié)構(gòu)。

表結(jié)構(gòu).png

??太多了湃番,不寫了夭织,下一篇寫增刪改查。

項(xiàng)目鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吠撮,一起剝皮案震驚了整個(gè)濱河市尊惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖弄屡,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戴卜,死亡現(xiàn)場離奇詭異,居然都是意外死亡琢岩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門师脂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來担孔,“玉大人,你說我怎么就攤上這事吃警「馄” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵酌心,是天一觀的道長拌消。 經(jīng)常有香客問我,道長安券,這世上最難降的妖魔是什么墩崩? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮侯勉,結(jié)果婚禮上鹦筹,老公的妹妹穿的比我還像新娘。我一直安慰自己址貌,他們只是感情好铐拐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著练对,像睡著了一般遍蟋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上螟凭,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天虚青,我揣著相機(jī)與錄音,去河邊找鬼赂摆。 笑死挟憔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的烟号。 我是一名探鬼主播绊谭,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汪拥!你這毒婦竟也來了达传?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宪赶,沒想到半個(gè)月后宗弯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搂妻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年蒙保,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲主。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡邓厕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扁瓢,到底是詐尸還是另有隱情详恼,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布引几,位于F島的核電站昧互,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伟桅。R本人自食惡果不足惜敞掘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望楣铁。 院中可真熱鬧渐逃,春花似錦、人聲如沸民褂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赊堪。三九已至面殖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哭廉,已是汗流浹背脊僚。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遵绰,地道東北人辽幌。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像椿访,于是被迫代替她去往敵國和親乌企。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容