Flutter 入門指北(Part 14)之實戰(zhàn)篇

該文已授權(quán)公眾號 「碼個蛋」对嚼,轉(zhuǎn)載請指明出處

講完了常用的部件和網(wǎng)絡(luò)請求后,差不多該進(jìn)入整體實戰(zhàn)了绳慎,這里我們將寫一個比較熟悉的項目纵竖,郭神的 cool weather。項目將使用 fluro 實現(xiàn)路由管理偷线,dio 實現(xiàn)網(wǎng)絡(luò)請求磨确,rxdart 實現(xiàn) BLoC 進(jìn)行狀態(tài)管理和邏輯分離,使用文件声邦,shared_preferences乏奥,sqflite 實現(xiàn)本地的數(shù)據(jù)持久化。這邊先給出項目的地址:flutter_weather亥曹,以及最后實現(xiàn)的效果圖:

首頁.png
天氣詳情頁.gif
城市切換.gif
全局主題切換.gif

除了 fluro 別的基本上前面都講了邓了,所以在開始正式的實戰(zhàn)前恨诱,先講下 fluro

Fluro

fluro 是對 Navigator 的一個封裝,方便更好的管理路由跳轉(zhuǎn)骗炉,當(dāng)然還存在一些缺陷照宝,例如目前只支持傳遞字符串,不能傳遞中文等句葵,但是這些問題都算不上是大問題厕鹃。

fluro 的使用很簡單,大概分如下的步驟:

  1. 在全局定義一個 Router 實例

    final router = Router(); 
    
  2. 使用 Router 實例定義路徑和其對應(yīng)的 Handler 對象

    // 例如定義一個 CityPage 的路徑和 Handler
    Handler cityHandler = Handler(handlerFunc: (_, params) {
      // 傳遞的參數(shù)都在 params 中乍丈,params 是一個 Map<String, List<String>> 類型參數(shù)
      String cityId = params['city_id']?.first; 
      return BlocProvider(child: WeatherPage(city: cityId), bloc: WeatherBloc());
    });
    
    // 定義路由的路徑和參數(shù)
    // 需要注意的是剂碴,第一個頁面的路徑必須為 "/",別的可為 "/" + 任意拼接
    router.define('/city', handler: cityHandler);
    // 或者官方提供的另一種方式
    router.define('/city/:city_id', handler: cityHandler);
    
  3. router 注冊到 MaterialApponGenerateRoute

    MaterialApp(onGenerateRoute: router); 
    
  4. 最后通過 Router 實例進(jìn)行跳轉(zhuǎn)轻专,如果有參數(shù)傳遞則會在新的頁面收到

    router.navigateTo(context, '/city?city_id=CN13579');
    // 或者官方的方式
    router.navigateTo(context, '/city/CN13579');
    

在 fluro 中提供了多種路由動畫忆矛,包括 fadeIninFromRight 等请垛。講完了使用催训,就進(jìn)入實戰(zhàn)了。

flutter_weather 實戰(zhàn)

導(dǎo)入插件

在開始的時候宗收,已經(jīng)提到了整體功能的實現(xiàn)需求漫拭,所以這邊需要導(dǎo)入的插件以及存放圖片的文件夾如下:

dependencies:
  flutter:
    sdk: flutter
    
  cupertino_icons: ^0.1.2
  fluro: ^1.4.0
  dio: ^2.1.0
  shared_preferences: ^0.5.1+2
  sqflite: ^1.1.3
  fluttertoast: ^3.0.3
  rxdart: ^0.21.0
  path_provider: 0.5.0+1

dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true

  assets:
    - images/
頂層靜態(tài)實例的實現(xiàn)

有許多實例需要在頂層注冊,然后在全局使用镜雨,包括但不限于 fluro 的 router嫂侍,http,database 等等荚坞。在這個項目中挑宠,需要用到的就是這三個實例,會在全局調(diào)用颓影,所以在開始前進(jìn)行初始化各淀,當(dāng)然 http 和 database 在使用的時候創(chuàng)建也可以,完全看個人習(xí)慣诡挂,但是 fluro 的管理類必須在一開始就注冊完成碎浇。首先需要定義一個 Application 類用來存放這些靜態(tài)實例

class Application {
  static HttpUtils http; // 全局網(wǎng)絡(luò)
  static Router router; // 全局路由
  static DatabaseUtils db; // 全局?jǐn)?shù)據(jù)庫
}

接著就是對相應(yīng)方法類的編寫,其中 HttpUtilDatabaseUtils 在前面有講過璃俗,這邊不重復(fù)講奴璃,會講下數(shù)據(jù)庫如何建立。

Fluro 路由管理類

首先城豁,需要知道苟穆,該項目的界面大概分如下的界面(當(dāng)然可先只定義首頁,剩下用到了再定義,該項目相對簡單雳旅,所以先列出來):省選擇頁跟磨,市選擇頁,區(qū)選擇頁攒盈,天氣展示頁抵拘,設(shè)置頁。所以 fluro 的管理類可按如下定義:

// 查看 `routers/routers.dart` 文件
class Routers {
  /// 各個頁面對應(yīng)的路徑
  static const root = '/';
  static const weather = '/weather';
  static const provinces = '/provinces';
  static const cities = '/cities';
  static const districts = '/districts';
  static const settings = '/settings';

  /// 該方法用于放到 `main` 方法中定義所有的路由型豁,
  /// 對應(yīng)的 handler 可放同一個文件僵蛛,也可放另一個文件,看個人喜好
  static configureRouters(Router router) {
    router.notFoundHandler = notFoundHandler;

    router.define(root, handler: rootHandler); // 首頁

    router.define(weather, handler: weatherHandler); // 天氣展示頁

    router.define(provinces, handler: provincesHandler); // 省列表頁

    router.define(cities, handler: citiesHandler); // 省下市列表頁

    router.define(districts, handler: districtsHandler); // 市下區(qū)列表頁

    router.define(settings, handler: settingsHandler); // 設(shè)置頁
  }

