Flutter中的存儲(chǔ)

Flutter 是 Google 開(kāi)源的 UI 工具包,一套代碼多端應(yīng)用極大的提升了開(kāi)發(fā)效率,此外直接調(diào)用skia(c/c++)代碼的能力,也使得它具備媲美原生的渲染性能。但是涉及到非UI層的任務(wù)時(shí),F(xiàn)lutter仍然需要依托原生框架,比如相機(jī)、存儲(chǔ)、藍(lán)牙等功能,于是各種對(duì)原生能力封裝的Plugin就產(chǎn)生了,Android、iOS各自平臺(tái)提供原生能力,flutter側(cè)進(jìn)行對(duì)接,提供dart語(yǔ)言編寫(xiě)的api 給flutter側(cè)調(diào)用,使得flutter開(kāi)發(fā)人員依然可以一套代碼,多端應(yīng)用。

存儲(chǔ)數(shù)據(jù)到磁盤是開(kāi)發(fā)中常見(jiàn)的操作,比如用戶信息、一些不經(jīng)常變動(dòng)的數(shù)據(jù)、通訊錄等,以便下次打開(kāi)APP用戶可以不經(jīng)過(guò)網(wǎng)絡(luò)請(qǐng)求,快速預(yù)覽APP中的內(nèi)容。根據(jù)需要存儲(chǔ)的數(shù)據(jù)量的大小,用戶可以選擇適合自己的方案。對(duì)于少量的數(shù)據(jù),在原生側(cè)iOS一般直接使用UserDefaults,Android使用SharedPreferences,這兩種存儲(chǔ)方式一般用來(lái)存儲(chǔ)用戶或者APP信息等少量的數(shù)據(jù)。當(dāng)數(shù)據(jù)量大的時(shí)候就不適合使用了,一般會(huì)考慮基于SQLite的數(shù)據(jù)庫(kù)存儲(chǔ),或者是基于文件的存儲(chǔ)。

以上也是本文將要要講述核心:詳細(xì)介紹幾個(gè)存儲(chǔ)的優(yōu)質(zhì)框架的原理及使用,以便對(duì)大家需要使用存儲(chǔ)能力的時(shí)候有所幫助。

少量數(shù)據(jù)存儲(chǔ)

少量數(shù)據(jù)建議直接使用shared_preferences

preferences.png

這是官方維護(hù)的倉(cāng)庫(kù),它是對(duì)iOS中UserDefaults和Android中SharedPreferences的plugin封裝,iOS UserDefaults存儲(chǔ)在plist中,Android preferences存儲(chǔ)在xml中,原本各自操作都很簡(jiǎn)單,所以flutter側(cè)的封裝也很簡(jiǎn)單,整個(gè)代碼包含注釋不到200行。

因此在flutter側(cè)基于它來(lái)進(jìn)行少量數(shù)據(jù)的存儲(chǔ)也是十分方便的,在flutter側(cè)的類名也叫SharedPreferences,是個(gè)單例,實(shí)例化的時(shí)候會(huì)從磁盤中讀取到內(nèi)存,并且在內(nèi)存中保存一份,之后如果有新的數(shù)據(jù)存入的話,會(huì)同時(shí)進(jìn)行內(nèi)存和磁盤的更新,當(dāng)然寫(xiě)磁盤操作有極小的概率可能失敗,因此內(nèi)存中數(shù)據(jù)和磁盤中數(shù)據(jù)有極小概率不一致。

Future<bool> _setValue(String valueType, String key, Object value) {
    final String prefixedKey = '$_prefix$key';
    if (value == null) {
      _preferenceCache.remove(key);
      return _store.remove(prefixedKey);
    } else {
      if (value is List<String>) {
        // Make a copy of the list so that later mutations won't propagate
        _preferenceCache[key] = value.toList();
      } else {
        _preferenceCache[key] = value;
      }
      return _store.setValue(valueType, prefixedKey, value);
    }
  }

