Flutter GetX使用---簡潔的魅力!

前言

使用Bloc的時候,有一個讓我至今為止十分在意的問題,無法真正的跨頁面交互!在反復的查閱官方文檔后,使用一個全局Bloc的方式,實現了“偽”跨頁面交互,詳細可查看:flutter_bloc使用解析;fish_redux的廣播機制是可以比較完美的實現跨頁面交互的,我也寫了一篇幾萬字介紹如何使用該框架:fish_redux使用詳解,redux思想劃分是比較細的,寫起來會很費勁;最近嘗試了GetX相關功能,解決了我的相當一部分痛點

把整篇文章寫完后,我馬上把自己的一個demo里面所有Bloc代碼全用GetX替換,且去掉了Fluro框架;感覺用Getx雖然會省掉大量的模板代碼,但還是有些重復工作:創建文件夾,創建幾個必備文件,寫那些必須要寫的初始化代碼和類;略微繁瑣,為了對得起GetX給我開發帶來的巨大便利,我就花了一些時間,給它寫了一個插件! 上面這重復的代碼,文件,文件夾統統能一鍵生成!

GetX相關優勢

  • 依賴注入
    • GetX是通過依賴注入的方式,存儲相應的XxxGetxController;已經脫離了InheritedWidget那一套玩法,自己手動去管理這些實例,使用場景被大大拓展
    • 簡單的思路,卻能產生深遠的影響:優雅的跨頁面功能便是基于這種設計而實現的、獲取實例無需BuildContext、GetBuilder自動化的處理及其減少了入參等等
  • 跨頁面交互
    • 這絕對是GetX的一個優點!對于復雜的生產環境,跨頁面交互的場景,實在太常見了,GetX的跨頁面交互,實現的也較為優雅
  • 路由管理
    • getx內部實現了路由管理,而且用起來,非常簡單!bloc沒實現路由管理,我不得不找一個star量高的路由框架,就選擇了fluro,但是不得不吐槽下,fluro用起來真的很折磨人,每次新建一個頁面,最讓我抗拒的就是去寫fluro路由代碼,橫跨幾個文件來回寫,頭皮發麻
    • GetX實現了動態路由傳參,也就是說直接在命名路由上拼參數,然后能拿到這些拼在路由上的參數,也就是說用flutter寫H5,直接能通過Url傳值,OMG!可以無腦舍棄復雜的fluro了
  • 實現了全局BuildContext
  • 國際化,主題實現

如果深度使用過Provider,Bloc這類依賴InheritedWidget建立起的狀態管理框架;再看看GetX內部實現思想,就能發現,他們已經是倆種體系的東西了

對此,我來拋出一些問題:InheritedWidget存在什么缺點?為什么其數據傳遞和路由設計思想對立?為什么getx使用依賴注入?getx的obx自動刷新黑魔法是個什么鬼?

下來將全面的介紹GetX的使用,文章也不分篇水閱讀量了,力求一文寫清楚,方便大家隨時查閱

準備

引入

  • 首先導入GetX的插件
# getx 狀態管理框架 https://pub.flutter-io.cn/packages/get
# 非空安全最后一個版本(flutter 2.0之前版本)
get: ^3.26.0
    
# 空安全版本 最新版本請查看  https://pub.flutter-io.cn/packages/get
get: ^4.3.8

GetX地址

主入口配置

  • 只需要將MaterialApp改成GetMaterialApp即可
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: CounterGetPage(),
    );
  }
}
  • 各模塊導包,均使用下面包即可
import 'package:get/get.dart';

插件

這個getx代碼生成插件,我花了不少精力去完善,功能已經比較齊全了,希望對大家有所幫助。

歡迎大家提issue,提issue之前,請務必認真查看文檔:GetX代碼生成IDEA插件,超詳細功能講解,確保想提的需求,在本插件里面未被實現;上次有個老哥給我連開三個issue,提的需求都是早已實現的功能。。。

說明

插件地址

插件的功能含義

  • Model:生成GetX的模式
    • Default:默認模式,生成三個文件:state,logic,view
    • Easy:簡單模式,生成倆個文件:logic,view
  • Module Name:模塊的名稱,請使用大駝峰或小駝峰命名
  • 插件詳細功能說明,請查閱:GetX代碼生成IDEA插件,超詳細功能講解

安裝

  • 在設置里面選擇:Plugins ---> 輸入“getx”搜索 ---> 選擇名字為:“GeX” ---> 然后安裝 ---> 最后記得點擊下“Apply”
image-20210927092128051

效果圖

  • 生成模板代碼彈窗
getx_new
  • 提供后綴名修改,也支持了持久化
image-20210926111944785
  • Alt + Enter : 可以選擇包裹Widget,有四種可選:GetBuilder、GetBuilder(Auto Dispose),Obx、GetX,大大方便開發喲(^U^)ノ~YO
    • 如果你發現某個頁面,你的GetXController無法回收,可以使用 GetBuilder(Auto Dispose)Wrap 你的 Widget
image-20210802160603092
image-20210802160631405
  • 快捷代碼片段提示:我自己寫了很多,也有一部分直接引用:getx-snippets-intelliJ
    • 輸入 getx 前綴便有提示
image-20210922111700625

計數器

效果圖

counter_getx

實現

首先,當然是實現一個簡單的計數器,來看GetX怎么將邏輯層和界面層解耦的

  • 來使用插件生成下簡單文件
    • 模式選擇:Easy
    • 功能選擇:useFolder
image-20210927092300651