  /// 生成天氣顯示頁面路徑,需要用到城市 id
  static generateWeatherRouterPath(String cityId) => '$weather?city_id=$cityId';

  /// 生成省下的市列表頁相應(yīng)路徑 需要用到省 id 及省名
  static generateProvinceRouterPath(int provinceId, String name)
                    => '$cities?province_id=$provinceId&name=$name';
    
  /// 生成市下的區(qū)列表頁相應(yīng)路徑,需用到市 id 及市名
  static generateCityRouterPath(int provinceId, int cityId, String name) 
                    => '$districts?province_id=$provinceId&city_id=$cityId&name=$name';
}
/// 查看 `routers/handler.dart` 文件
Handler notFoundHandler = Handler(handlerFunc: (_, params) {
  Logger('RouterHandler:').log('Not Found Router'); // 當(dāng)找不到相應(yīng)的路由時豹芯,打印信息處理
});

Handler rootHandler = Handler(handlerFunc: (_, params) => SplashPage());

Handler weatherHandler = Handler(handlerFunc: (_, params) {
  String cityId = params['city_id']?.first; // 獲取相應(yīng)的參數(shù)
  return WeatherPage(city: cityId);
});

Handler provincesHandler = Handler(handlerFunc: (_, params) => ProvinceListPage());

Handler citiesHandler = Handler(handlerFunc: (_, params) {
  String provinceId = params['province_id']?.first;
  String name = params['name']?.first;
  return CityListPage(provinceId: provinceId, 
                      name: FluroConvertUtils.fluroCnParamsDecode(name));
});

Handler districtsHandler = Handler(handlerFunc: (_, params) {
  String provinceId = params['province_id']?.first;
  String cityId = params['city_id']?.first;
  String name = params['name']?.first;
  return DistrictListPage(provinceId: provinceId, cityId: cityId, 
                          name: FluroConvertUtils.fluroCnParamsDecode(name));
});

Handler settingsHandler = Handler(handlerFunc: (_, params) => SettingsPage());

那么界面的路由到這就編寫好了亿卤,但是前面提到了 fluro 目前不支持中文的傳遞,所以在傳遞中文時候热凹,需要先進(jìn)行轉(zhuǎn)碼泵喘,這邊提供一個自己寫的方法,小伙伴有更好的方法也可以直接在項目提 issue

/// 查看 `utils/fluro_convert_util.dart` 文件
class FluroConvertUtils {
  /// fluro 傳遞中文參數(shù)前般妙,先轉(zhuǎn)換纪铺,fluro 不支持中文傳遞
  static String fluroCnParamsEncode(String originalCn) {
    StringBuffer sb = StringBuffer();
    var encoded = Utf8Encoder().convert(originalCn); // utf8 編碼,會生成一個 int 列表
    encoded.forEach((val) => sb.write('$val,')); // 將 int 列表重新轉(zhuǎn)換成字符串
    return sb.toString().substring(0, sb.length - 1).toString();
  }

  /// fluro 傳遞后取出參數(shù)碟渺,解析
  static String fluroCnParamsDecode(String encodedCn) {
    var decoded = encodedCn.split('[').last.split(']').first.split(','); // 對參數(shù)字符串分割
    var list = <int>[];
    decoded.forEach((s) => list.add(int.parse(s.trim()))); // 轉(zhuǎn)回 int 列表
    return Utf8Decoder().convert(list); // 解碼
  }
}
Database 管理類編寫

因為數(shù)據(jù)庫的開啟是一個很耗資源的過程鲜锚,所以這邊通過單例并提取到頂層。在該項目中苫拍,數(shù)據(jù)庫主要用于存儲城市信息芜繁,因為城市之間的關(guān)聯(lián)比較復(fù)雜,如果通過 shared_preferences 或者文件存儲會很復(fù)雜绒极。

/// 查看 `utils/db_utils.dart` 文件
class DatabaseUtils {
  final String _dbName = 'weather.db'; // 數(shù)據(jù)表名
  final String _tableProvinces = 'provinces'; // 省表
  final String _tableCities = 'cities'; // 市表
  final String _tableDistricts = 'districts'; // 區(qū)表
  static Database _db;

  static DatabaseUtils _instance;

  static DatabaseUtils get instance => DatabaseUtils();

  /// 將數(shù)據(jù)庫的初始化放到私有構(gòu)造中骏令,值允許通過單例訪問
  DatabaseUtils._internal() {
    getDatabasesPath().then((path) async {
      _db = await openDatabase(join(path, _dbName), version: 1, onCreate: (db, version) {
        db.execute('create table $_tableProvinces('
            'id integer primary key autoincrement,'
            'province_id integer not null unique,' // 省 id,id 唯一
            'province_name text not null' // 省名
            ')');

        db.execute('create table $_tableCities('
            'id integer primary key autoincrement,'
            'city_id integer not null unique,' // 市 id垄提,id 唯一
            'city_name text not null,' // 市名
            'province_id integer not null,' // 對應(yīng)的省的 id榔袋,作為外鍵同省表關(guān)聯(lián)
            'foreign key(province_id) references $_tableProvinces(province_id)'
            ')');

        db.execute('create table $_tableDistricts('
            'id integer primary key autoincrement,'
            'district_id integer not null unique,' // 區(qū) id
            'district_name text not null,' // 區(qū)名
            'weather_id text not null unique,' // 查詢天氣用的 id,例如 CN13579826铡俐,id 唯一
            'city_id integer not null,' // 對應(yīng)市的 id凰兑,作為外鍵同市表關(guān)聯(lián)
            'foreign key(city_id) references $_tableCities(city_id)'
            ')');
      }, onUpgrade: (db, oldVersion, newVersion) {});
    });
  }

  /// 構(gòu)建單例
  factory DatabaseUtils() {
    if (_instance == null) {
      _instance = DatabaseUtils._internal();
    }
    return _instance;
  }

