本文使用原生Flutter
形式設計代碼,只講最基礎的東西,不使用任何其餘第三方庫(Provider
等)
寫了接近兩年的Flutter
,發現數據與事件的傳遞是新手在學習時常常問的問題:有不少初學者錯誤的在很是早期就引入provider
,BLOC
等模式去管理數據,過量使用外部框架,形成項目混亂難以組織代碼。其主要的緣由就是由於忽視了基礎的,最簡單的數據傳遞方式。segmentfault
很難想象有人把所有數據放在一個頂層provider
裏,而後絕對不寫StatefulWidget
。這種項目反正我是不維護,誰愛看誰看。
本文會列舉基本的事件與方法傳遞方式,而且舉例子講明如何使用基礎的方式實現這些功能。本文的例子都基於flutter
默認的加法demo修改,在dartpad
或者新建flutter
項目中便可運行本項目的代碼例子。數組
先來看下基本的幾個應用狀況,只要實現了這些狀況,在局部就能夠很是流暢的傳遞數據與事件:服務器
注意思考:下文的Widget
,哪些是StatefulWidget
?
描述:一個Widget
收到事件後,改變child顯示的值
實現功能:點擊加號讓數字+1
難度:⭐網絡
描述:一個Widget
在child收到事件時,改變本身的值
實現功能:點擊改變頁面顏色
難度:⭐app
描述:一個Widget
在child收到事件時,觸發本身的state的方法
實現功能:點擊發起網絡請求,刷新當前頁面
難度:⭐框架
描述:一個Widget
本身改變本身的值
實現功能:倒計時,從網絡加載數據
難度:⭐⭐⭐less
描述:一個Widget
本身的數據變化時,觸發state
的方法
實現功能:一個在數據改變時播放過渡動畫的組件
難度:⭐⭐⭐⭐異步
描述:一個Widget
收到事件後,觸發child
的state
的方法
實現功能:點擊按鈕讓一個child
開始倒計時或者發送請求
難度:⭐⭐⭐⭐⭐async
咱們平時寫項目基本也就是上面這些需求了,只要學會實現這些事件與數據傳遞,就能夠輕鬆寫出任何項目了。ide
使用簡單的回調就能夠實現這幾個需求,這也是整個flutter
的基礎:如何改變一個state
內的數據,以及如何改變一個widget
的數據。
描述:一個widget
收到事件後,改變child
顯示的值
實現功能:點擊加號讓數字+1描述:一個
widget
在child
收到事件時,改變本身的值
實現功能:點擊改變頁面顏色描述:一個
widget
在child
收到事件時,觸發本身的state
的方法
實現功能:點擊發起網絡請求,刷新當前頁面
這幾個都是毫無難度的,咱們直接看同一段代碼就好了
代碼:
/// 這段代碼是使用官方的代碼修改的,一般狀況下,只須要使用回調就能獲取點擊事件 class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { // 在按鈕的回調中,你能夠設置數據與調用方法 // 在這裏,讓計數器+1後刷新頁面 setState(() { _counter++; }); } // setState後就會使用新的數據從新進行build // flutter的build性能很是強,甚至支持每秒60次rebuild // 因此沒必要過於擔憂觸發build,可是要偶爾注意超大範圍的build @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ), floatingActionButton: _AddButton( onAdd: _incrementCounter, ), ); } } /// 通常會使用GestureDetector來獲取點擊事件 /// 由於官方的FloatingActionButton會自帶樣式,通常咱們會本身寫按鈕樣式 class _AddButton extends StatelessWidget { final Function onAdd; const _AddButton({Key key, this.onAdd}) : super(key: key); @override Widget build(BuildContext context) { return FloatingActionButton( onPressed: onAdd, child: Icon(Icons.add), ); } }
這種方式十分的簡單,只須要在回調中改變數據,再setState
就會觸發build
方法,根據當前的數據從新build
當前widget
,這也是flutter
最基本的刷新方法。
在flutter
中,只有StatefulWidget
才具備state
,state
才具備傳統意義上的生命週期(而不是頁面),經過這些週期,能夠作到一進入頁面,就開始從服務器加載數據,也可讓一個Widget
自動播放動畫
咱們先看這個需求:
描述:一個Widget
本身改變本身的值
實現功能:倒計時,從網絡加載數據
這也是一個常見的需求,可是不少新手寫到這裏就不會寫了,可能會錯誤的去使用FutureBuilder
進行網絡請求,會形成每次都反覆請求,實際上這裏是必須使用StatefulWidget
的state
來儲存請求返回信息的。
通常項目中,動畫,倒計時,異步請求此類功能須要使用state
,其餘大多數的功能並不須要存在state
。
例如這個widget
,會顯示一個數字:
class _CounterText extends StatelessWidget { final int count; const _CounterText({Key key, this.count}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Text('$count'), ); } }
能夠試着讓widget從服務器加載這個數字:
class _CounterText extends StatefulWidget { const _CounterText({Key key}) : super(key: key); @override __CounterTextState createState() => __CounterTextState(); } class __CounterTextState extends State<_CounterText> { @override void initState() { // 在initState中發出請求 _fetchData(); super.initState(); } // 在數據加載以前,顯示0 int count = 0; // 加載數據,模擬一個異步,請求後刷新 Future<void> _fetchData() async { await Future.delayed(Duration(seconds: 1)); setState(() { count = 10; }); } @override Widget build(BuildContext context) { return Center( child: Text('$count'), ); } }
又或者,咱們想讓這個數字每秒都減1,最小到0。那麼只須要把他變成stateful後,在initState中初始化一個timer,讓數字減少:
class _CounterText extends StatefulWidget { final int initCount; const _CounterText({Key key, this.initCount:10}) : super(key: key); @override __CounterTextState createState() => __CounterTextState(); } class __CounterTextState extends State<_CounterText> { Timer _timer; int count = 0; @override void initState() { count = widget.initCount; _timer = Timer.periodic( Duration(seconds: 1), (timer) { if (count > 0) { setState(() { count--; }); } }, ); super.initState(); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Center( child: Text('${widget.initCount}'), ); } }
這樣咱們就能看到這個widget
從輸入的數字每秒減小1。
因而可知,widget
能夠在state
中改變數據,這樣咱們在使用StatefulWidget
時,只須要給其初始數據,widget
會根據生命週期加載或改變數據。
在這裏,我建議的用法是在Scaffold
中加載數據,每一個頁面都由一個Stateful
的Scaffold
和若干StatelessWidget
組成,由Scaffold
的State
管理全部數據,再刷新便可。
注意,即便這個頁面的body是ListView
,也不推薦ListView
管理本身的state
,在當前state
維護數據的list
便可。使用ListView.builder
構建列表便可避免更新數組時,在頁面上刷新列表的所有元素,保持高性能刷新。
描述:一個Widget
本身的數據變化時,觸發state
的方法
實現功能:一個在數據改變時播放過渡動畫的組件
作這個以前,咱們先看一個簡單的需求:一行widget
,接受一個數字,數字是偶數時,距離左邊24px
,奇數時距離左邊60px
。
這個確定很簡單,咱們直接StatelessWidget
就寫出來了;
class _Row extends StatelessWidget { final int number; const _Row({ Key key, this.number, }) : super(key: key); double get leftPadding => number % 2 == 1 ? 60.0 : 24.0; @override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: leftPadding, ), child: Text('$number'), ); } }
這樣就簡單的實現了這個效果,可是實際運行的時候發現,數字左右橫跳,很不美觀。看來就有必要優化這個widget
,讓他左右移動的時候播放動畫,移動過去,而不是跳來跳去。
一個比較簡單的方案是,傳入一個AnimationController
來精確控制,可是這樣太複雜了。這種場景下,咱們在使用的時候一般只想更新數字,再setState,就但願他在內部播放動畫(一般是過渡動畫),就能夠不用去操做複雜的AnimationController
了。
實際上,這個時候咱們使用didUpdateWidget
這個生命週期就能夠了,在state
所依附的widget
更新時,就會觸發這個回調,你能夠在這裏響應上層傳遞的數據的更新,在內部播放動畫。
代碼:
class _Row extends StatefulWidget { final int number; const _Row({ Key key, this.number, }) : super(key: key); @override __RowState createState() => __RowState(); } class __RowState extends State<_Row> with TickerProviderStateMixin { AnimationController animationController; double get leftPadding => widget.number % 2 == 1 ? 60.0 : 24.0; @override void initState() { animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), lowerBound: 24, upperBound: 60, ); animationController.addListener(() { setState(() {}); }); super.initState(); } // widget更新,就會觸發這個方法 @override void didUpdateWidget(_Row oldWidget) { // 播放動畫去當前位置 animationController.animateTo(leftPadding); super.didUpdateWidget(oldWidget); } @override void dispose() { animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: animationController.value, ), child: Text('${widget.number}'), ); } }
這樣在狀態之間就完成了一個很是平滑的動畫切換,不再會左右橫跳了。
這裏咱們仍是先看需求
描述:一個Widget
收到事件後,觸發child
的state
的方法
實現功能:點擊按鈕讓一個child
開始倒計時或者發送請求(調用state的方法)
難度:⭐⭐⭐⭐⭐
首先必須明確的是,若是出如今業務邏輯裏,這裏是顯然不合理,是須要避免的。StatefulWidget
嵌套時應當避免互相調用方法,在這種時候,最好是將child
的state
中的方法與數據,向上提取放到當前層state
中。
這裏能夠簡單分析一下:
State
的didUpdateWidget
生命週期更加合理。這裏咱們也能夠勉強實現一下,在flutter
框架中,我推薦使用ValueNotifier
進行傳遞,child
監聽ValueNotifier
便可。controller
進去,而後child
註冊一個回調進controller
,這樣就能夠經過controller
控制。這裏也可使用provider
,eventbus
等庫,或者用key
,globalKey
相關方法實現。可是,必須再強調一次:無論用什麼方式實現,這種嵌套是不合理的,項目中須要互相調用state的方法時,應當合併寫在一個state
裏。原則上,須要避免此種嵌套,不管如何實現,都不該當是項目中的通用作法。
雖然不推薦在業務代碼中這樣寫,可是在框架的代碼中是能夠寫這種結構的(由於必須暴露接口)。這種狀況能夠參考ScrollController
,你能夠經過這個Controller
控制滑動狀態。
值得一提的是:ScrollController
繼承自ValueNotifier
。因此使用ValueNotifier
仍然是推薦作法。
其實controller
模式也是flutter
源碼中常見的模式,通常用於對外暴露封裝的方法。controller
相比於其餘的方法,比較複雜,好在咱們不會常常用到。
做爲例子,讓咱們實現一個CountController
類,來幫咱們調用組件內部的方法。
代碼:
class CountController extends ValueNotifier<int> { CountController(int value) : super(value); // 逐個增長到目標數字 Future<void> countTo(int target) async { int delta = target - value; for (var i = 0; i < delta.abs(); i++) { await Future.delayed(Duration(milliseconds: 1000 ~/ delta.abs())); this.value += delta ~/ delta.abs(); } } // 實在想不出什麼例子了,總之是能夠這樣調用方法 void customFunction() { _onCustomFunctionCall?.call(); } // 目標state註冊這個方法 Function _onCustomFunctionCall; } class _Row extends StatefulWidget { final CountController controller; const _Row({ Key key, @required this.controller, }) : super(key: key); @override __RowState createState() => __RowState(); } class __RowState extends State<_Row> with TickerProviderStateMixin { @override void initState() { widget.controller.addListener(() { setState(() {}); }); widget.controller._onCustomFunctionCall = () { print('響應方法調用'); }; super.initState(); } // 這裏controller應該是在外面dispose // @override // void dispose() { // widget.controller.dispose(); // super.dispose(); // } @override Widget build(BuildContext context) { return Container( height: 60, width: double.infinity, alignment: Alignment.centerLeft, padding: EdgeInsets.only( left: 24, ), child: Text('${widget.controller.value}'), ); } }
使用controller
能夠徹底控制下一層state
的數據和方法調用,比較靈活。可是代碼量大,業務中應當避免寫這種模式,只在複雜的地方構建controller
來控制數據。若是你寫了不少自定義controller
,那應該反思你的項目結構是否是出了問題。不管如何實現,這種傳遞方式都不該當是項目中的通用作法。
全局的數據,可使用頂層provider
或者單例管理,個人習慣是使用單例,這樣獲取數據能夠不依賴context
。
簡單的單例寫法,擴展任何屬性到單例便可。
class Manager { // 工廠模式 factory Manager() =>_getInstance(); static Manager get instance => _getInstance(); static Manager _instance; Manager._internal() { // 初始化 } static Manager _getInstance() { if (_instance == null) { _instance = new Manager._internal(); } return _instance; } }
做者:馬嘉倫
日期:2020/07/22
平臺:Segmentfault,勿轉載
個人其餘文章:
【開發經驗】Flutter避免代碼嵌套,寫好build方法
【Flutter工具】fmaker:自動生成倍率切圖/自動更換App圖標
【Flutter應用】Flutter精仿抖音開源
【Flutter工具】多是Flutter上最簡單的本地數據保存方案
寫這篇文章的緣由,是由於看到很多人在學習flutter
時,對於數據與事件的傳遞很是的不熟悉,又很早的去學習provider
等第三方框架,對於基礎的東西又只知其一;不知其二,致使代碼混亂項目混亂,不知如何傳遞數據,如何去刷新界面。因此寫這篇文章總結了最基礎的各類事件與數據的傳遞方法。
簡單總結,flutter
改變數據最基礎的就是這麼幾種模式:
state
的數據,setState
向child
傳遞新數據child
的事件回調child
更新目標數據,child
監聽數據的變化,更加細節的改變本身的state
child
傳遞controller
,全面控制child
的state
項目中只須要這幾種模式就能很簡單的所有寫完了,使用provider
等其餘的庫,代碼上並不會有特別大的改善和進步。仍是但願你們學習flutter
的時候,能先摸清基本的寫法,再進行更深層次的學習。