來看下生成的默認代碼,默認代碼十分簡單,詳細解釋放在倆種狀態管理里

  • logic
import 'package:get/get.dart';

class CounterGetLogic extends GetxController {

}
  • view
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'logic.dart';

class CounterGetPage extends StatelessWidget {
  final logic = Get.put(CounterGetLogic());

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

簡單狀態管理

GetBuilder:這是一個極其輕巧的狀態管理器,占用資源極少!

  • logic:先來看看logic層
    • 因為是處理頁面邏輯的,加上Controller單詞過長,也防止和Flutter自帶的一些控件控制器弄混,所以該層用logic結尾,這里就定為了logic
    • 當然這點隨個人意向,寫Event,Controller均可(插件生成代碼,支持自定義通用后綴)
class CounterEasyLogic extends GetxController {
  var count = 0;

  void increase() {
    ++count;
    update();
  }
}
  • view
class CounterEasyPage extends StatelessWidget {
  final logic = Get.put(CounterEasyLogic());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('計數器-簡單式')),
      body: Center(
        child: GetBuilder<CounterEasyLogic>(builder: (logic) {
          return Text(
            '點擊了 ${logic.count} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • 分析下:GetBuilder這個方法
    • init:雖然上述代碼沒用到,但是,這個參數是存在在GetBuilder中的,因為在加載變量的時候就使用Get.put()生成了CounterEasyGetLogic對象,GetBuilder會自動查找該對象,所以,就可以不使用init參數
    • builder:方法參數,擁有一個入參,類型便是GetBuilder所傳入泛型的類型
    • initState,dispose等:GetBuilder擁有StatefulWidget所有周期回調,可以在相應回調內做一些操作

響應式狀態管理

當數據源變化時,將自動執行刷新組件的方法

  • logic層
    • 這里變量數值后寫.obs操作,是說明定義了該變量為響應式變量,當該變量數值變化時,頁面的刷新方法將自動刷新
    • 基礎類型,List,類都可以加.obs,使其變成響應式變量
class CounterRxLogic extends GetxController {
  var count = 0.obs;

  ///自增
  void increase() => ++count;
}
  • view層
class CounterRxPage extends StatelessWidget {
  final logic = Get.put(CounterRxLogic());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('計數器-響應式')),
      body: Center(
        child: Obx(() {
          return Text(
            '點擊了 ${logic.count.value} 次',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • 可以發現刷新組件的方法極其簡單:Obx(),這樣可以愉快的到處寫定點刷新操作了

  • Obx()方法刷新的條件

    • 只有當響應式變量的值發生變化時,才會會執行刷新操作,當某個變量初始值為:“test”,再賦值為:“test”,并不會執行刷新操作
    • 當你定義了一個響應式變量,該響應式變量改變時,包裹該響應式變量的Obx()方法才會執行刷新操作,其它的未包裹該響應式變量的Obx()方法并不會執行刷新操作,Cool!
  • 來看下如果把整個類對象設置成響應類型,如何實現更新操作呢?

    • 下面解釋來自官方README文檔
    • 這里嘗試了下,將整個類對象設置為響應類型,當你改變了類其中一個變量,然后執行更新操作,只要包裹了該響應類變量的Obx(),都會實行刷新操作,將整個類設置響應類型,需要結合實際場景使用
// model
// 我們將使整個類成為可觀察的,而不是每個屬性。
class User{
    User({this.name = '', this.age = 0});
    String name;
    int age;
}

// controller
final user = User().obs;
//當你需要更新user變量時。
user.update( (user) { // 這個參數是你要更新的類本身。
    user.name = 'Jonny';
    user.age = 18;
});
// 更新user變量的另一種方式。
user(User(name: 'Jo?o', age: 35));

// view
Obx(()=> Text("Name ${user.value.name}: Age: ${user.value.age}"));
// 你也可以不使用.value來訪問模型值。
user().name; // 注意是user變量,而不是類變量(首字母是小寫的)。

總結

分析

  • Obx是配合Rx響應式變量使用、GetBuilder是配合update使用:請注意,這完全是倆套定點刷新控件的方案
    • 區別:前者響應式變量變化,Obx自動刷新;后者需要使用update手動調用刷新
  • 每一個響應式變量,都需要生成對應的GetStream,占用資源大于基本數據類型,會對內存造成一定壓力
  • GetBuilder內部實際上是對StatefulWidget的封裝,所以占用資源極小

使用場景

  • 一般來說,對于大多數場景都是可以使用響應式變量的
  • 但是,在一個包含了大量對象的List,都使用響應式變量,將生成大量的GetStream,必將對內存造成較大的壓力,該情況下,就要考慮使用簡單狀態管理了
  • 總的來說:推薦GetBuilder和update配合的寫法
    • GetBuilder內置回收GetxController的功能,能避免一些無法自動回收GetxController的坑爹問題
      • 使用GetBuilder的自動回收:GetBuilder需要設置assignId: true;或使用插件一鍵Wrap Widget:GetBuilder(Auto Dispose)
    • 使用Obx,相關變量定義初始化以及實體更新和常規寫法不同,會對初次接觸該框架的人,造成很大的困擾
    • getx的IDEA插件現已支持一鍵Wrap Widget生成GetBuilder,可以一定程度上提升你的開發效率

跨頁面交互

跨頁面交互,在復雜的場景中,是非常重要的功能,來看看GetX怎么實現跨頁面事件交互的

效果圖

  • 體驗一下
  • Cool,這才是真正的跨頁面交互!下級頁面能隨意調用上級頁面事件,且關閉頁面后,下次重進,數據也很自然重置了(全局Bloc不會重置,需要手動重置)
jump_getx

實現

頁面一

常規代碼

  • logic
    • 這里的自增事件,是供其它頁面調用的,該頁面本身沒使用
class GetJumpOneLogic extends GetxController {
  var count = 0;

  ///跳轉到跨頁面
  void toJumpTwo() {
    Get.toNamed(RouteConfig.getJumpTwo, arguments: {'msg': '我是上個頁面傳遞過來的數據'});
  }

  ///跳轉到跨頁面
  void increase() {
    count = ++count;
    update();
  }
}
  • view
    • 此處就一個顯示文字和跳轉功能
class GetJumpOnePage extends StatelessWidget {
  /// 使用Get.put()實例化你的類,使其對當下的所有子路由可用。
  final logic = Get.put(GetJumpOneLogic());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text('跨頁面-One')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.toJumpTwo(),
        child: const Icon(Icons.arrow_forward_outlined),
      ),
      body: Center(
        child: GetBuilder<GetJumpOneLogic>(
          builder: (logic) {
            return Text('跨頁面-Two點擊了 ${logic.count} 次',
                style: TextStyle(fontSize: 30.0));
          },
        ),
      ),
    );
  }
}

頁面二

這個頁面就是重點了

  • logic
    • 將演示怎么調用前一個頁面的事件
    • 怎么接收上個頁面數據
    • 請注意,GetxController包含比較完整的生命周期回調,可以在onInit()接受傳遞的數據;如果接收的數據需要刷新到界面上,請在onReady回調里面接收數據操作,onReady是在addPostFrameCallback回調中調用,刷新數據的操作在onReady進行,能保證界面是初始加載完畢后才進行頁面刷新操作的
class GetJumpTwoLogic extends GetxController {
  var count = 0;
  var msg = '';

  @override
  void onReady() {
    var map = Get.arguments;
    msg = map['msg'];
    update();

    super.onReady();
  }

  ///跳轉到跨頁面
  void increase() {
    count = ++count;
    update();
  }
}
  • view
    • 加號的點擊事件,點擊時,能實現倆個頁面數據的變換
    • 重點來了,這里通過Get.find(),獲取到了之前實例化GetXController,獲取某個模塊的GetXController后就很好做了,可以通過這個GetXController去調用相應的事件,也可以通過它,拿到該模塊的數據!
class GetJumpTwoPage extends StatelessWidget {
  final oneLogic = Get.find<GetJumpOneLogic>();
  final twoLogic = Get.put(GetJumpTwoLogic());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text('跨頁面-Two')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          oneLogic.increase();
          twoLogic.increase();
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Column(mainAxisSize: MainAxisSize.min, children: [
          //計數顯示
          GetBuilder<GetJumpTwoLogic>(
            builder: (logic) {
              return Text('跨頁面-Two點擊了 ${twoLogic.count} 次',
                  style: TextStyle(fontSize: 30.0));
            },
          ),

          //傳遞數據
          GetBuilder<GetJumpTwoLogic>(
            builder: (logic) {
              return Text('傳遞的數據:${twoLogic.msg}',
                  style: TextStyle(fontSize: 30.0));
            },
          ),
        ]),
      ),
    );
  }
}

總結

GetX這種的跨頁面交互事件,真的是非常簡單了,侵入性也非常的低,不需要在主入口配置什么,在復雜的業務場景下,這樣簡單的跨頁面交互方式,就能實現很多事了

進階吧!計數器

我們可能會遇到過很多復雜的業務場景,在復雜的業務場景下,單單某個模塊關于變量的初始化操作可能就非常多,在這個時候,如果還將state(狀態層)和logic(邏輯層)寫在一起,維護起來可能看的比較暈

這里將狀態層和邏輯層進行一個拆分,這樣在稍微大一點的項目里使用GetX,也能保證結構足夠清晰了!

在這里就繼續用計數器舉例吧!

實現

此處需要劃分三個結構了:state(狀態層),logic(邏輯層),view(界面層)

  • 這里使用插件生成下模板代碼
    • Model:選擇Default(默認)
    • Function:useFolder(默認選中)
image-20211127151801015

來看下生成的模板代碼

  • state
class GetCounterHighState {
  GetCounterHighState() {
    ///Initialize variables
  }
}
  • logic
import 'package:get/get.dart';

import 'state.dart';

class GetCounterHighLogic extends GetxController {
  final GetCounterHighState state = GetCounterHighState();
}
  • view
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'logic.dart';

class GetCounterHighPage extends StatelessWidget {
  final logic = Get.put(GetCounterHighLogic());
  final state = Get.find<GetCounterHighLogic>().state;

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

為什么寫成這樣三個模塊,需要把State單獨提出來,請速速瀏覽下方

改造

  • state
    • 這里使用劃分出來的state層,來統一管理所有的狀態變量
    • 涉及到狀態變量定義和Logic層徹底分開
class GetCounterHighState {
  late int count;

  GetCounterHighState() {
    count = 0;
  }
}
  • logic
    • 邏輯層就比較簡單,需要注意的是:開始時需要實例化狀態類
class GetCounterHighLogic extends GetxController {
  final GetCounterHighState state = GetCounterHighState();

  ///自增
  void increase() {
    state.count = ++state.count;
    update();
  }
}
  • view
    • 實際上view層,和之前的幾乎沒區別,區別的是把狀態層給獨立出來了
    • 因為CounterHighGetLogic被實例化,所以直接使用Get.find<CounterHighGetLogic>()就能拿到剛剛實例化的邏輯層,然后拿到state,使用單獨的變量接收下
    • ok,此時:logic只專注于觸發事件交互,state只專注數據
class GetCounterHighPage extends StatelessWidget {
  final logic = Get.put(GetCounterHighLogic());
  final state = Get.find<GetCounterHighLogic>().state;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('計數器-進階版')),
      body: Center(
        child: GetBuilder<GetCounterHighLogic>(
          builder: (logic) {
            return Text(
              '點擊了 ${state.count} 次',
              style: TextStyle(fontSize: 30.0),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: Icon(Icons.add),
      ),
    );
  }
}

對比

看了上面的改造,屏幕前的你可能想吐槽了:坑比啊,之前簡簡單單的邏輯層,被拆成倆個,還搞得這么麻煩,你是猴子請來的逗比嗎?

大家先別急著吐槽,當業務過于復雜,state層,也是會維護很多東西的,讓我們看看下面的一個小栗子,下面實例代碼是不能直接運行的,想看詳細運行代碼,請查看項目:flutter_use

  • state
class MainState {
  ///選擇index
  late int selectedIndex;

  ///控制是否展開
  late bool isUnfold;

  ///是否縮放
  late bool isScale;

  ///分類按鈕數據源
  late List<BtnInfo> list;

  ///Navigation的item信息
  late List<BtnInfo> itemList;

  ///PageView頁面
  late List<Widget> pageList;
  late PageController pageController;

  MainState() {
    //初始化index
    selectedIndex = 0;
    //默認不展開
    isUnfold = false;
    //默認不縮放
    isScale = false;
    //PageView頁面
    pageList = [
      KeepAlivePage(FunctionPage()),
      KeepAlivePage(ExamplePage()),
      KeepAlivePage(SettingPage()),
    ];
    //item欄目
    itemList = [
      BtnInfo(
        title: "功能",
        icon: Icon(Icons.bubble_chart),
      ),
      BtnInfo(
        title: "范例",
        icon: Icon(Icons.opacity),
      ),
      BtnInfo(
        title: "設置",
        icon: Icon(Icons.settings),
      ),
    ];
    //頁面控制器
    pageController = PageController();
  }
}
  • logic
class MainLogic extends GetxController {
  final state = MainState();

  @override
  void onInit() {
    ///初始化應用信息
    InitConfig.initApp(Get.context);
    super.onInit();
  }

  ///切換tab
  void switchTap(int index) {
    state.selectedIndex = index;
    state.pageController.jumpToPage(index);
    update();
  }

  ///是否展開側邊欄
  void onUnfold(bool isUnfold) {
    state.isUnfold = !state.isUnfold;
    update();
  }

  ///是否縮放
  void onScale(bool isScale) {
    state.isScale = !state.isScale;
    update();

    initWindow(scale: isScale ? 1.25 : 1.0);
  }
}
  • view
class MainPage extends StatelessWidget {
  final MainLogic logic = Get.put(MainLogic());
  final MainState state = Get.find<MainLogic>().state;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Row(children: [
        ///側邊欄區域
        GetBuilder<MainLogic>(
          builder: (logic) {
            return SideNavigation(
              selectedIndex: state.selectedIndex,
              isUnfold: state.isUnfold,
              isScale: state.isScale,
              sideItems: state.itemList,
              //點擊item
              onItem: (index) => logic.switchTap(index),
              //展開側邊欄
              onUnfold: (isUnfold) => logic.onUnfold(isUnfold),
              //縮放整體布局
              onScale: (isScale) => logic.onScale(isScale),
            );
          },
        ),

        ///Expanded占滿剩下的空間
        Expanded(
          child: PageView.builder(
            physics: NeverScrollableScrollPhysics(),
            itemCount: state.pageList.length,
            itemBuilder: (context, index) => state.pageList[index],
            controller: state.pageController,
          ),
        )
      ]),
    );
  }
}

從上面可以看出,state層里面的狀態已經較多了,當某些模塊涉及到大量的:提交表單數據,跳轉數據,展示數據等等,state層的代碼會相當的多,相信我,真的是非常多,一旦業務發生變更,還要經常維護修改,就蛋筒了

在復雜的業務下,將狀態層(state)和業務邏輯層(logic)分開,絕對是個明智的舉動

最后

  • 該模塊的效果圖就不放了,和上面計數器效果一模一樣,想體驗一下,可點擊:體驗一下
  • 簡單的業務模塊,可以使用倆層結構:logic,view;復雜的業務模塊,推薦使用三層結構:state,logic,view

Binding的使用

說明

大家可能發現了,插件上增加了addBinding的功能

image-20210927093135423

再加上getx的demo也用了binding,想必各位靚仔就非常想使用這個功能

這個功能實際的作用非常簡單

  • 統一管理單模塊使用的GetXController
  • binding模塊需要在getx路由頁面進行綁定;進入頁面的時候,統一懶注入binding模塊的GetXController

這樣做當然有好處

  • 可以統一管理復雜模塊的多個GetXController

請注意

  • 不建議在Get.to()方法里面進行binding綁定
    • 如果存在多個頁面跳轉到存在binding頁面,你的每個Get.to()方法都需要綁定
    • 這樣極其容易出bug,對后面接盤的人,十分不友好
  • 使用binding,你理應使用getx的命名路由

鄭重申明:不使用binding,并不會對功能有任何的影響

使用

首先必須搭建好路由模塊

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: RouteConfig.testOne,
      getPages: RouteConfig.getPages,
    );
  }
}