  /// 查詢所有的省,`ProvinceModel` 為省市接口返回數(shù)據(jù)生成的 model 類
  /// 查看 `model/province_model.dart` 文件
  Future<List<ProvinceModel>> queryAllProvinces() async =>
      ProvinceModel.fromProvinceTableList(await _db.rawQuery('select province_id, province_name from $_tableProvinces'));

  /// 查詢某個省內(nèi)的所有市
  Future<List<ProvinceModel>> queryAllCitiesInProvince(String proid) async => ProvinceModel.fromCityTableList(await _db.rawQuery(
        'select city_id, city_name from $_tableCities where province_id = ?',
        [proid],
      ));

  /// 查詢某個市內(nèi)的所有區(qū)审丘,`DistrictModel` 為區(qū)接口返回數(shù)據(jù)生成的 model 類
  /// 查看 `model/district_model.dart` 文件
  Future<List<DistrictModel>> queryAllDistrictsInCity(String cityid) async => DistrictModel.fromDistrictTableList(await _db.rawQuery(
        'select district_id, district_name, weather_id from $_tableDistricts where city_id = ?',
        [cityid],
      ));

  /// 將所有的省插入數(shù)據(jù)庫
  Future<void> insertProvinces(List<ProvinceModel> provinces) async {
    var batch = _db.batch();
    provinces.forEach((p) => batch.rawInsert(
          'insert or ignore into $_tableProvinces (province_id, province_name) values (?, ?)',
          [p.id, p.name],
        ));
    batch.commit();
  }

  /// 將省對應(yīng)下的所有市插入數(shù)據(jù)庫
  Future<void> insertCitiesInProvince(List<ProvinceModel> cities, String proid) async {
    var batch = _db.batch();
    cities.forEach((c) => batch.rawInsert(
          'insert or ignore into $_tableCities (city_id, city_name, province_id) values (?, ?, ?)',
          [c.id, c.name, proid],
        ));
    batch.commit();
  }

  /// 將市下的所有區(qū)插入數(shù)據(jù)庫
  Future<void> insertDistrictsInCity(List<DistrictModel> districts, String cityid) async {
    var batch = _db.batch();
    districts.forEach((d) => batch.rawInsert(
          'insert or ignore into $_tableDistricts (district_id, district_name, weather_id, city_id) values (?, ?, ?, ?)',
          [d.id, d.name, d.weatherId, cityid],
        ));
    batch.commit();
  }
}

定義完全局使用的方法吏够,就可以在 main 函數(shù)中進(jìn)行相關(guān)的初始化了

/// 查看 `main.dart` 文件
void main() {
  // 初始化 fluro router
  Router router = Router();
  Routers.configureRouters(router);
  Application.router = router;

  // 初始化 http
  Application.http = HttpUtils(baseUrl: WeatherApi.WEATHER_HOST);

  // 初始化 db
  Application.db = DatabaseUtils.instance;

  // 強(qiáng)制豎屏,因為設(shè)置豎屏為 `Future` 方法,防止設(shè)置無效可等返回值后再啟動 App
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitDown, DeviceOrientation.portraitUp]).then((_) {
    runApp(WeatherApp()); // App 類可放在同個文件稿饰,個人習(xí)慣單獨一個文件存放

    if (Platform.isAndroid) {
      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
    }
  });
}
class WeatherApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
            title: 'Weather App',
            onGenerateRoute: Application.router.generator, // 將 fluro 的路由進(jìn)行注冊
            debugShowCheckedModeBanner: false,
          );
  }
}

初始化完畢锦秒,接著就可以進(jìn)行頁面的編寫了。

首頁編寫

首頁主要是為了對 App 的一個大概展示喉镰,或者是一些廣告的展示旅择,同時也給一些數(shù)據(jù)初始化提供時間,當(dāng)用戶進(jìn)入后有更好的體驗效果侣姆。我們在這里就做一個圖標(biāo)的展示(圖標(biāo)可自行到項目中 images 文件夾查找)生真,延時 5s 后跳轉(zhuǎn)下個頁面。

/// 查看 `splash_page.dart` 文件
class SplashPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// 因為已經(jīng)引入了 rxdart捺宗,這里通過 rxdart.timer 進(jìn)行倒計時
    /// 當(dāng)然也可以使用 Futuer.delayed 進(jìn)行倒計時
    /// 5s 計時柱蟀,如果已經(jīng)選擇城市,跳轉(zhuǎn)天氣界面蚜厉,否則進(jìn)入城市選擇
    Observable.timer(0, Duration(milliseconds: 5000)).listen((_) {
      PreferenceUtils.instance.getString(PreferencesKey.WEATHER_CITY_ID)
          .then((city) {
        // 如果當(dāng)前還未選擇城市长已,則進(jìn)入城市選擇頁,否則跳轉(zhuǎn)天氣詳情頁
        // replace: true 即為 Navigator.pushReplacement 方法
        Application.router.navigateTo(context, city.isEmpty 
                                      ? Routers.provinces 
                                      : Routers.generateWeatherRouterPath(city), 
                                                                        replace: true);
      });
    });

    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        color: Colors.white,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 展示圖標(biāo)
            Image.asset(Resource.pngSplash, width: 200.0, height: 200.0),
            // 展示文字提醒昼牛,用 SizedBox 設(shè)置區(qū)域大小
            SizedBox(
                width: MediaQuery.of(context).size.width * 0.7,
                child: Text(
                  '所有天氣數(shù)據(jù)均為模擬數(shù)據(jù)术瓮,僅用作學(xué)習(xí)目的使用,請勿當(dāng)作真實的天氣預(yù)報軟件來使用',
                  textAlign: TextAlign.center,
                  softWrap: true,
                  style: TextStyle(color: Colors.red[700], fontSize: 16.0),
                ))
          ],
        ),
      ),
    );
  }
}
城市選擇頁面

當(dāng)首次進(jìn)入的時候贰健,用戶肯定沒有選擇城市胞四,所以先編寫城市選擇列表頁面,因為整體的項目使用 BLoC 分離業(yè)務(wù)邏輯和頁面伶椿,所以先編寫數(shù)據(jù)管理類吧辜伟,把數(shù)據(jù)請求和改變的業(yè)務(wù)邏輯放到這塊,BLoC 的實現(xiàn)在前面講過了脊另,這邊就不重復(fù)提了导狡。可以查看文章:狀態(tài)管理及 BLoC