還需要注意點(diǎn)的一點(diǎn)是,如果native側(cè)進(jìn)行了SharedPreferences或者NSUserDefaults的存儲(chǔ)、修改操作,flutter側(cè)的SharedPreferences單例,并不會(huì)自行更新到內(nèi)存中,需要調(diào)用reload方法進(jìn)行內(nèi)存的更新。

Future<void> reload() async {
    final Map<String, Object> preferences =
        await SharedPreferences._getSharedPreferencesMap();
    _preferenceCache.clear();
    _preferenceCache.addAll(preferences);
  }
// 讀取操作api
Set<String> getKeys()
dynamic get(String key)
bool getBool(String key)
int getInt(String key)
double getDouble(String key)
String getString(String key)
bool containsKey(String key)
List<String> getStringList(String key)

// 寫(xiě)入操作api
Future<bool> setBool(String key, bool value)
Future<bool> setInt(String key, int value)
Future<bool> setDouble(String key, double value)
Future<bool> setString(String key, String value)
Future<bool> setStringList(String key, List<String> value)
Future<bool> remove(String key)

在項(xiàng)目開(kāi)發(fā)的時(shí)候,我們先獲取preferences單例,然后按照上述api進(jìn)行操作即可,由于比較簡(jiǎn)單,這里不再做實(shí)際示例介紹。

大量數(shù)據(jù)存儲(chǔ)

數(shù)據(jù)量大的話一般會(huì)基予SQLite進(jìn)行操作,目前flutter側(cè)最好的基于sqlite的插件是sqflite

sqflite.png

它在原生iOS側(cè)基于FMDB封裝,Android側(cè)基于系統(tǒng)的sqlite封裝,使用起來(lái)比preferences稍微復(fù)雜點(diǎn),需要打開(kāi)關(guān)閉數(shù)據(jù)庫(kù),自己建表進(jìn)行增、刪、改、查。其原理通過(guò)channel通信,將sql指令發(fā)送到原生側(cè),原生操作完數(shù)據(jù)庫(kù),再將數(shù)據(jù)返回給flutter側(cè)。plugin flutter側(cè)將用戶的操作最終都是轉(zhuǎn)化為sql指令,當(dāng)然flutter側(cè)不只是轉(zhuǎn)發(fā)用戶的sql操作,接下來(lái)會(huì)詳細(xì)進(jìn)行講述。

sql.PNG

使用sqlite存儲(chǔ) demo地址

  • 首先是獲取默認(rèn)數(shù)據(jù)庫(kù)存放路徑
var databasesPath = await getDatabasesPath();

通過(guò)getDatabasesPath 第一次調(diào)用的時(shí)候,通過(guò)channel向native側(cè)發(fā)送一條消息,native將路徑地址返回給flutter側(cè),flutter緩存此地址,之后再調(diào)用此方法,直接返回緩存的地址。

  • 然后定義自己的數(shù)據(jù)庫(kù)db路徑
String path = join(databasesPath, 'demo.db');
以上兩步操作的結(jié)果:
flutter: databasesPath /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents
flutter: db path /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents/demo.db

這時(shí)只是獲取了地址db實(shí)例并不會(huì)創(chuàng)建。

創(chuàng)建數(shù)據(jù)庫(kù)

Database database = await openDatabase(_dbPath);

創(chuàng)建數(shù)據(jù)庫(kù)最少只需要給定一個(gè)路徑就行,當(dāng)然這個(gè)api中還有其他可選參數(shù)