class RouteConfig {
  static const String testOne = "/testOne";
  static const String testTwo = "/testOne/testTwo";

  static final List<GetPage> getPages = [
    GetPage(
      name: testOne,
      page: () => TestOnePage(),
      binding: TestOneBinding(),
    ),
    GetPage(
      name: testTwo,
      page: () => TestTwoPage(),
      binding: TestTwoBinding(),
    ),
  ];
}

創建頁面模塊

  • 選中addBinding功能
image-20210927093312584
  • 創建TestOne
///logic層
class TestOneLogic extends GetxController {
  void jump() => Get.toNamed(RouteConfig.testTwo);
}

///view層
class TestOnePage extends StatelessWidget {
  final logic = Get.find<TestOneLogic>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('頁面一')),
      body: Center(child: Text('頁面一', style: TextStyle(fontSize: 30.0))),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.jump(),
        child: Icon(Icons.arrow_forward),
      ),
    );
  }
}

///binding層
class TestOneBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => TestOneLogic());
  }
}
  • TestTwo
///logic層
class TestTwoLogic extends GetxController {

}

///view層
class TestTwoPage extends StatelessWidget {
  final logic = Get.find<TestTwoLogic>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('頁面二')),
      body: Center(child: Text('頁面二', style: TextStyle(fontSize: 30.0))),
    );
  }
}