/// 查看 `provinces_bloc.dart` 文件
class ProvincesBloc extends BaseBloc {
  final _logger = Logger('ProvincesBloc');

  List<ProvinceModel> _provinces = []; // 全國省
  List<ProvinceModel> _cities = []; // 省內(nèi)市
  List<DistrictModel> _districts = []; // 市內(nèi)區(qū)

  List<ProvinceModel> get provinces => _provinces;

  List<ProvinceModel> get cities => _cities;

  List<DistrictModel> get districts => _districts;

  BehaviorSubject<List<ProvinceModel>> _provinceController = BehaviorSubject();

  BehaviorSubject<List<ProvinceModel>> _citiesController = BehaviorSubject();

  BehaviorSubject<List<DistrictModel>> _districtController = BehaviorSubject();

  /// stream尝蠕,用于 StreamBuilder 的 stream 參數(shù)
  Observable<List<ProvinceModel>> get provinceStream 
                                             => Observable(_provinceController.stream);

  Observable<List<ProvinceModel>> get cityStream => Observable(_citiesController.stream);

  Observable<List<DistrictModel>> get districtStream
                                            => Observable(_districtController.stream);

  /// 通知刷新省份列表
  changeProvinces(List<ProvinceModel> provinces) {
    _provinces.clear();
    _provinces.addAll(provinces);
    _provinceController.add(_provinces);
  }

  /// 通知刷新城市列表
  changeCities(List<ProvinceModel> cities) {
    _cities.clear();
    _cities.addAll(cities);
    _citiesController.add(_cities);
  }

  /// 通知刷新區(qū)列表
  changeDistricts(List<DistrictModel> districts) {
    _districts.clear();
    _districts.addAll(districts);
    _districtController.add(_districts);
  }

  /// 請求全國省
  Future<List<ProvinceModel>> requestAllProvinces() async {
    var resp = await Application.http.getRequest(WeatherApi.WEATHER_PROVINCE, 
                                           error: (msg) => _logger.log(msg, 'province'));
    return resp == null || resp.data == null ? [] : ProvinceModel.fromMapList(resp.data);
  }

  /// 請求省內(nèi)城市
  Future<List<ProvinceModel>> requestAllCitiesInProvince(String proid) async {
    var resp = await Application.http
        .getRequest('${WeatherApi.WEATHER_PROVINCE}/$proid', 
                                           error: (msg) => _logger.log(msg, 'city'));
    return resp == null || resp.data == null ? [] : ProvinceModel.fromMapList(resp.data);
  }

  /// 請求市內(nèi)的區(qū)
  Future<List<DistrictModel>> requestAllDistricts(String proid, String cityid) async {
    var resp = await Application.http
        .getRequest('${WeatherApi.WEATHER_PROVINCE}/$proid/$cityid', 
                                           error: (msg) => _logger.log(msg, 'district'));
    return resp == null || resp.data == null ? [] : DistrictModel.fromMapList(resp.data);
  }
    
  @override
  void dispose() { // 及時銷毀
    _provinceController?.close();
    _citiesController?.close();
    _districtController?.close();
  }
}

寫完 BLoC 需要對其進(jìn)行注冊烘豌,因為城市選擇相對還是比較頻繁的,所以可以放最頂層進(jìn)行注冊

return  BlocProvider(
      bloc: ProvincesBloc(), // 城市切換 BLoC
      child: MaterialApp(
        title: 'Weather App',
        onGenerateRoute: Application.router.generator,
        debugShowCheckedModeBanner: false,
      ),
    );

城市選擇就是一個列表看彼,直接通過 ListView 生成即可廊佩,前面講 ListView 的時候提到,盡可能固定 item 的高度靖榕,會提高繪制效率

/// 查看 `provinces_page.dart` 文件
class ProvinceListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _bloc = BlocProvider.of<ProvincesBloc>(context);

    // 進(jìn)入的時候先使用數(shù)據(jù)庫的數(shù)據(jù)填充界面
    Application.db.queryAllProvinces().then((ps) => _bloc.changeProvinces(ps));
    // 網(wǎng)絡(luò)數(shù)據(jù)更新列表并刷新數(shù)據(jù)庫數(shù)據(jù)
    _bloc.requestAllProvinces().then((provinces) {
      _bloc.changeProvinces(provinces);
      Application.db.insertProvinces(provinces);
    });

    return Scaffold(
      appBar: AppBar(
        title: Text('請選擇省份'),
      ),
      body: Container(
        color: Colors.black12,
        alignment: Alignment.center,
        // 省列表選擇
        child: StreamBuilder(
          stream: _bloc.provinceStream,
          initialData: _bloc.provinces,
          builder: (_, AsyncSnapshot<List<ProvinceModel>> snapshot) 
            => !snapshot.hasData || snapshot.data.isEmpty
            // 如果當(dāng)前的數(shù)據(jù)未加載則給一個加載标锄,否則顯示列表加載
              ? CupertinoActivityIndicator(radius: 12.0) 
              : ListView.builder(
              physics: BouncingScrollPhysics(),
              padding: const EdgeInsets.symmetric(horizontal: 12.0),
              itemBuilder: (_, index) => InkWell(
                child: Container(
                  alignment: Alignment.centerLeft,
                  child: Text(snapshot.data[index].name, style: TextStyle(fontSize: 18.0, color: Colors.black)),
                ),
                onTap: () => Application.router.navigateTo(
                    context,
                    // 跳轉(zhuǎn)下層省內(nèi)城市選擇,需要將當(dāng)前的省 id 以及省名傳入
                    Routers.
                    generateProvinceRouterPath(snapshot.data[index].id, FluroConvertUtils.fluroCnParamsEncode(snapshot.data[index].name)),
                    transition: TransitionType.fadeIn),
              ),
              itemExtent: 50.0,
              itemCount: snapshot.data.length),
        ),
      ),
    );
  }
}

