該文已授權公眾號 「碼個蛋」,轉載請指明出處
在 Flutter
中,自帶手勢監聽的目前為止好像只有按鈕部件和一些 chip
部件,例如 Text
等部件需要實現手勢監聽,就需要藉助帶有監聽事件的部件來實現了,這節我們會講下 InkWell
和 GestureDetector
來實現手勢的監聽。
####InkWell
在前面的一些例子中,小夥伴應該看到了好幾次 InkWell
這個部件,通過它我們可以實現對一些手勢的監聽,並實現 MD
的水波紋效果,舉個簡單的一個例子
InkWell(
child: Text('點我...點我...我能響應點選手勢'),
onTap: () => print('啊...我被點選了...')
),
複製程式碼
那麼當點選 Text
的時候就會響應點選事件,控制檯輸出日誌
我們還是老套路,分析下原始碼。Ctrl
點選 InkWell
來檢視原始碼(Android Studio
的操作,別的我不懂喔...),然後,「嗯...除了建構函式怎麼什麼都沒有???」那隻能看它的父類 InkResponse
了,在那之前,我們看下 InkWell
的說明
/// A rectangular area of a [Material] that responds to touch.
複製程式碼
InkWell
是在 MaterialDesign
風格下的一個用來響應觸控的矩形區域(注意加粗的文字,1.如果不是 MD
風格的部件下,你是不能用這個來做點選響應的;2.InkWell
是一塊矩形區域,如果你要的是圓形區域,8 好意思,不行!)
/// The [InkWell] widget must have a [Material] widget as an ancestor. The
/// [Material] widget is where the ink reactions are actually painted. This
/// matches the material design premise wherein the [Material] is what is
/// actually reacting to touches by spreading ink.```
複製程式碼
InkWell
必須要有一個 Material
風格的部件作為錨點,巴拉巴拉巴拉....再次強調必須要在 MD
風格下使用。
接下來看下 InkResponse
吧
####InkResponse
const InkResponse({
Key key,
this.child, // 需要監聽的子部件
// 一個 `GestureTapCallback` 型別引數,看下 `GestureTapCallback` 的定義,
// `typedef GestureTapCallback = void Function();` 就是簡單的無參無返回型別引數
// 監聽手指點選事件
this.onTap,
// 一個 `GestureTapDownCallback` 型別引數,需要 `TapDownDetails` 型別引數,
// `TapDownDetails` 裡面有個 `Offset` 引數用於記錄點選的位置,監聽手指點選螢幕的事件
this.onTapDown,
// 同 `onTap` 表示點選事件取消監聽
this.onTapCancel,
// 同 `onTap` 表示雙擊事件監聽
this.onDoubleTap,
// 一個 `GestureLongPressCallback` 型別引數,也是無參無返回值,表示長按的監聽
this.onLongPress,
// 監聽高亮的變化,返回 `true` 表示往高亮變化,`false` 相反
this.onHighlightChanged,
// 是否需要裁剪區域,`InkWell` 該值為 `true`,會根據 `highlightShape` 裁剪
this.containedInkWell = false,
// 高亮的外形,`InkWell` 該值設定成 `BoxShape.rectangle`,所以是個矩形區域
this.highlightShape = BoxShape.circle,
this.radius, // 手指點下去的時候,出現水波紋的半徑
this.borderRadius, // 點選時候外圈陰影的圓角半徑
this.customBorder,
this.highlightColor, // 高亮顏色
this.splashColor, // 手指點下生成的水波顏色
this.splashFactory, // 兩個值 `InkRipple.splashFactory` 和 `InkSplash.splashFactory`
this.enableFeedback = true, // 檢測到手勢是否有反饋
this.excludeFromSemantics = false,
})
複製程式碼
所以一些簡單的觸控事件直接通過 InkWell
或者 InkResponse
就能夠實現,但是面臨一些比較複雜的手勢,就有點不太夠用了,我們需要通過 GestureDector
來進行處理
GestureDector
GestureDetector
也是一個部件,主要實現對各種手勢動作的監聽,其監聽事件檢視下面的表格
回撥方法 | 回撥描述 |
---|---|
onTapDown |
點選螢幕的手勢觸碰到螢幕時候觸發 |
onTapUp |
點選螢幕抬手後觸發,點選結束 |
onTap |
點選事件已經完成的時候觸發,和 onTapUp 幾乎同時 |
onTapCancel |
點選未完成,被其它手勢取代的時候觸發 |
onDoubleTap |
雙擊螢幕的時候觸發 |
onLongPress |
長按螢幕的時候觸發 |
onLongPressUp |
長按螢幕後抬手觸發 |
onVerticalDragDown |
觸碰到螢幕,可能發生垂直方向移動觸發,onVerticalDrag 系列事件不會同 onHorizontalDrag 系列事件同時發生 ,如果發生了 onVerticalDrag 則接下來如何變化移動,都不會觸發 onHorizontalDrag 事件,除非取消後重新觸發。判斷兩者的關鍵是準備滑動的意圖,先發生橫向滑動則觸發 onHorizontalDrag 事件,否則 onVerticalDrag 事件。 |
onVerticalDragStart |
觸碰到螢幕,並開始發生垂直方向的移動觸發 |
onVerticalDragUpdate |
垂直方向移動的距離變化觸發 |
onVerticalDragEnd |
抬手取消垂直方向移動的時候觸發 |
onVerticalDragCancel |
觸發 onVerticalDragDown 但是沒有完成整個 onVerticalDrag 事件觸發 |
onHorizontalDrag 系列介紹省略同上... |
|
onPanDown |
觸碰到螢幕,準備滑動的時候觸發,onPan 系列回撥不可和 onVerticalDrag 或者 onHorizontalDrag 系列回撥同時設定 |
onPanStart |
觸碰到螢幕,並開始滑動時候觸發 |
onPanUpdate |
滑動位置發生改變的時候觸發 |
onPanEnd |
滑動完成並抬手的時候觸發 |
onPanCancel |
觸發 onPanDown 但是沒有完成整個 onPan 事件觸發 |
onScaleStart |
兩個手指之間建立聯絡點觸發,初始縮放比例為 1.0 |
onScaleUpdate |
手指距離發生變化,縮放比例也跟隨變化觸發 |
onScaleEnd |
手指抬起,至間的聯絡斷開時候觸發 |
還有 onForcePress
系列事件,這個是根據對螢幕的擠壓力度進行觸發,需要達到某些定值才能觸發。GestureDetector
有個 behavior
屬性用於設定手勢監聽過程中的表現形式
deferToChild
預設值,觸控到child
的範圍才會觸發手勢,空白處不會觸發opaque
不透明模式,防止background widget
接收到手勢translucent
半透明模式,剛好同opaque
相反,允許background widget
接收到手勢
介紹完了手勢,那就可以實際操練起來了,比如,實現一個跟隨手指運動的小方塊,先看下效果圖
簡單的分析下,通過 Positioned
來設定小方塊的位置,根據 GestureDetector
的 onPanUpdate
修改 Positioned
的 left
和 top
值,當 onPanEnd
或者 onPanCancel
的時候設定為原點,那麼就可以有如圖的效果了
class GestureDemoPage extends StatefulWidget {
@override
_GestureDemoPageState createState() => _GestureDemoPageState();
}
class _GestureDemoPageState extends State<GestureDemoPage> {
double left = 0.0;
double top = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Gesture Demo'),
),
body: Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(child: Container(width: 50.0, height: 50.0, color: Colors.red), left: left, top: top),
GestureDetector(
behavior: HitTestBehavior.translucent,
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width - 10,
height: MediaQuery.of(context).size.height),
onPanDown: (details) {
setState(() {
left = details.globalPosition.dx;
top = details.globalPosition.dy;
});
},
onPanUpdate: (details) {
setState(() {
left = details.globalPosition.dx;
top = details.globalPosition.dy;
});
},
onPanCancel: () {
setState(() {
left = 0.0;
top = 0.0;
});
},
onPanEnd: (details) {
setState(() {
left = 0.0;
top = 0.0;
});
},
)
],
));
}
}
複製程式碼
如果說要實現一個放大縮小的方塊,就可以通過 onScaleUpdate
中獲取到的 details.scale
來設定方塊的寬高即可。這個比較簡單就留給小夥伴們自己實現效果了。
該部分程式碼檢視 gesture_main.dart
檔案
Animation 動畫
Flutter
的 Animation
是個抽象類,具體的實現需要看其子類 AnimationController
,在這之前,先了解下 Animation
的一些方法和介紹。
-
addListener
/removeListener
新增的監聽用於監聽值的變化,remove
用於停止監聽 -
addStatusListener
/removeStatusListener
新增動畫狀態變化的監聽,remove
停止監聽,Animation
的狀態有 4 種:dismissed
動畫初始狀態,反向運動結束狀態,forward
動畫正向運動狀態,reverse
動畫反向運動狀態,completed
動畫正向運動結束狀態。 -
drive
方法用於連線動畫,例如官方舉的例子,因為AnimationController
是其子類,所以也擁有該方法Animation<Alignment> _alignment1 = _controller.drive( AlignmentTween( begin: Alignment.topLeft, end: Alignment.topRight, ), ); 複製程式碼
上面的例子將
AnimationController
和AlignmentTween
結合成一個Animation<Alignment>
動畫,當然drive
可以結合多個動畫,例如Animation<Alignment> _alignment3 = _controller .drive(CurveTween(curve: Curves.easeIn)) .drive(AlignmentTween( begin: Alignment.topLeft, end: Alignment.topRight, )); 複製程式碼
因為 Animation
是抽象類,所以具體的還是需要通過 AnimationController
來實現。
####AnimationController
AnimationController({
double value, // 設定初始的值
this.duration, // 動畫的時長
this.debugLabel, // 主要是用於 `toString` 方法中輸出資訊
this.lowerBound = 0.0, // 最小範圍
this.upperBound = 1.0, // 最大範圍
// AnimationController 結束時候的行為,有 `normal` 和 `preserve` 兩個值可選
this.animationBehavior = AnimationBehavior.normal,
// 這個屬性可以通過 with `SingleTickerProviderStateMixin`
// 或者 `TickerProviderStateMixin` 引入到 `State`,通過 `this` 指定
@required TickerProvider vsync,
})
複製程式碼
AnimationController
控制動畫的方法有這麼幾個
forward
啟動動畫,和上面提到的forward
狀態不一樣reverse
方向啟動動畫repeat
重複使動畫執行stop
停止動畫reset
重置動畫
大概瞭解了 AnimationController
,接下來通過一個實際的小例子來加深下印象,例如實現如下效果,點選開始動畫,結束後再點選反向動畫
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000), lowerBound: 28.0, upperBound: 50.0);
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
// 一定要釋放資源
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Center(
child: IconButton(
icon: Icon(Icons.android, color: Colors.green[500], size: _animationController.value),
onPressed: () {
// 根據狀態執行不同動畫運動方式
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward();
}),
),
);
}
}
複製程式碼
那麼如果要實現無限動畫呢,那就可以通過 addStatusListener
監聽動畫的狀態來執行,修改程式碼,在 initState
增加如下程式碼
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse(); // 正向結束後開始反向
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward(); // 反向結束後開始正向
});
_animationController.forward(); // 啟動動畫
複製程式碼
把 Center
的 child
替換成一個 Icon
,因為上面已經啟動了動畫,所以不需要再用點選去啟動了,執行後就會無限放大縮小迴圈跑了。
在這個例子中,通過設定 AnimationController
的 lowerBound
和 upperBound
實現了動畫的變化範圍,接下來,將通過 Tween
來實現動畫的變化範圍。先看下 Tween
的一些介紹。
Tween
/// A linear interpolation between a beginning and ending value.
///
/// [Tween] is useful if you want to interpolate across a range.
///
/// To use a [Tween] object with an animation, call the [Tween] object's
/// [animate] method and pass it the [Animation] object that you want to
/// modify.
///
/// You can chain [Tween] objects together using the [chain] method, so that a
/// single [Animation] object is configured by multiple [Tween] objects called
/// in succession. This is different than calling the [animate] method twice,
/// which results in two separate [Animation] objects, each configured with a
/// single [Tween].
複製程式碼
Tween
是一個線性插值(如果要修改運動的插值,可以通過 CurveTween
來修改),所以線上性變化的時候很有用
通過呼叫 Tween
的 animate
方法生成一個 Animation
(animate
一般傳入 AnimationController
)
還可以通過 chain
方法將多個 Tween
結合到一起,這樣就不需要多次去呼叫 Tween
的 animate
方法來生成動畫了,多次呼叫 animate
相當於使用了兩個分開的動畫來完成效果,但是 chain
結合到一起就是一個動畫過程
那麼對前面的動畫進行一些修改,通過 Tween
來控制值的變化
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _scaleAnimation; // 動畫例項,用於修改值的大小
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 1000)); // 不通過 `lowerBound` 和 `upperBound` 設定範圍,改用 `Tween`
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed)
_animationController.forward();
});
// 通過 `Tween` 的 `animate` 生成一個 Animation
// 再通過 Animation.value 進行值的修改
_scaleAnimation = Tween(begin: 28.0, end: 50.0).animate(_animationController);
_animationController.forward();
}
@override
void dispose() {
// 一定要釋放資源
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Center(
// 通過動畫返回的值,修改圖示的大小
child: Icon(Icons.favorite, color: Colors.red, size: _scaleAnimation.value),
),
);
}
}
複製程式碼
再次執行,還是能過達到之前的效果,那麼很多小夥伴肯定會問了,「**,加了那麼多程式碼,效果還是和以前的一樣,還不如不加...」好吧,我無法反駁,但是如果要實現多個動畫呢,那麼使用 Tween
就有優勢了,比如我們讓圖示大小變化的同時,顏色和位置也發生變化,只通過 AnimationController
要怎麼實現? 又比如說,運動的方式要先加速後減速,那隻通過 AnimationController
要如何實現?這些問題通過 Tween
就會非常方便解決,直接上程式碼
class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _scaleAnimation; // 用於控制圖示大小
Animation<Color> _colorAnimation; // 控制圖示顏色
Animation<Offset> _positionAnimation; // 控制圖示位置
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));
// 當動畫值發生變化的時候,重繪下 icon
_animationController.addListener(() {
setState(() {});
});
_animationController.addStatusListener((status) {
if (_animationController.status == AnimationStatus.completed)
_animationController.reverse();
else if (_animationController.status == AnimationStatus.dismissed) _animationController.forward();
});
// 通過 `chain` 結合 `CurveTween` 修改動畫的運動方式,曲線型別可自行替換
_scaleAnimation =
Tween(begin: 28.0, end: 50.0).chain(CurveTween(curve: Curves.decelerate)).animate(_animationController);
_colorAnimation = ColorTween(begin: Colors.red[200], end: Colors.red[900])
.chain(CurveTween(curve: Curves.easeIn))
.animate(_animationController);
_positionAnimation = Tween(begin: Offset(100, 100), end: Offset(300, 300))
.chain(CurveTween(curve: Curves.bounceInOut))
.animate(_animationController);
_animationController.forward(); // 啟動動畫
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Stack(
children: <Widget>[
Positioned(
child: Icon(Icons.favorite, color: _colorAnimation.value, size: _scaleAnimation.value),
left: _positionAnimation.value.dx,
top: _positionAnimation.value.dy,
)
],
),
);
}
}
複製程式碼
那麼最後的效果圖
當然,Flutter
中已經實現的 Tween
還有很多,包括 BorderTween
、TextStyleTween
、ThemeDataTween
..等等,實現的方式都是類似的,小夥伴們可以自己慢慢看。
AnimationWidget
在上面的例子中,都是通過 addListener
監聽動畫值變化,然後通過 setState
方法來實現重新整理效果。那麼 Flutter
也提供了一個部件 AnimationWidget
來實現動畫部件,就不需要一直監聽了,還是實現上面的例子
class RunningHeart extends AnimatedWidget {
final List<Animation> animations; // 傳入動畫列表
final AnimationController animationController; // 控制動畫
RunningHeart({this.animations, this.animationController})
// 對傳入的引數進行限制(當然你也可以不做限制)
: assert(animations.length == 3),
assert(animations[0] is Animation<Color>),
assert(animations[1] is Animation<double>),
assert(animations[2] is Animation<Offset>),
super(listenable: animationController);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
// 之前的 animation 都通過 animations 引數傳入到 `AnimationWidget`
child: Icon(Icons.favorite, color: animations[0].value, size: animations[1].value),
left: animations[2].value.dx,
top: animations[2].value.dy,
)
],
);
}
}
複製程式碼
其實內部返回的部件和前面的是一樣的
接著對 _AnimationDemoPageState
類進行修改,註釋 initState
中的 _animationController.addListener
所有內容,然後將 body
屬性替換成新建的 RunningHeart
部件,記得傳入的動畫列表的順序
body: RunningHeart(
animations: [_colorAnimation, _scaleAnimation, _positionAnimation],
animationController: _animationController,
)
複製程式碼
這樣就實現了剛才一樣的效果,並且沒有一直呼叫 setState
來重新整理。
該部分程式碼檢視 animation_main.dart
檔案
StaggeredAnimations
Flutter
還提供了交錯動畫,聽名字就可以知道,是按照時間軸,進行不同的動畫,並且由同個AnimationController
進行控制。因為沒有找到好的例子,原諒我直接搬官方的例子來講,官方交錯動畫 demo
在繼續看之前,先了解下 Interval
/// An [Interval] can be used to delay an animation. For example, a six second
/// animation that uses an [Interval] with its [begin] set to 0.5 and its [end]
/// set to 1.0 will essentially become a three-second animation that starts
/// three seconds later.
複製程式碼
Interval
用來延遲動畫,例如一個時長 6s 的動畫,通過 Interval
設定其 begin
引數為 0.5,end
引數設定為 1.0,那麼這個動畫就會變成 3s 的動畫,並且開始的時間延遲了 3s。
瞭解 Interval
功能後,就可以看下例項了,當然我們不和官方的 demo 一樣,中間加個旋轉動畫
class StaggeredAnim extends StatelessWidget {
final AnimationController controller;
final Animation<double> opacity;
final Animation<double> width;
final Animation<double> height;
final Animation<EdgeInsets> padding;
final Animation<BorderRadius> border;
final Animation<Color> color;
final Animation<double> rotate;
StaggeredAnim({Key key, this.controller}):
// widget 透明度
opacity = Tween(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.0, 0.1, curve: Curves.ease))),
// widget 寬
width = Tween(begin: 50.0, end: 150.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.1, 0.250, curve: Curves.ease))),
// widget 高
height = Tween(begin: 50.0, end: 150.0)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
// widget 底部距離
padding = EdgeInsetsTween(begin: const EdgeInsets.only(top: 150.0), end: const EdgeInsets.only(top: .0))
.animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
// widget 旋轉
rotate = Tween(begin: 0.0, end: 0.25)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.375, 0.5, curve: Curves.ease))),
// widget 外形
border = BorderRadiusTween(begin: BorderRadius.circular(5.0), end: BorderRadius.circular(75.0))
.animate(CurvedAnimation(parent: controller, curve: Interval(0.5, 0.75, curve: Curves.ease))),
// widget 顏色
color = ColorTween(begin: Colors.blue, end: Colors.orange)
.animate(CurvedAnimation(parent: controller, curve: Interval(0.75, 1.0, curve: Curves.ease))),
super(key: key);
Widget _buildAnimWidget(BuildContext context, Widget child) {
return Container(
padding: padding.value,
alignment: Alignment.center,
// 旋轉變化
child: RotationTransition(
turns: rotate, // turns 表示當前動畫的值 * 360° 角度
child: Opacity(
opacity: opacity.value, // 透明度變化
child: Container(
width: width.value, // 寬度變化
height: height.value, // 高度變化
decoration: BoxDecoration(
color: color.value, // 顏色變化
border: Border.all(color: Colors.indigo[300], width: 3.0),
borderRadius: border.value), // 外形變化
),
),
),
);
}
@override
Widget build(BuildContext context) {
// AnimatedBuilder 繼承 AnimationWidget,用來快速構建動畫部件
return AnimatedBuilder(animation: controller, builder: _buildAnimWidget);
}
}
複製程式碼
然後修改 body
的引數,設定成我們的動畫,當點選的時候就會啟動動畫
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _playAnim,
child: Center(
// 定義一個外層圈,能夠使動畫顯眼點
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1), border: Border.all(color: Colors.black.withOpacity(0.5))),
child: StaggeredAnim(controller: _controller),
),
),
)
複製程式碼
看下最後的效果吧
該部分程式碼檢視 staggered_animation_main.dart
檔案
結束前,我們再講一種比較簡單的 Hreo
動畫,用來過渡用。
Hero
通過指定 Hero
中的 tag
,在切換的時候 Hero
會尋找相同的 tag
,並實現動畫,具體的實現邏輯,這裡可以推薦一篇文章 談一談Flutter中的共享元素動畫Hero,裡面寫的很詳細,就不造車輪了。當然這邊還是得提供個簡單的 demo 的,替換前面的 body
引數
body: Container(
alignment: Alignment.center,
child: InkWell(
child: Hero(
tag: 'hero_tag', // 這裡指定 tag
child: Image.asset('images/ali.jpg', width: 100.0, height: 100.0),
),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => HeroPage())),
),
)
複製程式碼
然後建立 HeroPage
介面,當然也可以是個 Dialog
,只要通過路由實現即可
class HeroPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
child: Hero(tag: 'hero_tag', child: Image.asset('images/ali.jpg', width: 200.0, height: 200.0)),
onTap: () => Navigator.pop(context),
),
),
);
}
}
複製程式碼
看下最後的效果圖:
該部分程式碼檢視 animation_main.dart
檔案
這一部分講的比較多,小夥伴可以慢慢消化,下節我會盡量填下之前留下的狀態管理的坑。
最後程式碼的地址還是要的:
-
文章中涉及的程式碼:demos
-
基於郭神
cool weather
介面的一個專案,實現BLoC
模式,實現狀態管理:flutter_weather -
一個課程(當時買了想看下程式碼規範的,程式碼更新會比較慢,雖然是跟著課上的一些寫程式碼,但是還是做了自己的修改,很多地方看著不舒服,然後就改成自己的實現方式了):flutter_shop
如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支援我繼續寫下去的動力~