///binding層
class TestTwoBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => TestTwoLogic());
  }
}

總結

這邊我寫了一個極其簡單的范例,僅僅是個跳轉頁面的功能,我覺得,應該可以展示binding的功能了

  • 就是統一管理某個模塊需要注入的多個GetXController
  • 請注意,該注入是懶注入,只有使用了 find + 對應的泛型,才會被真正的注入的getx的全局map實例里

實際上,手動寫binding文件,還是有點麻煩,寫了binding,view層的使用也需要做相應的變動

鐵汁們,為了幫你們節省點開發時間,這點浪費你們生命且沒什么技術含量的事情,已經在插件里幫你完成

  • 有需要的,選中addBinding功能即可

  • GetPage里面綁定binding的操作,只能麻煩你們自己動下手了,項目結構千變萬化,這玩意沒法定位

路由管理

GetX實現了一套用起來十分簡單的路由管理,可以使用一種極其簡單的方式導航,也可以使用命名路由導航

關于簡單路由和命名路由的區別

  • 簡單路由:十分簡單,看下下面的例子
Get.to(SomePage());
  • 命名路由
    • 在web上,可以直接通過命名的url直接導航頁面
    • 實現路由攔截的操作,舉一個官方文檔的例子:很輕松的實現了一個未登錄,跳轉登錄頁面功能