對于市和區(qū)的列表選擇也類似茁计,除了最后的點擊會有些區(qū)別頁面的布局幾乎一致料皇,這邊只提下點擊事件

/// 查看 `cities_page.dart` 文件
Application.router.navigateTo(
                                    context,
                                    // 跳轉(zhuǎn)下層省內(nèi)城市選擇
                                    Routers.generateProvinceRouterPath(
                                        snapshot.data[index].id, FluroConvertUtils.fluroCnParamsEncode(snapshot.data[index].name)),
                                    transition: TransitionType.fadeIn),
                              )
// 設(shè)置為當(dāng)前區(qū)谓松,并清理路由 stack,并將天氣界面設(shè)置到最上層
onTap: () {
 PreferenceUtils.instance
     .saveString(PreferencesKey.WEATHER_CITY_ID, snapshot.data[index].weatherId);
    
                                  Application.router.navigateTo(context, Routers.generateWeatherRouterPath(snapshot.data[index].weatherId),
                                      transition: TransitionType.inFromRight, clearStack: true);
                                })
天氣詳情頁面

天氣詳情頁面相對部件會多點践剂,為了看著舒服一點鬼譬,這里拆成多個部分來編寫,在這之前還是先編寫數(shù)據(jù)的管理類逊脯,因為天氣詳情接口返回的數(shù)據(jù)嵌套層次比較多优质,關(guān)系比較復(fù)雜,不適合用 database 來做持久化军洼,所以這里采用文件持久化方式巩螃。當(dāng)然有些小伙伴會問干嘛不使用 shared_preferences 來存儲,理論上應(yīng)該沒有太大的問題匕争,但是個人建議相對復(fù)雜的數(shù)據(jù)使用文件存儲會相對比較好點避乏,一定要說個為什么,我也說不出來甘桑。

/// 查看 `weather_bloc.dart` 文件
class WeatherBloc extends BaseBloc {
  final _logger = Logger('WeatherBloc');

  WeatherModel _weather; // 天氣情況

  String _background = WeatherApi.DEFAULT_BACKGROUND; // 背景

  WeatherModel get weather => _weather;

  String get background => _background;

  BehaviorSubject<WeatherModel> _weatherController = BehaviorSubject();

  BehaviorSubject<String> _backgroundController = BehaviorSubject();

  Observable<WeatherModel> get weatherStream => Observable(_weatherController.stream);

  Observable<String> get backgroundStream => Observable(_backgroundController.stream);

  /// 更新天氣情況
  updateWeather(WeatherModel weather) {
    _weather = weather;
    _weatherController.add(_weather);
  }

  /// 更新天氣背景
  updateBackground(String background) {
    _background = background;
    _backgroundController.add(_background);
  }

  // 請求天氣情況
  Future<WeatherModel> requestWeather(String id) async {
    var resp = await Application.http
        .getRequest(WeatherApi.WEATHER_STATUS, 
                    params: {'cityid': id, 'key': WeatherApi.WEATHER_KEY}, 
                    error: (msg) => _logger.log(msg, 'weather'));
    // 請求數(shù)據(jù)成功則寫入到文件中
    if (resp != null && resp.data != null) {
      _writeIntoFile(json.encode(resp.data));
    }
    return WeatherModel.fromMap(resp.data);
  }

  Future<String> requestBackground() async {
    var resp = await Application.http
        .getRequest<String>(WeatherApi.WEATHER_BACKGROUND, 
                            error: (msg) => _logger.log(msg, 'background'));
    return resp == null || resp.data == null ? WeatherApi.DEFAULT_BACKGROUND : resp.data;
  }

  // 獲取存儲文件路徑
  Future<String> _getPath() async => 
      '${(await getApplicationDocumentsDirectory()).path}/weather.txt';

  // 寫入到文件
  _writeIntoFile(String contents) async {
    File file = File(await _getPath());
    if (await file.exists()) file.deleteSync();
    file.createSync();
    file.writeAsString(contents);
  }

  // 文件讀取存儲信息拍皮,如果不存在文件則返回空字符串 '',不推薦返回 null
  Future<String> readWeatherFromFile() async {
    File file = File(await _getPath());
    return (await file.exists()) ? file.readAsString() : '';
  }

  @override
  void dispose() {
    _weatherController?.close();
    _backgroundController?.close();
  }
}

天氣詳情的刷新只有當(dāng)個頁面跑杭,所以 BLoC 的注冊值需要在路由上注冊即可春缕,在 fluro 對應(yīng) handler 中加入注冊

Handler weatherHandler = Handler(handlerFunc: (_, params) {
  String cityId = params['city_id']?.first; // 這個 id 可以通過 BLoC 獲取也可以
  return BlocProvider(child: WeatherPage(city: cityId), bloc: WeatherBloc());
});

那么接下來就可以編寫界面了,先實現(xiàn)最外層的背景圖變化

/// 查看 `weather_page.dart` 文件
class WeatherPage extends StatelessWidget {
  final String city;

  WeatherPage({Key key, this.city}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var _bloc = BlocProvider.of<WeatherBloc>(context);
    // 請求背景并更新
    _bloc.requestBackground().then((b) => _bloc.updateBackground(b));

    // 先讀取本地文件緩存進(jìn)行頁面填充
    _bloc.readWeatherFromFile().then((s) {
      if (s.isNotEmpty) {
        _bloc.updateWeather(WeatherModel.fromMap(json.decode(s)));
      }
    });

    // 再請求網(wǎng)絡(luò)更新數(shù)據(jù)
    _bloc.requestWeather(city).then((w) => _bloc.updateWeather(w));

    return Scaffold(
      body: StreamBuilder(
          stream: _bloc.backgroundStream,
          initialData: _bloc.background,
          builder: (_, AsyncSnapshot<String> themeSnapshot) => Container(
                padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: Colors.black12,
                  image: DecorationImage(
                      image: NetworkImage(themeSnapshot.data), fit: BoxFit.cover),
                ),
                child: // 具體內(nèi)部布局通過拆分小部件實現(xiàn)
              )),
    );
  }
}