Future<Database> openDatabase(String path,
    {int version,
    OnDatabaseConfigureFn onConfigure,
    OnDatabaseCreateFn onCreate,
    OnDatabaseVersionChangeFn onUpgrade,
    OnDatabaseVersionChangeFn onDowngrade,
    OnDatabaseOpenFn onOpen,
    bool readOnly = false,
    bool singleInstance = true})
  • version db版本,用來(lái)決定是否進(jìn)行創(chuàng)建、升降級(jí)。只有設(shè)置了version,onCreate、onUpgrade、onDowngrade這三個(gè)可選回調(diào)才可能被調(diào)用,這三個(gè)回調(diào)最多只會(huì)調(diào)用一個(gè),當(dāng)version不變時(shí),三個(gè)回調(diào)都不會(huì)被調(diào)用。
  • onConfigure 打開(kāi)db時(shí)首先執(zhí)行這個(gè)回調(diào),在這個(gè)回調(diào)中可以執(zhí)行db的初始化操作,比如外鍵的設(shè)置或者提前寫(xiě)日志。
  • onCreate 只有當(dāng)db不存在時(shí),第一次調(diào)用openDatabase才會(huì)執(zhí)行此回調(diào),可以利用這個(gè)時(shí)機(jī),創(chuàng)建一些所需的table。
  • onUpgrade 有兩個(gè)場(chǎng)景會(huì)執(zhí)行此回調(diào),1.初始創(chuàng)建db時(shí),onCreate回調(diào)未設(shè)置。2.db已經(jīng)存在,并且version比db中上次記錄的的version大。可以在此方法中執(zhí)行必要的遷移操作。
  • onDowngrade 只有version比db中記錄的的version小時(shí)才會(huì)執(zhí)行。這種情況很少見(jiàn),只有當(dāng)新版本的代碼創(chuàng)建了一個(gè)數(shù)據(jù)庫(kù),然后與舊版本的代碼交互時(shí)才會(huì)出現(xiàn)這種情況,應(yīng)該盡量避免這種情況。
  • onOpen 這個(gè)回調(diào)最后執(zhí)行,在version被重置之后,openDatabase返回結(jié)果之前。
  • readOnly 默認(rèn)false,如果設(shè)置為true,則不允許任何修改操作
  • singleInstance 默認(rèn)為true,這樣針對(duì)同樣的dbPath,將返回同一個(gè)db實(shí)例。當(dāng)多次調(diào)用openDatabase的時(shí)候,只有首次調(diào)用的回調(diào)會(huì)生效,再次調(diào)用同一path時(shí)候,只會(huì)返回db實(shí)例,忽略新設(shè)置的參數(shù)。

這幾個(gè)可選回調(diào)順序是

1. [onConfigure]
2. [onCreate] or [onUpgrade] or [onDowngrade]
5. [onOpen]

我們可以創(chuàng)建多個(gè)db實(shí)例,每個(gè)db實(shí)例中可以創(chuàng)建多張table。這點(diǎn)和原生操作數(shù)據(jù)庫(kù)是一致的。

除了在每次數(shù)據(jù)庫(kù)初始創(chuàng)建onCreate或者升級(jí)時(shí)的onUpgrade回調(diào)中操作表,還可以在其他時(shí)機(jī)進(jìn)行操作,我們可以新建表、修改表字段,修改表字段對(duì)應(yīng)值得類型等,對(duì)表的操作都是通過(guò)sql語(yǔ)句進(jìn)行操作,下面展示幾個(gè)示例:

  • 新增一張表
_database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)');

如果db中已經(jīng)存在相同的表,再次創(chuàng)建不會(huì)生效,對(duì)原有表不會(huì)有影響。

  • 新增表字段
_database.execute('alter table Test2 ADD num2 REAL NOT NULL Default 0');
  • 更改表字段
_database.execute('alter table Test2 rename column num to num3');

合適的場(chǎng)景是,當(dāng)表里的字段不滿足時(shí),可以在數(shù)據(jù)庫(kù)升級(jí)的回調(diào)onUpgrade中進(jìn)行表的更改,當(dāng)然這有業(yè)務(wù)決定。

其他操作,還有這些sql語(yǔ)句和原生側(cè)操作一樣。

修改字段默認(rèn)值
alter table 表名 drop constraint 約束名字 // 刪除表的字段的原有約束
alter table 表名 add constraint 約束名字 DEFAULT 默認(rèn)值 for 字段名稱  // 添加一個(gè)表的字段的約束并指定默認(rèn)值

修改字段類型:
alter table 表名 alter column name nvarchar(10) not null