GetStorage box = GetStorage();

GetMaterialApp(
  getPages: [
    GetPage(name: '/', page:(){
      return box.hasData('token') ? Home() : Login();
    })
  ]
)

簡單路由

  • 主入口配置
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: MainPage(),
    );
  }
}
  • 路由的相關使用
    • 使用是非常簡單,使用Get.to()之類api即可,此處簡單演示,詳細api說明,放在本節結尾
//跳轉新頁面
Get.to(SomePage());

命名路由導航

這里是推薦使用命名路由導航的方式

  • 統一管理起了所有頁面
  • 在app中可能感受不到,但是在web端,加載頁面的url地址就是命名路由你所設置字符串,也就是說,在web中,可以直接通過url導航到相關頁面

下面說明下,如何使用

  • 首先,在主入口出配置下
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialRoute: RouteConfig.main,
      getPages: RouteConfig.getPages,
    );
  }
}
  • RouteConfig類
    • 下面是我的相關頁面,和其映射的頁面,請根據自己的頁面進行相關編寫
class RouteConfig {
  ///主頁面
  static const String main = "/";

  ///演示SmartDialog控件 喜馬拉雅  dialog頁面
  static const String smartDialog = "/smartDialog";
  static const String himalaya = "/himalaya";
  static const String dialog = "/dialog";

  ///bloc計數器模塊 Bloc跨頁面傳遞事件
  static const String blCubitCounterPage = "/blCubitCounterPage";
  static const String blBlocCounterPage = "/blBlocCounterPage";
  static const String cubitSpanOne = "/cubitSpanOne";
  static const String cubitSpanTwo = "/cubitSpanOne/cubitSpanTwo";
  static const String streamPage = "/streamPage";
  static const String blCustomBuilderPage = "/blCustomBuilderPage";
  static const String counterEasyCPage = "/counterEasyCPage";

  ///測試布局頁面
  static const String testLayout = "/testLayout";

  ///GetX 計數器  跨頁面交互
  static const String getCounterRx = "/getCounterRx";
  static const String getCounterEasy = "/counterEasyGet";
  static const String getCounterHigh = "/counterHighGet";
  static const String getJumpOne = "/jumpOne";
  static const String getJumpTwo = "/jumpOne/jumpTwo";
  static const String getCounterBinding = "/getCounterBinding";
  static const String counterEasyXBuilderPage = "/counterEasyXBuilder";
  static const String counterEasyXEbxPage = "/counterEasyXEbx";

  ///Provider
  static const String proEasyCounterPage = "/proEasyCounterPage";
  static const String proHighCounterPage = "/proHighCounterPage";
  static const String proSpanOnePage = "/proSpanOnePage";
  static const String proSpanTwoPage = "/proSpanOnePage/proSpanTwoPage";
  static const String testNotifierPage = "/testNotifierPage";
  static const String customBuilderPage = "/customBuilderPage";
  static const String counterEasyPPage = "/counterEasyPPage";
  static const String counterGlobalEasyPPage = "/counterGlobalEasyPPage";