頁面最頂部是顯示兩個按鈕艘蹋,一個跳轉(zhuǎn)城市選擇,一個跳轉(zhuǎn)設(shè)置頁面票灰,顯示當(dāng)前的城市

class FollowedHeader extends StatelessWidget {
  final AsyncSnapshot<WeatherModel> snapshot; // snapshot 通過上層傳入

  FollowedHeader({Key key, this.snapshot}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        // 城市選擇頁面跳轉(zhuǎn)按鈕
        IconButton(
            icon: Icon(Icons.home, color: Colors.white, size: 32.0),
            onPressed: () => Application.router.
            navigateTo(context, Routers.provinces, 
                       transition: TransitionType.inFromLeft)),
        // 當(dāng)前城市
        Text('${snapshot.data.heWeather[0].basic.location}', 
             style: TextStyle(fontSize: 28.0, color: Colors.white)),
        // 設(shè)置頁面跳轉(zhuǎn)按鈕
        IconButton(
            icon: Icon(Icons.settings, color: Colors.white, size: 32.0),
            onPressed: () => Application.router
            .navigateTo(context, Routers.settings, 
                        transition: TransitionType.inFromRight))
      ],
    );
  }
}

接著是當(dāng)前的天氣詳情部分

class CurrentWeatherState extends StatelessWidget {
  final AsyncSnapshot<WeatherModel> snapshot;

  CurrentWeatherState({Key key, this.snapshot}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var _now = snapshot.data.heWeather[0].now;
    var _update = snapshot.data.heWeather[0].update.loc.split(' ').last;
      
    return Column(
      crossAxisAlignment: CrossAxisAlignment.end,
      children: <Widget>[
        // 當(dāng)前的溫度
        Text('${_now.tmp}℃', style: TextStyle(fontSize: 50.0, color: Colors.white)),
        // 當(dāng)前的天氣狀況
        Text('${_now.condTxt}', style: TextStyle(fontSize: 24.0, color: Colors.white)),
        Row( // 刷新的時間
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            Icon(Icons.refresh, size: 16.0, color: Colors.white),
            Padding(padding: const EdgeInsets.only(left: 4.0)),
            Text(_update, style: TextStyle(fontSize: 12.0, color: Colors.white))
          ],
        )
      ],
    );
  }
}

接下來是一個天氣預(yù)報的列表塊女阀,以為是一個列表,當(dāng)然可以通過 Cloumn 來實現(xiàn)屑迂,但是前面有提到過一個列表「粘合劑」---- CustomScrollView浸策,所以這里的整體連接最后會通過 CustomScrollView 來實現(xiàn),那么你可以放心在最上層容器的 child 屬性加上 CustomScrollView 了惹盼。接著來實現(xiàn)這塊預(yù)報模塊

class WeatherForecast extends StatelessWidget {
  final AsyncSnapshot<WeatherModel> snapshot;

  WeatherForecast({Key key, this.snapshot}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var _forecastList = snapshot.data.heWeather[0].dailyForecasts; // 獲取天氣預(yù)報

    return SliverFixedExtentList(
        delegate: SliverChildBuilderDelegate(
          (_, index) => Container(
              color: Colors.black54, // 外層設(shè)置背景色庸汗,防止被最外層圖片背景遮擋文字
              padding: const EdgeInsets.all(12.0),
              alignment: Alignment.centerLeft,
              child: index == 0 // 當(dāng)?shù)谝粋€ item 情況,顯示 ‘預(yù)報’
                  ? Text('預(yù)報', style: TextStyle(fontSize: 24.0, color: Colors.white))
                  : Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        Text(_forecastList[index - 1].date,  // 預(yù)報的日期
                             style: TextStyle(fontSize: 16.0, color: Colors.white)),
                        Expanded( // 天氣情況手报,這邊通過 expanded 進(jìn)行占位蚯舱,并居中顯示
                            child: Center(child: Text(_forecastList[index - 1].cond.txtD, 
                                                      style: TextStyle(fontSize: 16.0,                                                                  color: Colors.white))),
                            flex: 2),
                        Expanded(
                            child: Row( // 最高溫度,最低溫度
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: <Widget>[
                                Text(_forecastList[index - 1].tmp.max, 
                                     style: TextStyle(fontSize: 16.0, 
                                                      color: Colors.white)),
                                Text(_forecastList[index - 1].tmp.min, 
                                     style: TextStyle(fontSize: 16.0, 
                                                      color: Colors.white)),
                              ],
                            ),
                            flex: 1)
                      ],
                    )),
          childCount: _forecastList.length + 1, // 這個數(shù)量需要 +1掩蛤,因為有個標(biāo)題需要一個數(shù)量
        ),
        itemExtent: 50.0);
  }
}

接著是空氣質(zhì)量報告枉昏,一個標(biāo)題,下面由兩個布局進(jìn)行平分

class AirQuality extends StatelessWidget {
  final AsyncSnapshot<WeatherModel> snapshot;

  AirQuality({Key key, this.snapshot}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var quality = snapshot.data.heWeather[0].aqi.city;
    return Container(
        padding: const EdgeInsets.all(12.0),
        color: Colors.black54,
        alignment: Alignment.centerLeft,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            // 標(biāo)題
            Padding(padding: const EdgeInsets.only(bottom: 20.0), child: 
                    Text('空氣質(zhì)量', style: TextStyle(fontSize: 24.0, 
                                                  color: Colors.white))),
            Row(
              children: <Widget>[
                // 通過 expanded 進(jìn)行平分橫向距離
                Expanded(
                    child: Center(
                  // 內(nèi)部居中顯示
                  child: Column(
                    children: <Widget>[
                      Text('${quality.aqi}', style: 
                           TextStyle(fontSize: 40.0, color: Colors.white)),
                      Text('AQI 指數(shù)', style: 
                           TextStyle(fontSize: 20.0, color: Colors.white)),
                    ],
                  ),
                )),
                Expanded(
                    child: Center(
                  child: Column(
                    children: <Widget>[
                      Text('${quality.pm25}', style: 
                           TextStyle(fontSize: 40.0, color: Colors.white)),
                      Text('PM2.5 指數(shù)', style: 
                           TextStyle(fontSize: 20.0, color: Colors.white)),
                    ],
                  ),
                )),
              ],
            )
          ],
        ));
  }
}