當(dāng)數(shù)據(jù)庫(kù)和表都建立好之后,接下來(lái)就是數(shù)據(jù)操作了,這里flutter側(cè)有兩種方式操作,可以直接編寫(xiě)sql語(yǔ)句,也可以使用flutter側(cè) helpers操作,兩種操作各有優(yōu)劣。Raw Sql方式更加直觀,但是sql語(yǔ)句編寫(xiě)容易出錯(cuò)

-- Raw Sql SQL helpers
優(yōu)勢(shì) 直觀,sql直接發(fā)送到native側(cè)處理 書(shū)寫(xiě)簡(jiǎn)單,不易出錯(cuò)
劣勢(shì) 直接編寫(xiě)sql 語(yǔ)句容易出錯(cuò) 需要一層轉(zhuǎn)換,底層仍是調(diào)用Raw Sql方式
  • Raw Sql方式
// 增
int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');

// 刪
await _database.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);

// 改
await _database.rawUpdate('UPDATE Test SET name = ?, value = ? WHERE name = ?', ['updated name', '9876', 'some name']);

// 查
List<Map> list = await _database.rawQuery('SELECT * FROM Test');
  • SQL helpers
    SQL helpers是flutter側(cè)對(duì)直接操作sql的封裝,以insert為例,借助SqlBuilder這個(gè)類提供的能力,將SQL helpers Api轉(zhuǎn)化成sql字符串,最終還是轉(zhuǎn)換為RAW Sql方式執(zhí)行。
Future<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm}) {
    final builder = SqlBuilder.insert(table, values,
        nullColumnHack: nullColumnHack, conflictAlgorithm: conflictAlgorithm);
    return rawInsert(builder.sql, builder.arguments);
  }
// 增
// table表名;values Map數(shù)據(jù),可以是model2json轉(zhuǎn)成的數(shù)據(jù);
// nullColumnHack字段為空時(shí)處理語(yǔ)句,conflictAlgorithm沖突處理枚舉
uture<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm});

// 刪
// where篩選條件,如果where為null,則刪除整個(gè)表中的數(shù)據(jù),whereArgs即其參數(shù)
Future<int> delete(String table, {String where, List<dynamic> whereArgs});

// 改
// values將要更新到表中的值,如果后面的篩選條件不設(shè)置,將更新整個(gè)表
Future<int> update(String table, Map<String, dynamic> values,
      {String where,
      List<dynamic> whereArgs,
      ConflictAlgorithm conflictAlgorithm});

// 查
// distinct是否排重,true的話返回的每行數(shù)據(jù)都是唯一的
// 返回表中哪幾列的數(shù)據(jù),傳null將返回所有列,最好對(duì)數(shù)據(jù)進(jìn)行過(guò)濾,以免讀取太多不相干的數(shù)據(jù)
Future<List<Map<String, dynamic>>> query(String table,
      {bool distinct,
      List<String> columns,
      String where,
      List<dynamic> whereArgs,
      String groupBy,
      String having,
      String orderBy,
      int limit,
      int offset});

SQL helpers可以和數(shù)據(jù)模型結(jié)合使用,根據(jù)業(yè)務(wù)需求,編寫(xiě)對(duì)應(yīng)的增刪改查api,外部使用就會(huì)非常精簡(jiǎn),下面是個(gè)具體的小例子:

final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';

class Todo {
  int id;
  String title;
  bool done;

  Map<String, dynamic> toMap() {
    var map = <String, dynamic>{
      columnTitle: title,
      columnDone: done == true ? 1 : 0
    };
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }

  Todo();

  Todo.fromMap(Map<String, dynamic> map) {
    id = map[columnId];
    title = map[columnTitle];
    done = map[columnDone] == 1;
  }
}

class TodoProvider {
  Database db;

  Future open(String path) async {
    db = await openDatabase(path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute('''
create table $tableTodo ( 
  $columnId integer primary key autoincrement, 
  $columnTitle text not null,
  $columnDone integer not null)
''');
    });
  }

  Future<Todo> insert(Todo todo) async {
    todo.id = await db.insert(tableTodo, todo.toMap());
    return todo;
  }

  Future<Todo> getTodo(int id) async {
    List<Map> maps = await db.query(tableTodo,
        columns: [columnId, columnDone, columnTitle],
        where: '$columnId = ?',
        whereArgs: [id]);
    if (maps.length > 0) {
      return Todo.fromMap(maps.first);
    }
    return null;
  }

  Future<int> delete(int id) async {
    return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
  }

  Future<int> update(Todo todo) async {
    return await db.update(tableTodo, todo.toMap(),
        where: '$columnId = ?', whereArgs: [todo.id]);
  }

  Future close() async => db.close();
}