  ///別名映射頁面
  static final List<GetPage> getPages = [
    GetPage(name: main, page: () => MainPage()),
    GetPage(name: dialog, page: () => DialogPage()),
    GetPage(name: blCubitCounterPage, page: () => BlCubitCounterPage()),
    GetPage(name: blBlocCounterPage, page: () => BlBlocCounterPage()),
    GetPage(name: streamPage, page: () => StreamPage()),
    GetPage(name: blCustomBuilderPage, page: () => BlCustomBuilderPage()),
    GetPage(name: counterEasyCPage, page: () => CounterEasyCPage()),
    GetPage(name: testLayout, page: () => TestLayoutPage()),
    GetPage(name: smartDialog, page: () => SmartDialogPage()),
    GetPage(name: cubitSpanOne, page: () => CubitSpanOnePage()),
    GetPage(name: cubitSpanTwo, page: () => CubitSpanTwoPage()),
    GetPage(name: getCounterRx, page: () => GetCounterRxPage()),
    GetPage(name: getCounterEasy, page: () => GetCounterEasyPage()),
    GetPage(name: getCounterHigh, page: () => GetCounterHighPage()),
    GetPage(name: getJumpOne, page: () => GetJumpOnePage()),
    GetPage(name: getJumpTwo, page: () => GetJumpTwoPage()),
    GetPage(
      name: getCounterBinding,
      page: () => GetCounterBindingPage(),
      binding: GetCounterBinding(),
    ),
    GetPage(name: counterEasyXBuilderPage, page: () => EasyXCounterPage()),
    GetPage(name: counterEasyXEbxPage, page: () => EasyXEbxCounterPage()),
    GetPage(name: himalaya, page: () => HimalayaPage()),
    GetPage(name: proEasyCounterPage, page: () => ProEasyCounterPage()),
    GetPage(name: proHighCounterPage, page: () => ProHighCounterPage()),
    GetPage(name: proSpanOnePage, page: () => ProSpanOnePage()),
    GetPage(name: proSpanTwoPage, page: () => ProSpanTwoPage()),
    GetPage(name: testNotifierPage, page: () => TestNotifierPage()),
    GetPage(name: customBuilderPage, page: () => CustomBuilderPage()),
    GetPage(name: counterEasyPPage, page: () => CounterEasyPPage()),
    GetPage(name: counterGlobalEasyPPage, page: () => CounterGlobalEasyPPage()),
  ];
}

路由API

請注意命名路由,只需要在api結尾加上Named即可,舉例:

  • 默認:Get.to(SomePage());
  • 命名路由:Get.toNamed(“/somePage”);

詳細Api介紹,下面內容來自GetX的README文檔,進行了相關整理

  • 導航到新的頁面
Get.to(NextScreen());
Get.toNamed("/NextScreen");
  • 關閉SnackBars、Dialogs、BottomSheets或任何你通常會用Navigator.pop(context)關閉的東西
Get.back();
  • 進入下一個頁面,但沒有返回上一個頁面的選項(用于SplashScreens,登錄頁面等)
Get.off(NextScreen());
Get.offNamed("/NextScreen");
  • 進入下一個界面并取消之前的所有路由(在購物車、投票和測試中很有用)
Get.offAll(NextScreen());
Get.offAllNamed("/NextScreen");
  • 發送數據到其它頁面

只要發送你想要的參數即可。Get在這里接受任何東西,無論是一個字符串,一個Map,一個List,甚至一個類的實例。

Get.to(NextScreen(), arguments: 'Get is the best');
Get.toNamed("/NextScreen", arguments: 'Get is the best');

在你的類或控制器上:

print(Get.arguments);
//print out: Get is the best
  • 要導航到下一條路由,并在返回后立即接收或更新數據
var data = await Get.to(Payment());
var data = await Get.toNamed("/payment");
  • 在另一個頁面上,發送前一個路由的數據
Get.back(result: 'success');
// 并使用它,例:
if(data == 'success') madeAnything();
  • 如果你不想使用GetX語法,只要把 Navigator(大寫)改成 navigator(小寫),你就可以擁有標準導航的所有功能,而不需要使用context,例如:
// 默認的Flutter導航
Navigator.of(context).push(
  context,
  MaterialPageRoute(
    builder: (BuildContext context) {
      return HomePage();
    },
  ),
);

// 使用Flutter語法獲得,而不需要context。
navigator.push(
  MaterialPageRoute(
    builder: (_) {
      return HomePage();
    },
  ),
);

// get語法
Get.to(HomePage());

動態網頁鏈接

  • 這是一個非常重要的功能,在web端,可以保證通過url傳參數到頁面

Get提供高級動態URL,就像在Web上一樣。Web開發者可能已經在Flutter上想要這個功能了,Get也解決了這個問題。

Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo");

在你的controller/bloc/stateful/stateless類上:

print(Get.parameters['id']);
// out: 354
print(Get.parameters['name']);
// out: Enzo

你也可以用Get輕松接收NamedParameters。

void main() {
  runApp(
    GetMaterialApp(
      initialRoute: '/',
      getPages: [
      GetPage(
        name: '/',
        page: () => MyHomePage(),
      ),
      GetPage(
        name: '/profile/',
        page: () => MyProfile(),
      ),
       //你可以為有參數的路由定義一個不同的頁面,也可以為沒有參數的路由定義一個不同的頁面,但是你必須在不接收參數的路由上使用斜杠"/",就像上面說的那樣。
       GetPage(
        name: '/profile/:user',
        page: () => UserProfile(),
      ),
      GetPage(
        name: '/third',
        page: () => Third(),
        transition: Transition.cupertino  
      ),
     ],
    )
  );
}