接下來是生活質(zhì)量模塊揍鸟,看著也是個列表兄裂,但是后臺返回的不是列表,而是根據(jù)不同字段獲取不同質(zhì)量指數(shù),因為布局類似晰奖,所以可以對其進(jìn)行封裝再整體調(diào)用

class LifeSuggestions extends StatelessWidget {
  final AsyncSnapshot<WeatherModel> snapshot;

  LifeSuggestions({Key key, this.snapshot}) : super(key: key);

  // 生活指數(shù)封裝
  Widget _suggestionWidget(String content) =>
      Padding(padding: const EdgeInsets.only(top: 20.0), child: 
              Text(content, style: TextStyle(color: Colors.white, fontSize: 16.0)));

  @override
  Widget build(BuildContext context) {
    var _suggestion = snapshot.data.heWeather[0].suggestion;

    return Container(
      padding: const EdgeInsets.all(12.0),
      color: Colors.black54,
      alignment: Alignment.centerLeft,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('生活建議', style: TextStyle(fontSize: 24.0, color: Colors.white)),
          _suggestionWidget('舒適度:${_suggestion.comf.brf}\n${_suggestion.comf.txt}'),
          _suggestionWidget('洗車指數(shù):${_suggestion.cw.brf}\n${_suggestion.cw.txt}'),
          _suggestionWidget('運動指數(shù):
                                ${_suggestion.sport.brf}\n${_suggestion.sport.txt}'),
        ],
      ),
    );
  }
}

所有的分模塊都已經(jīng)編寫完成谈撒,剩下就是通過粘合劑進(jìn)行組裝了

child: StreamBuilder(
                    initialData: _bloc.weather,
                    stream: _bloc.weatherStream,
                    builder: (_, AsyncSnapshot<WeatherModel> snapshot) => !snapshot.hasData
                        ? CupertinoActivityIndicator(radius: 12.0)
                        : SafeArea(
                            child: RefreshIndicator(
                                child: CustomScrollView(
                                  physics: BouncingScrollPhysics(),
                                  slivers: <Widget>[
                                    SliverToBoxAdapter(child: FollowedHeader(snapshot: snapshot)),
                                    // 實時天氣
                                    SliverPadding(
                                      padding: const EdgeInsets.symmetric(vertical: 30.0),
                                      sliver: SliverToBoxAdapter(
                                        child: CurrentWeatherState(snapshot: snapshot, city: city),
                                      ),
                                    ),
                                    // 天氣預(yù)報
                                    WeatherForecast(snapshot: snapshot),
                                    // 空氣質(zhì)量
                                    SliverPadding(
                                      padding: const EdgeInsets.symmetric(vertical: 30.0),
                                      sliver: SliverToBoxAdapter(child: AirQuality(snapshot: snapshot)),
                                    ),
                                    // 生活建議
                                    SliverToBoxAdapter(child: LifeSuggestions(snapshot: snapshot))
                                  ],
                                ),
                                onRefresh: () async {
                                  _bloc.requestWeather(city).then((w) => _bloc.updateWeather(w));
                                  return null;
                                }),
                          )),

最后就剩下設(shè)置頁的全局主題切換了

設(shè)置頁全局主題切換

既然提到了數(shù)據(jù)的切換,那肯定就涉及 BLoC 毫無疑問了匾南,還是照常編寫管理類

/// 查看 `setting_bloc.dart` 文件
class SettingBloc extends BaseBloc {
  /// 所有主題色列表
  static const themeColors = [Colors.blue, Colors.red, Colors.green, 
                              Colors.deepOrange, Colors.pink, Colors.purple];

  Color _color = themeColors[0];

  Color get color => _color;

  BehaviorSubject<Color> _colorController = BehaviorSubject();

  Observable<Color> get colorStream => Observable(_colorController.stream);

  /// 切換主題通知刷新
  switchTheme(int themeIndex) {
    _color = themeColors[themeIndex];
    _colorController.add(_color);
  }

  @override
  void dispose() {
    _colorController?.close();
  }
}

因為是全局的切換啃匿,那么這個 BLoC 肯定需要在最頂層進(jìn)行注冊,這邊就不貼代碼了午衰,同 ProvinceBloc 一致立宜。接著編寫界面,設(shè)置界面因為有 GridView 和其他部件臊岸,所以也需要用 CustomScrollView 作為粘合劑橙数,當(dāng)然,你也可以用 Wrap 代替 GridView 來實現(xiàn)網(wǎng)格帅戒,就不需要用 CustomScrollView灯帮,使用 Column 即可。

class SettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _bloc = BlocProvider.of<SettingBloc>(context);

    return StreamBuilder(
        stream: _bloc.colorStream,
        initialData: _bloc.color,
        // Theme 是 Flutter 自帶的一個設(shè)置主題的部件逻住,里面可以設(shè)置多種顏色钟哥,
        // 通過接收到 color 的變化,改變主題色瞎访,其他頁面也如此設(shè)置腻贰,小伙伴可以自己添加
        builder: (_, AsyncSnapshot<Color> snapshot) => Theme(
            // IconThemeData 用于設(shè)置按鈕的主題色
              data: ThemeData(primarySwatch: snapshot.data, iconTheme: IconThemeData(color: snapshot.data)),
              child: Scaffold(
                appBar: AppBar(
                  title: Text('設(shè)置'),
                ),
                body: Container(
                  color: Colors.black12,
                  padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
                  child: CustomScrollView(
                    slivers: <Widget>[
                      SliverPadding(
                        padding: const EdgeInsets.only(right: 12.0),
                        sliver: SliverToBoxAdapter(
                            child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: <Widget>[
                            Text('當(dāng)前主題色:', style: TextStyle(fontSize: 16.0, 
                                                            color: snapshot.data)),
                            Container(width: 20.0, height: 20.0, color: snapshot.data)
                          ],
                        )),
                      ),
                      SliverPadding(padding: const EdgeInsets.symmetric(vertical: 15.0)),
                      SliverGrid(
                          delegate: SliverChildBuilderDelegate(
                              (_, index) => InkWell(
                                    child: Container(color: SettingBloc.themeColors[index]),
                                    onTap: () {
                                        // 選擇后進(jìn)行保存,當(dāng)下次進(jìn)入的時候直接使用該主題色
                                        // 同時切換主題色
                                      _bloc.switchTheme(index);
                                      PreferenceUtils.instance.saveInteger(PreferencesKey.THEME_COLOR_INDEX, index);
                                    },
                                  ),
                              childCount: SettingBloc.themeColors.length),
                          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 20.0, crossAxisSpacing: 20.0)),
                    ],
                  ),
                ),
              ),
            ));
  }
}

最終全局的主題切換也實現(xiàn)了扒秸。

編寫完代碼播演,需要打包啊,Android 下的打包大家肯定沒問題伴奥,這里講下 flutter 下如何打包 apk写烤,ipa 因為沒有 mac 所以你們懂的。

apk 文件打包
  1. 創(chuàng)建 jks 文件拾徙,如果已經(jīng)存在可忽略這步從第二步開始洲炊。打開終端并輸入

    keytool -genkey -v -keystore [你的簽名文件路徑].jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

    release1.png

    然后輸入密碼以及一些基本信息就可以創(chuàng)建成功了

  2. 在項目的 android 目錄下創(chuàng)建一個 key.properties 文件,里面進(jìn)行如下配置

    storePassword=<password from previous step>
    keyPassword=<password from previous step>
    keyAlias=key
    storeFile=<[你的簽名文件路徑].jks>
    
  3. android/app 下的 build.gradle 中進(jìn)行如下修改

    apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
        
    // 增加如下部分代碼
    def keystorePropertiesFile = rootProject.file("key.properties")
    def keystoreProperties = new Properties()
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    
    android {
        // ... 
        defaultConfigs{
            // ...
        }
        
        // 增加如下代碼
        signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']
            }
        }
        
        buildTypes{
            // ...
        }
    }
    
  4. 再次打開終端運行 flutter build apk 會自動生成一個 apk 文件尼啡,文件路徑為

    [你的項目地址]\build\app\outputs\apk\release

  5. 通過 flutter install 就可以將正式包運行到手機(jī)上

總結(jié)

2019.03.09 - 2019.04.08暂衡,一個月時間,花了整整一個月終于是寫完了崖瞭,也算是給一直關(guān)注的小伙伴們有個交代了古徒。對于寫系列文,說實話真的很煎熬读恃,一是因為前期需要做好整體的思路構(gòu)造隧膘,需要從簡入繁代态,一步步深入,如果整體思路沒有搭建好疹吃,那就會走向一條不歸路蹦疑,要么硬著頭皮上,要么退回來從頭開始萨驶,二是為了寫這套系列文歉摧,基本上舍棄了所有的私人時間,寫完了自己還得看上好幾遍才敢發(fā)布腔呜。但是收獲的的確很多叁温,期間看了很多 flutter 的源碼,對部件上的一些認(rèn)識和理解也會有所的加深核畴,也會去學(xué)習(xí)源碼的編寫規(guī)范膝但,對日后工作上也會有很大幫助。最后希望這套系列文能帶更多的小伙伴入門 Flutter谤草,大家下次再見咯~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跟束,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子丑孩,更是在濱河造成了極大的恐慌冀宴,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件温学,死亡現(xiàn)場離奇詭異略贮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)仗岖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門刨肃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人箩帚,你說我怎么就攤上這事』苹荆” “怎么了紧帕?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長桅打。 經(jīng)常有香客問我是嗜,道長,這世上最難降的妖魔是什么挺尾? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任鹅搪,我火速辦了婚禮,結(jié)果婚禮上遭铺,老公的妹妹穿的比我還像新娘丽柿。我一直安慰自己恢准,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布甫题。 她就那樣靜靜地躺著馁筐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坠非。 梳的紋絲不亂的頭發(fā)上敏沉,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音炎码,去河邊找鬼盟迟。 笑死,一個胖子當(dāng)著我的面吹牛潦闲,可吹牛的內(nèi)容都是我干的攒菠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矫钓,長吁一口氣:“原來是場噩夢啊……” “哼要尔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起新娜,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤赵辕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后概龄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體还惠,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年私杜,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚕键。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡衰粹,死狀恐怖锣光,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铝耻,我是刑警寧澤誊爹,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站瓢捉,受9級特大地震影響频丘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泡态,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一搂漠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧某弦,春花似錦桐汤、人聲如沸而克。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拍摇。三九已至,卻和暖如春馆截,著一層夾襖步出監(jiān)牢的瞬間充活,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工蜡娶, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留混卵,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓窖张,卻偏偏與公主長得像幕随,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宿接,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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

  • 一張揉皺的百元鈔票赘淮,其價值具備的能量是什么力量?兩個完全不同的級別睦霎,分別體現(xiàn)著每一個人的生命價值梢卸。如一棵大樹視為一...
    OrientalLion閱讀 450評論 0 0
  • 如果沒有萌寶,我的生命將會多么的干涸副女。 從小不喜歡挨著小孩的我蛤高,在有了萌寶后,一切都變了碑幅。我是那么地喜歡賴著他的身...
    菲凝_a523閱讀 114評論 1 0
  • 千里鳶飛影綽綽 好似情關(guān)難越 若堪悲意訴愁心 卻已是囚緣陌路戴陡,經(jīng)年而已。
    胡子啦茬先森閱讀 433評論 0 0