以上代碼可以直在sqlite存儲(chǔ) demo地址中查看。

  • 事務(wù)
    如果有一組不可分割的操作,可以使用transaction進(jìn)行處理,也即事務(wù)
Future<T> transaction<T>(Future<T> Function(Transaction txn) action,
      {bool exclusive});

// 一個(gè)具體小例子
database.transaction((txn) async {
                int id1 = await txn.rawInsert(
                    'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
                print('inserted1: $id1');
                int id2 = await txn.rawInsert(
                    'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
                    ['another name', 12345678, 3.1416]);
                print('inserted2: $id2');
              });

需要注意的是,事務(wù)中不要直接再調(diào)用database進(jìn)行操作,否則可能造成死鎖,而是使用變量txn操作。

  • 批處理
    經(jīng)過(guò)上面的介紹我們可以知道,所有操作都是通過(guò)channel發(fā)送到原生側(cè)處理的,操作多的話,就會(huì)產(chǎn)生很多的來(lái)回通信數(shù)據(jù),為了優(yōu)化這個(gè)過(guò)程可以將一組操作放到Batch中,它會(huì)將一組指令打包成一條,注意最后有個(gè)batch.commit();操作
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();

存儲(chǔ)數(shù)據(jù)到文件

除了以上兩種方式,還可以將數(shù)據(jù)存儲(chǔ)到文件中,這里也推薦官方維護(hù)的path_provider

path_provider.png

  • 首先 仍是需要制定/獲取文件路徑
Future<String> get _localPath async {
    Directory _path = await getApplicationDocumentsDirectory();
    Directory _directory = await Directory("${_path.path}/test").create(recursive: true);
    return _directory.path;
  }
  • 然后獲取文件
    文件格式類型按自己需求指定。
Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/homePageId001.json');
  }

  • 讀寫(xiě)操作不只有字符串,還可以是其他類型。
Future<String> readStr() async {
   try {
     final file = await _localFile;
     var content = await file.readAsString();
     return content;
   } catch (e) {
     return 'error';
   }
  }
  • 寫(xiě)
Future<File> writeStr(str) async {
    final file = await _localFile;
    return file.writeAsString('$str');
  }

開(kāi)發(fā)時(shí)檢查

無(wú)論是以上哪種方式,都是可以在開(kāi)發(fā)時(shí)查看存儲(chǔ)到磁盤中的數(shù)據(jù)是否正常,這也和原生開(kāi)發(fā)時(shí)類似。以iOS為例,每個(gè)APP運(yùn)行數(shù)據(jù)都是隔離的,有自己的文件夾,我們可以將所開(kāi)發(fā)APP的文件夾下載下來(lái),選中Xcode-> Window -> Devices and simulators 然后按照下圖操作:


下載.png

選中運(yùn)行的程序,下載所在APP運(yùn)行的數(shù)據(jù),雙擊顯示包內(nèi)容:


path.png

我們的db文件操作文件存儲(chǔ)在此,preferences文件存儲(chǔ)在AppData -> Library -> Preferences目錄下,操作文件preferences可以直接打開(kāi)查看,這里說(shuō)下db文件:

dbdemo.png

每次對(duì)數(shù)據(jù)有修改,我們可以在開(kāi)發(fā)時(shí),查看對(duì)應(yīng)db中的表或者數(shù)據(jù)的改動(dòng)。

以上就是我對(duì)Flutter中存儲(chǔ)的總結(jié),大家根據(jù)業(yè)務(wù)情況選擇適合自己的方案,當(dāng)然還有其他優(yōu)秀框架,歡迎一起交流。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

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