發送命名路由數據

Get.toNamed("/profile/34954");

在第二個頁面上,通過參數獲取數據

print(Get.parameters['user']);
// out: 34954

現在,你需要做的就是使用Get.toNamed()來導航你的命名路由,不需要任何context(你可以直接從你的BLoC或Controller類中調用你的路由),當你的應用程序被編譯到web時,你的路由將出現在URL中。

資源釋放

關于GetxController的資源釋放,這個欄目的內容相當重要!

資源未釋放的場景

在我們使用GetX的時候,可能沒什么GetxController未被釋放的感覺,這種情況,是因為我們一般都是用了getx的那一套路由跳轉api(Get.to、Get.toName...)之類:使用Get.toName,肯定需要使用GetPage;如果使用Get.to,是不需要在GetPage中注冊的,Get.to的內部有一個添加到GetPageRoute的操作

通過上面會在GetPage注冊可知,說明在我們跳轉頁面的時候,GetX會拿你到頁面信息存儲起來,加以管理,下面倆種場景會導致GetxController無法釋放

  • GetxController可被自動釋放的條件

    • GetPage+Get.toName配套使用,可釋放
    • 直接使用Get.to,可釋放
  • GetxController無法被自動釋放場景

    • 未使用GetX提供的路由跳轉:直接使用原生路由api的跳轉操作
    • 這樣會直接導致GetX無法感知對應頁面GetxController的生命周期,會導致其無法釋放
Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => XxxxPage()),
);

由此,可從上面推導,GetxController無法被釋放的場景

  • 不使用GetX路由
  • PageView,TabView等子頁面
  • 使用GetX封裝的復雜組件

解決方案

這邊我模擬了上面場景,寫了一個解決方案

  • 第一個頁面跳轉
Navigator.push(
    Get.context,
    MaterialPageRoute(builder: (context) => AutoDisposePage()),
);
  • 演示頁面
    • 這地方地方必須要使用StatefulWidget,因為在這種情況,無法感知生命周期,就需要使用StatefulWidget生命周期
    • 在dispose回調處,把當前GetxController從整個GetxController管理鏈中刪除即可
class AutoDisposePage extends StatefulWidget {
  @override
  _AutoDisposePageState createState() => _AutoDisposePageState();
}

class _AutoDisposePageState extends State<AutoDisposePage> {
  final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      appBar: AppBar(title: const Text('計數器-自動釋放')),
      body: Center(
        child: Obx(
          () => Text('點擊了 ${logic.count.value} 次',
              style: TextStyle(fontSize: 30.0)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: const Icon(Icons.add),
      ),
    );
  }

  @override
  void dispose() {
    Get.delete<AutoDisposeLogic>();
    super.dispose();
  }
}

class AutoDisposeLogic extends GetxController {
  var count = 0.obs;

  ///自增
  void increase() => ++count;
}

看到這,你可能會想,啊這!怎么這么麻煩,我怎么還要寫StatefulWidget,好麻煩!

各位放心,這個問題,我也想到了,我特地在插件里面加上了自動回收的功能

  • 如果你寫的頁面無法被回收,記得勾選autoDispose
    • 怎么判斷頁面的GetxController是否能被回收呢?實際上很簡單,上面的未被釋放的場景已經描述的比較清楚了,不清楚的話,就再看看
image-20210922112126153

來看下代碼,default模式一樣可以的

  • view
class AutoDisposePage extends StatefulWidget {
  @override
  _AutoDisposePageState createState() => _AutoDisposePageState();
}

class _AutoDisposePageState extends State<AutoDisposePage> {
  final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());

  @override
    Widget build(BuildContext context) {
      return Container();
    }

  @override
  void dispose() {
    Get.delete<AutoDisposeLogic>();
    super.dispose();
  }
}
  • logic
class AutoDisposeLogic extends GetxController {

}

優化解決方案

上面的是個通用解決方法,你不需要額外的引入任何其它的東西;但是這種方案用到了StatefulWidget,代碼多了一大坨,讓我有點膈應

鄙人有著相當的強迫癥,想了很久

  • 本來是想GetBuilder寫個回收邏輯,然后提個PR給作者

    • 發現getx框架已經做了這樣的處理,但是,需要配套一個參數開啟使用
    • 在GetBuilder里面寫了回收邏輯:對Obx刷新模塊無法起效,Obx刷新控件內部無法定位到GetXController,所以無法做回收操作
  • 那只能從外部入手,我就寫了一個通用控件,來對相應的GetXController進行回收

    • 這個通用控件,我也給getx提了PR,一直在審核
    • 就算這個控件的PR通過了,集成到getx中,getx低版本也無法使用,沒轍
    • 這邊我給出這個通用回收控件代碼,各位可以自行復制到項目中使用

GetBindWidget

  • 該控件可以回收單個GetXController(bind參數),可以加上對應tag(tag參數);也可以回收多個GetXController(binds),可以加上多個tag(tags參數,請和binds 一 一 對應;無tag的GetXController的,tag可以寫成空字符:"")
import 'package:flutter/material.dart';
import 'package:get/get.dart';

