Flutter第十二章(Flutter 中單例,shared_preferences 實現數據持久化,`IO 文件讀寫操作數據持久化)

版權聲明:本文為作者原創書籍。轉載請注明作者和出處,未經授權,嚴禁私自轉載,侵權必究?。?!

情感語錄: 不要去追一匹馬,用追馬的時間種草,待到春暖花開時,就會有一群駿馬任你挑選;豐富自己,比取悅他人更有力量!

歡迎來到本章節,上一章節介紹了常用對話框和進度條的使用,知識點回顧 戳這里 Flutter基礎第十一章

本章知識點主要介紹 Flutter 中的數據共享和持久化,很顯然,本章知識點相當重要,數據共享和持久化操作基本是每個應用的必備操作。比如:一個應用里有很多界面都會用到我的的用戶信息,而我的用戶信息一開始就在登錄時就返回了,我們把用戶的基本信息存儲在一個全局的變量里即可實現數據共享,讓其他視圖直接引用該變量值即可 。但是這樣的存儲只能達到內存級別的,應用關閉后這些數據就被丟失了。要實現關閉應用后再次重啟應用我的用戶信息而不丟失那該怎么辦呢? 這就是本章節的重點了------ 持久化

本章簡要:

1、Flutter 中單例介紹

2、shared_preferences 實現數據持久化

3、IO 文件讀寫操作數據持久化

一、全局靜態變量

Flutter 中的 Dart 語言如同 Java 語言一樣,存在很多地方的相似性。在通過使用 static 關鍵字 為一個變量開辟內存地址后,而其他需要引用到該變量值時, 只需要將內存地址指向該變量即可實現數據共享。

如: 我定義了一個 Global 類來管理全局的的靜態資源:

class Global{

  //定義一個靜態屬性 name 
  static String name = "張三";

}

在需要使用到該變量值的地方 通過 Global.name 方式獲取該變量的值。

二、單例

單例模式是日常開發中最常用的設計模式之一。Dart 是單線程模型,因此在Flutter 中實現單例 不需要像 Java 中加雙重檢查鎖去考慮多線程的問題。

Dart中的單例也有兩種模式:

1、餓漢式

餓漢式比較好理解:在類加載時,就進行實例的創建。加載時獲取實例速度較慢,運行時速度較快。通俗的講:"我管你吃不吃包子,反正我先把這坨包子蒸好放在這兒"

實例代碼:

class UserHelper{
  
    // 單例公開訪問點
    factory UserHelper() =>_userInstance();
  
    static UserHelper get instance => _userInstance();
  
    // 靜態私有成員,沒有初始化
    static UserHelper _instance = UserHelper._();
  
    // 私有構造函數
    UserHelper._() {
      // 具體初始化代碼
      print("------>初始化");
    }
  
    // 靜態、同步、私有訪問點
    static UserHelper _userInstance() {
  
      return _instance;
    }
  
    String getUserName(){
  
      return "張三";
    }
  
  }

餓漢式單例測試:

  void main(){

    var userHelper = UserHelper();
    var userHelper1 = UserHelper();
    var userHelper2 =  UserHelper.instance;

    print("------------> 對象:'${userHelper.hashCode} 相等  '+${identical(userHelper, userHelper1)}"); //true
    print("------------> 對象:'${userHelper1.hashCode} 相等  '+${identical(userHelper, userHelper2)}"); //true
    print("------------> 對象:'${userHelper2.hashCode} 相等  '+${identical(userHelper1, userHelper2)}"); //true

  }

控制臺輸出:

    I/flutter: ------>初始化
    I/flutter: ------------> 對象:'1070008279 相等  '+true
    I/flutter: ------------> 對象:'1070008279 相等  '+true
    I/flutter: ------------> 對象:'1070008279 相等  '+true

可以看出 UserHelper 類有且只初始化了一次,且 hashCode 值也都相等,證明后面的無論 new UserHelper() 多少次拿到的都是同一對象。

2、懶漢式

在類加載時,不創建實例。加載時速度較快,運行時獲取實例速度較慢。通俗的講:“我管你吃不吃包子,等你問我了我才開始給你做包子”

實例代碼:

  class UserHelper{

    // 單例公開訪問點
    factory UserHelper() =>_userInstance();

    static UserHelper get instance => _userInstance();

    // 靜態私有成員,沒有初始化
    static UserHelper _instance;

    // 私有構造函數
    UserHelper._() {
      // 具體初始化代碼
      print("------>初始化");
    }

    // 靜態、同步、私有訪問點
    static UserHelper _userInstance() {
      if (_instance == null) {
        _instance = UserHelper._();
      }
      return _instance;
    }


  }

懶漢式單例測試:

  void main(){

    var userHelper = UserHelper();
    var userHelper1 = UserHelper();
    var userHelper2 =  UserHelper.instance;

    print("------------> 對象:'${userHelper.hashCode} 相等  '+${identical(userHelper, userHelper1)}"); //true
    print("------------> 對象:'${userHelper1.hashCode} 相等  '+${identical(userHelper, userHelper2)}"); //true
    print("------------> 對象:'${userHelper2.hashCode} 相等  '+${identical(userHelper1, userHelper2)}"); //true

  }

控制臺輸出:

    I/flutter ( 8120): ------>初始化
    I/flutter ( 8120): ------------> 對象:'537698073 相等  '+true
    I/flutter ( 8120): ------------> 對象:'537698073 相等  '+true
    I/flutter ( 8120): ------------> 對象:'537698073 相等  '+true

無論是通過餓漢式 還是懶漢式方式實現的單例始終拿到的都是同一對象,而同一對象中的屬性值肯定是相等的,那么上面介紹的第一點通過靜態實現全局共享 也可以換成單例的方式實現。

下面列舉下使用單例的場景和好處:

1、對象需要頻繁的實例化和銷毀,此時考慮使用單例可以大幅度提高性能。

2、控制資源的使用。

3、控制實例產生的數量,達到節約資源的目的。

4、作為通信媒介使用,也就是數據共享。

三、shared_preferences 數據持久化

shared_preferences 是 Flutter 提供的 key-value 存儲插件,它通過Android和iOS平臺提供的機制來實現數據持久化到磁盤中。在 iOS 上封裝的是 NSUserDefault(后綴 .plist的文件中), 在 android 上封裝的是 SharedPreferences(后綴.xml文件中)。在使用上也是如同原生一樣簡單。

為工程添加 shared_preferences 插件:
1、在pubspec.yaml文件中添加依賴
    dependencies:
      fluttertoast: ^3.0.3
      flutter:
        sdk: flutter
      #添加持久化插件 sp
      shared_preferences: ^0.5.3+1

在 pubspec.yaml 添加依賴時特別需要注意縮進,多一個或少一個空格可能都將添加不上。

2、安裝依賴庫

執行 flutter packages get 命令;AS 開發工具直接右上角 packages get也可。

3、在使用的文件中導入該庫
import 'package:shared_preferences/shared_preferences.dart';
shared_preferences 源碼分析:
    class SharedPreferences {
      SharedPreferences._(this._preferenceCache);

      static const String _prefix = 'flutter.';
      static SharedPreferences _instance;
      static Future<SharedPreferences> getInstance() async {
        if (_instance == null) {
          final Map<String, Object> preferencesMap =
              await _getSharedPreferencesMap();
          _instance = SharedPreferences._(preferencesMap);
        }
        return _instance;
      }

      /// The cache that holds all preferences.
      ///
      /// It is instantiated to the current state of the SharedPreferences or
      /// NSUserDefaults object and then kept in sync via setter methods in this
      /// class.
      ///
      /// It is NOT guaranteed that this cache and the device prefs will remain
      /// in sync since the setter method might fail for any reason.
      final Map<String, Object> _preferenceCache;

      /// Returns all keys in the persistent storage.
      Set<String> getKeys() => Set<String>.from(_preferenceCache.keys);

      /// Reads a value of any type from persistent storage.
      dynamic get(String key) => _preferenceCache[key];

      /// Reads a value from persistent storage, throwing an exception if it's not a
      /// bool.
      bool getBool(String key) => _preferenceCache[key];

      /// Reads a value from persistent storage, throwing an exception if it's not
      /// an int.
      int getInt(String key) => _preferenceCache[key];

      /// Reads a value from persistent storage, throwing an exception if it's not a
      /// double.
      double getDouble(String key) => _preferenceCache[key];

      /// Reads a value from persistent storage, throwing an exception if it's not a
      /// String.
      String getString(String key) => _preferenceCache[key];

      /// Returns true if persistent storage the contains the given [key].
      bool containsKey(String key) => _preferenceCache.containsKey(key);

      /// Reads a set of string values from persistent storage, throwing an
      /// exception if it's not a string set.
      List<String> getStringList(String key) {
        List<Object> list = _preferenceCache[key];
        if (list != null && list is! List<String>) {
          list = list.cast<String>().toList();
          _preferenceCache[key] = list;
        }
        // Make a copy of the list so that later mutations won't propagate
        return list?.toList();
      }

      /// Saves a boolean [value] to persistent storage in the background.
      ///
      /// If [value] is null, this is equivalent to calling [remove()] on the [key].
      Future<bool> setBool(String key, bool value) => _setValue('Bool', key, value);

      /// Saves an integer [value] to persistent storage in the background.
      ///
      /// If [value] is null, this is equivalent to calling [remove()] on the [key].
      Future<bool> setInt(String key, int value) => _setValue('Int', key, value);

      /// Saves a double [value] to persistent storage in the background.
      ///
      /// Android doesn't support storing doubles, so it will be stored as a float.
      ///
      /// If [value] is null, this is equivalent to calling [remove()] on the [key].
      Future<bool> setDouble(String key, double value) =>
          _setValue('Double', key, value);

      /// Saves a string [value] to persistent storage in the background.
      ///
      /// If [value] is null, this is equivalent to calling [remove()] on the [key].
      Future<bool> setString(String key, String value) =>
          _setValue('String', key, value);

      /// Saves a list of strings [value] to persistent storage in the background.
      ///
      /// If [value] is null, this is equivalent to calling [remove()] on the [key].
      Future<bool> setStringList(String key, List<String> value) =>
          _setValue('StringList', key, value);

      /// Removes an entry from persistent storage.
      Future<bool> remove(String key) => _setValue(null, key, null);

      Future<bool> _setValue(String valueType, String key, Object value) {
        final Map<String, dynamic> params = <String, dynamic>{
          'key': '$_prefix$key',
        };
        if (value == null) {
          _preferenceCache.remove(key);
          return _kChannel
              .invokeMethod<bool>('remove', params)
              .then<bool>((dynamic result) => result);
        } 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;
          }
          params['value'] = value;
          return _kChannel
              .invokeMethod<bool>('set$valueType', params)
              .then<bool>((dynamic result) => result);
        }
      }

      /// Always returns true.
      /// On iOS, synchronize is marked deprecated. On Android, we commit every set.
      @deprecated
      Future<bool> commit() async => await _kChannel.invokeMethod<bool>('commit');

      /// Completes with true once the user preferences for the app has been cleared.
      Future<bool> clear() async {
        _preferenceCache.clear();
        return await _kChannel.invokeMethod<bool>('clear');
      }

      /// Fetches the latest values from the host platform.
      ///
      /// Use this method to observe modifications that were made in native code
      /// (without using the plugin) while the app is running.
      Future<void> reload() async {
        final Map<String, Object> preferences =
            await SharedPreferences._getSharedPreferencesMap();
        _preferenceCache.clear();
        _preferenceCache.addAll(preferences);
      }

      static Future<Map<String, Object>> _getSharedPreferencesMap() async {
        final Map<String, Object> fromSystem =
            await _kChannel.invokeMapMethod<String, Object>('getAll');
        assert(fromSystem != null);
        // Strip the flutter. prefix from the returned preferences.
        final Map<String, Object> preferencesMap = <String, Object>{};
        for (String key in fromSystem.keys) {
          assert(key.startsWith(_prefix));
          preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
        }
        return preferencesMap;
      }

      /// Initializes the shared preferences with mock values for testing.
      ///
      /// If the singleton instance has been initialized already, it is automatically reloaded.
      @visibleForTesting
      static void setMockInitialValues(Map<String, dynamic> values) {
        _kChannel.setMockMethodCallHandler((MethodCall methodCall) async {
          if (methodCall.method == 'getAll') {
            return values;
          }
          return null;
        });
        _instance?.reload();
      }
    }

從源碼上看非常簡單,首先 SharedPreferences 使用的一個單例模式,且是異步的。它在數據存儲上提供了:setInt、setBool、setString 和 setStringList 方法來設置特定類型的數據。在數據讀取上提供了:getString 、getInt 、 getDouble、getStringList方法來獲取數據。此外還提供了如下幾個工具API:

1、是否包含有該 key 值從存儲:containsKey(String key)

2、移除指定的 key 值存儲:remove(String key)

3、清除全部的持久化數據:clear()

4、提交存儲數據:commit(),該方法已被廢棄。

綜合運用:

首先我們在 全局類 Global 中初始化 SharedPreferences ,使用靜態方式方便其他地方使用。

import 'package:shared_preferences/shared_preferences.dart';

class Global{

  //定義一個全局的 sp
  static SharedPreferences preferences;

  //初始化
  static void initPreferences() async{
    preferences= await SharedPreferences.getInstance();
  }

}

特別注意:SharedPreferences單例的獲取是一個異步方法所以需要 await 、async 關鍵字。

其次:在程序啟動時就初始化

 void main(){

    runApp(MyApp());

    //初始化 sp
    Global.initPreferences();
  }

下面就可以在使用的地方直接引用即可,例子:

  import 'package:flutter/material.dart';
  import 'package:flutter_learn/util/Global.dart';
  import 'package:flutter_learn/util/ToastUtil.dart';
  import 'package:shared_preferences/shared_preferences.dart';
  
  //使用初始化好的靜態資源
  SharedPreferences sharedPreferences = Global.preferences;
  //定義一個內存級別的變量
  int count = 0;
  
  class PersistenPage extends StatefulWidget {
    PersistenPage({Key key}) : super(key: key);
  
    _PersistenPageState createState() => _PersistenPageState();
  }
  
  class _PersistenPageState extends State<PersistenPage> {
    @override
    Widget build(BuildContext context) {
      
      count = sharedPreferences.getInt("count") == null
          ? 0
          : sharedPreferences.getInt("count");
  
      return Scaffold(
          appBar: AppBar(
            title: Text("持久化"),
          ),
          body: Container(
            color: Colors.blue,
            width: double.infinity,
            child: Column(
              children: <Widget>[
                SizedBox(height: 20),
                Text('ShardPreferences方式持久化',
                    style: TextStyle(color: Colors.white)),
                SizedBox(height: 20),
                Text("當前累計數據:$count"),
                Row(
                  children: <Widget>[
                    SizedBox(width: 5),
                    RaisedButton(
                      color: Colors.deepPurple,
                      elevation: 20,
                      focusElevation: 40,
                      child: Text('自增', style: TextStyle(color: Colors.white)),
                      onPressed: () {
                        setState(() {
                          count++;
                          sharedPreferences?.setInt("count", count);
                        });
                      },
                    ),
                    SizedBox(width: 5),
                    RaisedButton(
                      color: Colors.deepPurple,
                      elevation: 20,
                      focusElevation: 40,
                      child: Text('清除', style: TextStyle(color: Colors.white)),
                      onPressed: () {
                        setState(() {
                          count = 0;
                          sharedPreferences.clear();
                          // sharedPreferences.remove("count");
                        });
                      },
                    ),
                    SizedBox(width: 5),
                    RaisedButton(
                      color: Colors.deepPurple,
                      elevation: 20,
                      focusElevation: 40,
                      child: Text('是否包含', style: TextStyle(color: Colors.white)),
                      onPressed: () {
                        setState(() {
                          count = 0;
                          bool flag =  sharedPreferences.containsKey("count");
                          ToastUtil.show("是否包含: $flag");
                        });
                      },
                    ),
                    SizedBox(width: 5),
                  ],
                ),
                SizedBox(height: 10),
              ],
            ),
          ));
    }
  }

案例效果圖:

SharedPreferences.gif

從上面可以看出,無論是退出當前界面,還是關閉應用后,再次回到持久化界面任然可以讀取到存儲到文件中的值,也就是實現了數據本地持久化。

四、IO 文件讀寫操作數據持久化

Flutter 同原生一樣支持對文件的讀和寫。Flutter 中 IO 其實是 Dart 中的一部分,由于Dart VM是運行在PC或服務器操作系統下,而Flutter是運行在移動操作系統中,他們的文件系統所以會有一些差異。為此 PathProvider 插件提供了一種平臺透明的方式來訪問設備文件系統上的常用位置。該類當前支持訪問兩個文件系統位置:

1、臨時文件夾:可以使用 getTemporaryDirectory() 來獲取臨時目錄;在 iOS 上對用的是 NSCachesDirectory 在 Android 對用的是 getCacheDir()。

2、應用的Documents 目錄:可以使用 getApplicationDocumentsDirectory 來獲取 Documents 目錄;在 iOS 對應的是 NSDocumentDirectory ,在 Android 上對應的是 AppData 目錄。

【特別注意:】臨時文件夾在執行系統的清空緩存時會清空該文件夾,documents 目錄只有在刪除應用時才會清空。

PathProvider和 SharedPreferences 一樣需要引入;具體步驟如下:
1、在pubspec.yaml 文件中添加聲明:
    dependencies:
      fluttertoast: ^3.0.3
      flutter:
        sdk: flutter
      #添加持久化插件 sp
      shared_preferences: ^0.5.3+1
      #添加文件庫
      path_provider: ^1.2.0
2、安裝依賴庫

執行 flutter packages get 命令;AS 開發工具直接右上角 packages get也可。

3、在使用的文件中導入該庫
import 'package:path_provider/path_provider.dart';
File類的代碼比較多,這里就不貼內部源碼了,下面只列出開發中常用的 API :

1、create() 創建文件,多級目錄 recursive 為真則遞歸創建
2、createSync() 同步方式創建文件

3、rename() 文件重命名
4、renameSync() 同步設置文件重命名

5、copy() 復制文件
6、copySync() 同步復制文件

7、length() 獲取文件長度
8、lengthSync() 同步獲取文件長度

9、absolute 獲取絕對路徑文件

10、lastAccessed() 獲取最后一次的訪問時間
11、lastAccessedSync() 同步獲取最后一次的訪問時間

12、setLastAccessed() 設置最后一次的訪問時間
13、setLastAccessedSync() 同步設置最后一次的訪問時間

14、lastModified() 獲取最后一次修改時間
15、lastModifiedSync() 同步獲取最后一次修改時間

16、setLastModified() 設置最后一次修改時間
17、setLastModifiedSync() 同步設置最后一次修改時間

18、open() 打開文件有多種方式和文件的訪問模式,詳情參閱源碼

19、readAsBytes() 讀取字節
20、readAsBytesSync() 同步讀取字節

21、readAsString() 讀取字符串
22、readAsStringSync() 同步讀取字符串

23、readAsLines() 讀取一行
24、readAsLinesSync() 同步讀取一行

25、writeAsBytes() 寫出字節數組
26、writeAsBytesSync() 同步寫出字節數組

27、writeAsString() 寫出字符串
28、writeAsStringSync() 同步寫出字符串

對 File 的操作有太多 API 了 ,更多內容只有自己去查閱源碼了。下面我們來將之前的計數器實例來改造下,使用文件的方式。

首先我們在 全局類 Global 中創建文件 ,使用靜態方式方便其他地方使用。

import 'dart:io';

import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

class Global{

  //定義一個全局的 sp
  static SharedPreferences preferences;

  static File file;

  //初始化
  static void initPreferences() async{
    preferences= await SharedPreferences.getInstance();
  }

  //初始化一個文件,方便使用
  static void initFile() async{

    final directory = await getApplicationDocumentsDirectory();
    if (!(file is File)) {
      final String path = directory.path;
      file = File('$path/myInfo.txt');
     if( !file.existsSync()){
       // 不存在則創建文件
       file.createSync(recursive: true);
     }
    }
  }

}

其次:在程序啟動時就初始化

  void main(){

    runApp(MyApp());

    //初始化 sp
    Global.initPreferences();

    //初始化文件
    Global.initFile();
  }

下面就可以在使用的地方直接引用即可,例子:

  import 'dart:io';

  import 'package:flutter/material.dart';
  import 'package:flutter_learn/util/Global.dart';
  import 'package:flutter_learn/util/ToastUtil.dart';
  import 'package:shared_preferences/shared_preferences.dart';

  //使用初始化好的靜態資源
  SharedPreferences sharedPreferences = Global.preferences;

  //使用初始化好的本地文件
  File localFile = Global.file;

  class PersistenPage extends StatefulWidget {
    PersistenPage({Key key}) : super(key: key);

    _PersistenPageState createState() => _PersistenPageState();
  }

  class _PersistenPageState extends State<PersistenPage> {
    @override
    Widget build(BuildContext context) {

      return Scaffold(
          appBar: AppBar(
            title: Text("持久化"),
          ),
          body: SingleChildScrollView(
            child: Container(
                color: Colors.blue,
                height: 1000,
                child: Column(
                  children: <Widget>[

                   // shardPreferences(),

                    IoReadWirte()
                  ],
                ),
            ),
          ));
    }


    //文件的讀寫
    Widget IoReadWirte(){
      //定義一個內存級別的變量
      String info = localFile.readAsStringSync();
      int count =int.parse(info??0);
      return Column(
        children: <Widget>[
          SizedBox(height: 20),
          Text('文件操作方式持久化',
              style: TextStyle(color: Colors.white)),
          SizedBox(height: 20),
          Text("當前累計數據:$count"),
          Row(
            children: <Widget>[
              SizedBox(width: 5),
              RaisedButton(
                color: Colors.deepPurple,
                elevation: 20,
                focusElevation: 40,
                child: Text('自增', style: TextStyle(color: Colors.white)),
                onPressed: () {
                  setState(() {
                    count++;
                    localFile.writeAsStringSync(count.toString());

                  });
                },
              ),
              SizedBox(width: 5),
              RaisedButton(
                color: Colors.deepPurple,
                elevation: 20,
                focusElevation: 40,
                child: Text('自減', style: TextStyle(color: Colors.white)),
                onPressed: () {
                  setState(() {
                    count--;
                    localFile.writeAsStringSync(count.toString());
                  });
                },
              ),
            ],
          ),
          SizedBox(height: 10),
        ],
      );
    }
  }

為了方便起見和減少文章篇幅,這里只貼出了 File 操作的全部代碼,shardPreferences 中的代碼暫時刪除,文末的工程實例中擁有全部代碼。

效果如下:

IO操作.gif

無論是用 SharedPreferences 還是文件操作 方式實現數據持久化都是非常簡單,在開發中 如果只是存儲一些簡單數據 其實用 SharedPreferences 就非常好了,如果涉及到一些文件(如:圖片,文檔等)才會去使用 File。

上面兩點都只是對一些單一數據的持久化,而要持久化關系型數據就顯現的捉衿見肘了,對于有關系的數據我們首先想到的肯定是通過數據庫去存儲,而數據庫在開發中也是重中之中,涉及到篇幅問題這里就不介紹了,這也是為下一篇文章做鋪墊吧

本章中很多都是通過靜態的方式去存值,其實在開發中應該減少這種方式的使用 即在需要使用的時候才去獲取,避免不必要的內存消耗!我這里只是為了方便演示才這樣寫。

好了本章節到此結束,又到了說再見的時候了,如果你喜歡請留下你的小紅星,創作真心也不容易;你們的支持才是創作的動力,如有錯誤,請熱心的你留言指正, 謝謝大家觀看,下章再會 O(∩_∩)O

實例源碼地址: https://github.com/zhengzaihong/flutter_learn

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容