/// GetBindWidget can bind GetxController, and when the page is disposed,
/// it can automatically destroy the bound related GetXController
///
///
/// Sample:
///
/// class SampleController extends GetxController {
///   final String title = 'My Awesome View';
/// }
///
/// class SamplePage extends StatelessWidget {
///   final controller = SampleController();
///
///   @override
///   Widget build(BuildContext context) {
///     return GetBindWidget(
///       bind: controller,
///       child: Container(),
///     );
///   }
/// }
class GetBindWidget extends StatefulWidget {
  const GetBindWidget({
    Key? key,
    this.bind,
    this.tag,
    this.binds,
    this.tags,
    required this.child,
  })  : assert(
          binds == null || tags == null || binds.length == tags.length,
          'The binds and tags arrays length should be equal\n'
          'and the elements in the two arrays correspond one-to-one',
        ),
        super(key: key);

  final GetxController? bind;
  final String? tag;

  final List<GetxController>? binds;
  final List<String>? tags;

  final Widget child;

  @override
  _GetBindWidgetState createState() => _GetBindWidgetState();
}

class _GetBindWidgetState extends State<GetBindWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() {
    _closeGetXController();
    _closeGetXControllers();

    super.dispose();
  }

  ///Close GetxController bound to the current page
  void _closeGetXController() {
    if (widget.bind == null) {
      return;
    }

    var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
    GetInstance().delete(key: key);
  }

  ///Batch close GetxController bound to the current page
  void _closeGetXControllers() {
    if (widget.binds == null) {
      return;
    }

    for (var i = 0; i < widget.binds!.length; i++) {
      var type = widget.binds![i].runtimeType.toString();

      if (widget.tags == null) {
        GetInstance().delete(key: type);
      } else {
        var key = type + (widget.tags?[i] ?? '');
        GetInstance().delete(key: key);
      }
    }
  }
}
  • 使用非常的簡單
/// 回收單個GetXController
class TestPage extends StatelessWidget {
  final logic = Get.put(TestLogic());

  @override
  Widget build(BuildContext context) {
    return GetBindWidget(
      bind: logic,
      child: Container(),
    );
  }
}

/// 回收多個GetXController
class TestPage extends StatelessWidget {
  final logicOne = Get.put(TestLogic(), tag: 'one');
  final logicTwo = Get.put(TestLogic());
  final logicThree = Get.put(TestLogic(), tag: 'three');

  @override
  Widget build(BuildContext context) {
    return GetBindWidget(
      binds: [logicOne, logicTwo, logicThree],
      tags: ['one', '', 'three'],
      child: Container(),
    );
  }
}

/// 回收日志
[GETX] Instance "TestLogic" has been created with tag "one"
[GETX] Instance "TestLogic" with tag "one" has been initialized
[GETX] Instance "TestLogic" has been created
[GETX] Instance "TestLogic" has been initialized
[GETX] Instance "TestLogic" has been created with tag "three"
[GETX] Instance "TestLogic" with tag "three" has been initialized
[GETX] "TestLogicone" onDelete() called
[GETX] "TestLogicone" deleted from memory
[GETX] "TestLogic" onDelete() called
[GETX] "TestLogic" deleted from memory
[GETX] "TestLogicthree" onDelete() called
[GETX] "TestLogicthree" deleted from memory

一些問題匯總

如果使用中,有比較坑的問題,希望大家在評論里提出來,我會在這個欄目匯總一下

  1. 無法跳轉重復頁面
  • 另一種表現形式:使用Get.to(Get.toName)在系統Dialog上跳轉頁面,未關閉Dialog;返回,再跳轉,會出現無法跳轉的情況

debug了下to方法內部的運行,發現他用了一個preventDuplicates參數,限制跳轉重復頁面

  • 為什么這樣做?
    • 優點:能解決多次點擊跳轉按鈕,跳轉多個重復頁面的問題
    • 缺點:限制了復雜業務跳轉重復頁面的場景

當然上面的缺點也不算是缺點,畢竟已經給了參數可以控制

  • 跳轉重復頁面,可以這樣寫
Get.to(XxxxPage(), preventDuplicates: false);
// 或者
Get.toNamed('xxx',  preventDuplicates: false);
  1. 使用PageView時,所有PageView頁面控制器,全被初始化問題

大家使用PageView,添加PageView頁面,PageView頁面用GetX構成,會發現所有的PageView頁面控制器全被初始化了!并不是切換到某個頁面時,對應頁面的控制器才被初始化!

PageView切換到某個頁面的時候,才會調用對應Page頁面的build方法;對于PageView頁面,控制器的注入過程,不能寫在類中了,需要將其移入到build方法中初始化。

  • 正常頁面,注入寫法(非PageView頁面)
class CounterEasyGetPage extends StatelessWidget {
  final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('計數器-簡單式')),
      body: Center(
        child: GetBuilder<CounterEasyGetLogic>(
          builder: (logicGet) => Text(
            '點擊了 ${logicGet.count} 次',
            style: TextStyle(fontSize: 30.0),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • PageView頁面,初始化位置必須調整
class CounterEasyGetPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic());
  
    return Scaffold(
      appBar: AppBar(title: const Text('計數器-簡單式')),
      body: Center(
        child: GetBuilder<CounterEasyGetLogic>(
          builder: (logicGet) => Text(
            '點擊了 ${logicGet.count} 次',
            style: TextStyle(fontSize: 30.0),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • 大家如果覺得手動移太麻煩的話,也可以選中插件的 isPageView 功能
image-20210927093414647

最后

相關地址

系列文章

引流了,手動滑稽.png

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

推薦閱讀更多精彩內容