- Framework(Dart): High level libs. Theme, Widgets, Layout, Animation, Gestures, Testing, etc.
- Plugin: JSON, Geolocation, Camera, Contacts, etc.
-
Engine(C/C++): Core C++ libs. Low level Flutter API:
- I/O, graphics, text layout, accessibility, plutin architechture, Dart runtime, Skia, etc.
- Embered: Platform specific. Android, iOS, Web, etc.
Three Trees
- Widget: Blueprint, public API. Build UI.
-
Element:
-
ComponentElement
: 由多個(gè)Element
組成 -
RenderObjectElement
: 持有一個(gè)RenderObejct
-
- RenderObject: Drawing, layout, handle interactions
runApp() -> root widget -> build() -> widget tree -> element tree -> render object tree
先來點(diǎn)前菜, 隨便看幾個(gè)例子有如下筆記:
- In Flutter, almost everything that makes up the user interface is a
Widget
- Stateful widget meaning that it has a
State object
(State<T> returned bycreateState()
method) -
setState(() {})
trigger a rebuild of the widget tree (by rerunbuild()
)- The Flutter framework has been optimized to make rerunning build methods fast
- 而不是像rxSwift或SwiftUI那樣, 用通知和觀察機(jī)制, 但說不上誰更好. 通知機(jī)制需要在定義的時(shí)候就修改, setState機(jī)制不需要改定義的地方, 只要修改觸發(fā)的地方
- SwiftUI的
padding
是一個(gè)修飾器, 意思是你先寫視圖, 我再來修飾你- 而Flutterr
Padding
則是一個(gè)組件, 而且是一個(gè)容器, 先寫容器再寫child - 同理, 你要align一個(gè)widget, 也是往外包裝, 而不是寫它的屬性
- 而Flutterr
-
List
只接受count, 不接受數(shù)組- 有l(wèi)azy特性, 不會(huì)實(shí)例化全部子組件
-
Material Design
的Card組件自帶了圓角和陰影-
elevation
的意思是卡片離屏幕的高度(控制陰影)
-
- 固定的間距用
SizedBox
- 可充滿parent的組件是
Expanded
, 類似SwiftUI里VStack后面給你自動(dòng)加了個(gè)Spacer()
- 我一開始還以為是可以點(diǎn)擊縮放的組件
- Flutter也有個(gè)獨(dú)立的
Spacer
組件,Spacer = Expanded(child: Container())
- 給視圖加交互特性跟給視圖加padding是一樣的, 要在外面包(
GestureDetector
)- 或者用各種Butotn
-
appBar
會(huì)自己加back - 檢查暗黑模式
Theme.of(context).brightness == Brightness.light
-
Function
的定義里并不約束入?yún)?Function func;
-
func(1)
,func('1')
,func(1, 2, '3')
都是可以的
-
- 主題:
Theme.of(context)
- 屏幕:
MediaQuery.of(context)
思考:
Padding(padding:child:)
和Container(padding:child:)
有什么區(qū)別?
實(shí)驗(yàn)下:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(10.0),
child: Text('WestWorld',style: TextStyle(fontSize: 30.0),),
),
Container(
padding: const EdgeInsets.all(10.0),
color: Colors.red,
child: const Text('WestWorld',style: TextStyle(fontSize: 30.0),)
),
],
)
如上圖, 在UI上是沒區(qū)別的, 在Widget樹上層級(jí)也是一樣的, 區(qū)別就在于Padding不能提供任何繪制, 只提供layout, 而Container則提供了繪制能力(比如背景色)
Dart
enum
- Dart的枚舉是int類型, 從0開始
-
enum
是class
的子類, 所以可以定義方法 -
enum
可以定義values
屬性, 返回所有枚舉值 -
enum
可以定義index
屬性, 返回枚舉值對(duì)應(yīng)的int值
// 1
enum Color { red, green, blue }
void main() {
print(Color.values); // [Color.red, Color.green, Color.blue]
print(Color.red.index); // 0
}
// 2
enum Color {
red("red", Colors.red),
green("green", Colors.green),
blue("blue", Colors.blue),
const Color(this.label, this.color);
final String label;
final Color color;
@override
String toString() => '$label: $hexCode';
}
extension ColorExtension on Color {
String get hex {
switch (this) {
case Color.red:
return 'FF0000';
case Color.green:
return '00FF00';
case Color.blue:
return '0000FF';
}
}
// 簡(jiǎn)潔寫法
String get hexCode {
const hexCodes = ['#FF0000', '#00FF00', '#0000FF'];
return hexCodes[this.index];
}
}
void main() {
print(Color.red.hex); // FF0000
print(Color.values[1].hex); // 00FF00
print(Color.blue); // blue: #0000FF
}
const
和final
-
const
是編譯時(shí)常量,final
是運(yùn)行時(shí)常量 -
const
可以用于class
的成員變量, 構(gòu)造函數(shù)和構(gòu)造函數(shù)的參數(shù),final
不行
class Color {
final String label;
final Color color;
const Color(this.label, this.color)
}
copyWith
基本上等于定義了一個(gè)構(gòu)造函數(shù), 只不過入?yún)⒍际强蛇x的, 使用的時(shí)候全部先判斷一下
class AppState {
AppState({
required this.products,
this.itmesInCart = const <String>{},
});
final List<String> products;
final Set<String> itemsInCart;
AppState copyWith({
List<String>? products,
Set<String>? itemsInCart,
}) {
return AppState(
products: products ?? this.products,
cart: itemsInCart ?? this.itemsInCart,
);
}
}
技巧
- random color:
Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0)
- with
import 'dart:math' as math;
- with
- 構(gòu)造函數(shù)
MyClass({Key? key}) : super(key: key);
, 簡(jiǎn)化為MyClass({super.key})
點(diǎn)擊事件
一些博客里說Flutter中有三種方式提供點(diǎn)擊事件: InkWell
, GestureDetector
, RaisedButton
(準(zhǔn)確地說應(yīng)該是各種button*):
-
InkWell
是Material
組件, 所以只能在Material
組件里使用
InkWell(
child: buildButtonColum(Icons.call, 'CALL'),
onTap:(){
print('CALL');
},
),
GestureDetector(
onTap: () {
print('onTap');
},
child: Text("DianJi")
)
RaisedButton(
onPressed: () {
print('onPressed');
},
child: Text("DianJi"),
)
其實(shí)還有更多:
ListTile(
leading: Icon(Icons.phone),
title: Text('CALL'),
subtilte: Text('to me'),
onTap: () {
print('CALL');
},
)
Iconbutton(icon: const Icons.remove,
onPressed: () {
print('onPressed');
})
FilledButton(onPressed: () {
print('onPressed');
}, child: Text("DianJi"))
// 這里用了extended, 因?yàn)榕渲昧藞D標(biāo)和文字
FloatingActionButton.extended(onPressed: () {
print('onPressed');
},
tooltip: 'button',
icon: const Icon(Icons.add),
label: Text("float button")
)
TextButton(onPressed: () => print(33), child: Text("Text button"))
ElevatedButton(onPressed: () => print(44), child: Text("more button"))
路由
- So far, you’ve used
MaterialApp
, which extendsWidgetsApp
. - WidgetsApp wraps many other common widgets that your app requires.
- Among these wrapped widgets there’s a top-level
Navigator
to manage the pages you push and pop.
- Among these wrapped widgets there’s a top-level
Navigator 1.0
Navigator.push(context, MaterialPageRoute(builder: (context) => const MyPage()));
Navigator.pop(context);
Router API (Navigator 2.0)
pubspec.yaml
url_launcher: ^6.2.1 // 跨平臺(tái)打開URL的包
go_router: ^13.0.1 // 聲明式路由語法糖包
- A user taps a button.
- The button handler tells the
app state
to update. - The router is a
listener
of the state. - Based on the new state changes, the router reconfigures the list of pages for the navigator.
- The navigator detects if there’s a new page in the list and handles the transitions to show the page.
image.png
用起來很復(fù)雜:
- you must create your
RouterDelegate
, bundle your app state logic with your navigator and configure when to show each route. - To support the web platform or handle deep links, you must implement
RouteInformationParser
to parse route information. - Eventually, developers and even Google realized the same thing: creating these components wasn’t straightforward. As a result, developers wrote other routing packages to make the process easier.(三方庫解君愁)
GoRouter
(個(gè)人開發(fā), Flutter團(tuán)隊(duì)接管)
import 'package:go_router/go_router.dart';
late final _router = GoRouter( // 注意是個(gè)late
initialLocation: '/login',
redirect: _appRedirect, // Add App Redirect
routes: [
// TODO: Add More Rotes
],
// Add Error Handler
errorPageBuilder: (context, state) {
return MaterialPage(
key: state.pageKey,
child: Scaffold(
body: Center(
child: Text(
state.error.toString(),
),
),
),
);
},
);
// redirect指的是在route之前一定要檢查的步驟
// 不滿足什么條件就跳什么頁面, 滿足什么條件則跳什么頁面, 可以理解為最核心的路由
Future<String?> _appRedirect(
BuildContext context, GoRouterState state) async {
final loggedIn = await _auth.loggedIn;
final isOnLoginPage = state.matchedLocation == '/login';
// Go to /login if the user is not signed in
if (!loggedIn) {
return '/login';
}
// Go to root of app / if the user is already signed in
else if (loggedIn && isOnLoginPage) {
return '/home'; // 任意指定路徑, 如果用外部存儲(chǔ)的變量的話, 那么等于也是個(gè)動(dòng)態(tài)路徑了, 即當(dāng)前取值是什么, 就能跳到那個(gè)頁面
}
// no redirect
return null;
}
改造MaterialApp
的構(gòu)造函數(shù):
return MaterialApp.router( // 這里
debugShowCheckedModeBanner: false,
routerConfig: _router, // 三方庫設(shè)置delegate, parser, provider等的地方
// TODO: Add Custom Scroll Behavior
title: 'Yummy',
scrollBehavior: CustomScrollBehavior(),
themeMode: themeMode,
theme: ...,
darkTheme: ..., );
- 跳頁:
context.go('/home')
- 取路徑參數(shù):
state.pathParameters['id']
context
, state
都是GoRoute
的構(gòu)造函數(shù)里builder的入?yún)?/p>
GoRoute(
path: '/home/:id',
builder: (context, state) {
final id = state.pathParameters['id'];
return HomePage(id: id);
},
routes: [...] // 如果有子頁面, 在這里配置
),
上圖我們看到了route是可以嵌套的, 所以才叫"聲明式"的, 跟UI樹一樣. 但就引出了一個(gè)問題, 如果要在別的tree里引用這個(gè)頁面呢?
其實(shí)嵌套聲明的結(jié)果就是路徑能拼接起來, 所以只要路徑拼接對(duì), 就能引用到子頁面.
彈出并跳到另一個(gè)頁面, 是需要自己分別手寫的
context.pop();
context.go('/${YummyTab.orders.value}');
登出后跳轉(zhuǎn)到登錄頁面:
widget.auth.signOut().then((value) => context.go('/login'));
上面解釋redirect
的時(shí)候提到過, 所以我們其實(shí)可以用redirect
來做登出后的跳轉(zhuǎn)而不是手寫跳轉(zhuǎn)
跳頁有兩種寫法:
context.go(path)
-
context.goNamed(name)
一般建議盡量用后面那種, 因?yàn)槁窂娇赡軙?huì)變, 以及name
會(huì)檢查錯(cuò)誤, 寫錯(cuò)了path
是不會(huì)檢查的.
其它庫:
State Management
- State Management: A way to manage the state of your app.
- State: The data that your app needs to keep track of.
-
State Management Frameworks: A set of tools and libraries that help you manage the state of your app.
- Provider: A state management framework that allows you to manage the state of your app in a declarative way.
Deep Links
用一個(gè)鏈接, 就能直接跳到某個(gè)頁面:
- URI Schemes
- [iOS Universal Links](https://www.kodeco.com/6080-universal- links-make-the-connection)
- [Android App Links](https://www.kodeco.com/18330247-deep- links-in-android-getting-started)
iOS setup (ios/Runner/Info.plist
):
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>kodeco.com</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yummy</string>
</array>
</dict>
</array>
注冊(cè)了yummy://kodeco.com/<paht>
Android setup (android/app/src/main/AndroidManifest.xml
):
<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="yummy"
android:host="kodeco.com" />
</intent-filter>
測(cè)試:
# iOS
open -a Simulator
xcrun simctl openurl booted 'yummy://kodeco.com/1'
# Android
adb shell am start -W -a android.intent.action.VIEW
-c android.intent.category.BROWSABLE \
-d "yummy://kodeco.com/home"
Recap:
- The app notifies
RouteInformationProvider
when there’s a new route to navigate to. - The provider passes the route information to
RouteInformationParser
to parse the URL string. - The parser converts the route information state to and from a URL string.
-
GoRouter
converts route information state to and from aRouteMatchList
. -
GoRouter
supports deep linking and web browser address bar paths out of the box. - In development mode, the Flutter web app doesn’t persist data between app launches. The web app generated in release mode will work on other browsers.
Platform Channels
- Platform Channels: A way to communicate between the Flutter app and the native platform (iOS and Android).
Widgets 概念
-
Widget: A basic building block of a Flutter app.
-
StatelessWidget:
- constructor -> build(context:)
- 刷新時(shí)機(jī):(insert into widget tree; ancestor nodes changes)
-
StatefulWidget: A widget that has mutable state.
- constructor -> createState() -> 反復(fù)調(diào)用
build(context:)
- initState(): 安卓的
onCreate()
, iOS的viewDidLoad()
- constructor -> createState() -> 反復(fù)調(diào)用
-
InheritedWidget: A widget that can pass data to its descendants. 能訪問父層級(jí)widget的state
-
lifting state up
: By adopting an inherited widget in your tree, you can reference the data from any of its descendants. - e.g.: Theme, network API...
- 高級(jí)framework:
Riverpod
-
- State: The mutable state of a widget.
-
StatelessWidget:
All widgets are immutable(可變的是
state
)
永遠(yuǎn)不要在
build()
里做復(fù)雜的計(jì)算
Asynchronous code should ALWAYS check if the
mounted
property is true before callingsetstate()
, because the widget may no longer be part of the widget tree.
按作用分:
- Structure and navigation
- Displaying information
- Positioning Widgets
- 感覺position與structure有點(diǎn)重合
- 事實(shí)上是
Positioned(left:top:right:bottom:child:)
這種類似于在元素內(nèi)絕對(duì)定位的組件
一些需要注意的:
-
RotatedBox
屬于哪一種組件? (根據(jù)quarterTurns
的值決定轉(zhuǎn)多少個(gè)90度) - 上一行title, 下一行內(nèi)容的結(jié)構(gòu)居然也有個(gè)組件, 叫
ListTile(tilte:subtitle:)
- 后來發(fā)現(xiàn)跟iOS的
UITableViewCell
一樣, 有leading
,title
,subtitle
,trailing
四個(gè)部分, 只不過它可以獨(dú)立使用.
- 后來發(fā)現(xiàn)跟iOS的
- 圓頭像也有組件:
CircleAvatar(radius:backgroundImage:)
-
AspectRatio(aspectRatio:child:)
里的比率指的是寬高比 -
Divider(thickness:height:color:)
, 對(duì), 分割線也給你做好了 -
Container
用來group widgets, 但SwiftUI中Group是不生成View的
切換安卓和蘋果主題:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
自定義組件內(nèi)字體的主題顏色(apply
):
final textTheme = Theme.of(context).textTheme
.apply(
bodyColor: Colors.white, // 純自定義
displayColor: Theme.of(context).colorTheme.onSurface, // 使用主題里定義好的別的顏色
);
Text(
'Hello World',
style: textTheme.titleMedium,
)
延伸 字體參考:
- 字體大小級(jí)別: Display > Headline > Title > Label > Body
- 每個(gè)級(jí)別下有: Large, Medium, Small
- 組合起來, 比如:
DisplayLarge
所以上文中更改字體顏色的bodyColor
, displayColor
就是分別針對(duì)的不同類別(style):
The displayColor is applied to [
displayLarge
], [displayMedium
], [displaySmall
], [headlineLarge
], [headlineMedium
], and [bodySmall
]. The bodyColor is applied to the remaining text styles.
Scrollable Widgets
- ListView: A scrollable, linear array of widgets.
- GridView: A scrollable, 2D array of widgets.
- SingleChildScrollView: A single child widget that can be scrolled.
- CustomScrollView: A scrollable widget that can contain multiple scrollable children.
- ScrollController: A controller for a scrollable widget.
- ScrollPosition: A position in a scrollable widget.
- ScrollPhysics: The physics that govern the motion of a scrollable widget.
ListView
- ListView =
Row
/Column
+ scroll - 包到
SizedBox
才能限制List的寬或高 - 也是一種sliverlist
- 如果ListView在
Column
里, 且不是唯一元素, 你可以試試, 不把它包到Expanded
里是顯示不出來的
// 固定數(shù)目的寫死children
ListView(
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
),
...,
],
)
// 用List.generate動(dòng)態(tài)生成children
ListView(
children: List.generate(100, (index) {
return ListTile(
title: Text('Item $index'),
);
}),
)
// 用ListBuilder
ListView.builder(
itemCount: 50,
itemBuilder: (context,index) {
return Container(
color: ColorUtils.randomColor(),
height: 50,
);
}
)
Nested ListView
- 如果你用
Column
包List, 那么具體的List需要有固定的高- 然后List只能在小區(qū)域內(nèi)滾動(dòng)
- 如果是List + List, 設(shè)置parent的
shrinkWrap
為true, 則parent的list高度會(huì)以childern的高度為高度(也即沒有滾動(dòng)區(qū)域)- 其實(shí)就是child的list有多高, 那么parent就把它包起來, 一起在parent的parent里滾動(dòng)(如果parent不支持滾動(dòng)那就是超出部分不可見)
- child需要設(shè)置的:
-
shrinkWrap
: true, 說明是固定高度的scrollable list -
primary
: false, 告知系統(tǒng)這個(gè)list不是主要的scrll view, 往外層找 -
physics
:NeverScrollableScrollPhysics
, 這個(gè)是雙保險(xiǎn), 也是設(shè)置無需滾動(dòng)(其它滾動(dòng)效果, 如反彈, 翻頁等也是這個(gè)屬性)
-
CustomScrollView(
slivers: <Widget>[
SliverAppBar(...),
SliverToBoxAdapter(
child:ListView(...),
),
SliverList(...),
SliverGrid(...),
],
)
CustomScrollView & Sliver
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('List Basic')),
body: Row(
children: [
Expanded(
child: CustomScrollView(
slivers: [_buildSliverList1()], // SliverList不能直接放到Column/Row里
)),
Expanded(child: _buildListView()),
Expanded(child: _buildListView2()),
],
),
);
}
// 兩種delegate(build 和 childList)
SliverList _buildSliverList1() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 100,
),
);
}
SliverList _buildSliverList2() {
return SliverList(
delegate: SliverChildListDelegate(
[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
],
),
)
}
見[[languages.flutter.sliver]]專題吧
GridView
-
GridView.count
: 指定列數(shù). -
GridView.builder
: 滾動(dòng)到的時(shí)候就build一個(gè)cell -
GridView.custom
: 使用sliverGridDelegate
來定制 -
GridView.extent
: 能定制每一行的列數(shù)
GridView _buildGridView(int columns) {
return GridView.builder(
padding: const EdgeInsets.all(0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 3.5,
crossAxisCount: columns,
),
itemBuilder: (context, index) => _buildGridItem(index),
itemCount: widget.restaurant.items.length,
shrinkWrap: true, // 隨cells數(shù)量而增高
physics: const NeverScrollableScrollPhysics(), // 不滾動(dòng), 意思是它的容器空間足夠, 也意味著容器本身是可以滾動(dòng)的, 所以子列表不需要在有限空間里滾動(dòng)
);
}
注意
gridDelegate
, 跟iOS的delegate
還是有區(qū)別的, iOS的代理是一個(gè)實(shí)時(shí)計(jì)算的方法, 而flutter里顯然更像一組options
, 或者說是一個(gè)colection的一組屬性.
但是
SliverList
的delegate
是可以傳入一個(gè)build方法的:
SliverChildBuilderDelegate((context, index) => widget, childCount:)
StaggeredGridView, 一個(gè)任意cell大小的grid view
DecorationBox
- 注意
DecorationBox
和BoxDecoration
的區(qū)別, 一個(gè)是widget, 一個(gè)是class -
BoxDecoration
is a class that provides a variety of properties for styling a box, such as color, border, and background image. - 一般要給視圖一個(gè)背景圖什么的, 與其自己去stack, flutter也給你提供了這個(gè)
DecorationBox
, 還是那句話, flutter給你提供的太多了, 我不認(rèn)為這是好事
下例中, 原始代碼是stack > image > text
, 因?yàn)榘l(fā)現(xiàn)文字在圖片上, 有時(shí)候難以閱讀, 就想給文字加漸變(加漸變就能讓文字容易閱讀? 我一般是選擇描邊或陰影), 這里就用到decoration組件了
看圖:
- 裝飾屬性是用
BoxDecoation
來描述的, 這里是添加了漸變 - 裝飾的是image, 而不是文字,
- 這里解釋了我的疑問, 為什么給文字加漸變就能易讀, 原來它是在圖片和文字中間加了一個(gè)中心漸變的層, 就這么簡(jiǎn)單
Drawer
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: scaffoldKey, // 使用這個(gè)key
endDrawer: _buildEndDrawer(), // 左右兩個(gè)抽屜
drawer: _buildDrawer(),
floatingActionButton: _buildFloatingActionButton(),
body: Center(
child: SizedBox(
width: constrainedWidth,
child: _buildCustomScrollView(),
),
),
)
// 觸發(fā):
scaffoldKey.currentState.openEndDrawer();
注意, 仍舊是Flutter的尿性, 你的Drawer必須是一個(gè)Drawer
, 任意容器Widget是不行的:
Widget _buildEndDrawer() {
return SizedBox(
width: drawerWidth,
child: Drawer( // Here
child: // 你真實(shí)的Widget(可以有自己完整的scaffold),
),
);
}
DatePicker, TimePicker
final picked = await showDatePicker(
context: context,
initialDate: selectedDate ?? DateTime.now(),
firstDate: _firstDate,
lastDate: _lastDate,
);
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
}
看到了嗎? 打開窗體, 獲取選中值, 是用一個(gè)await
來實(shí)現(xiàn)的, 并不需要回調(diào)或代理什么的, 同理, 時(shí)間選擇:
final picked = await showTimePicker(
context: context,
initialEntryMode: TimePickerEntryMode.input, // 默認(rèn)是輸入模式, 不是時(shí)鐘模式
initialTime: selectedTime ?? TimeOfDay.now(),
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat: true, // 覆蓋一個(gè)設(shè)備/媒體屬性, 通通用24小時(shí)制(慣用手法, 用QediaQuery包了一層)
),
child: child!,
);
},
);
if (picked != null && picked != selectedTime) {
setState(() {
selectedTime = picked;
});
}
Dismissible
左滑刪除也做了組件, 直接用就是了, 就是如果左滑出一個(gè)自定義的菜單, 那就得自行配置菜單項(xiàng), 動(dòng)畫也得自己寫了, 系統(tǒng)提供的是會(huì)清掉這個(gè)UI元素, 不要看觸發(fā)的動(dòng)作是一樣的就以為也能通過別的配置支持 (看看flutter_slidable
)
// 比如原始視圖是一個(gè)ListTile, 現(xiàn)在就包一層Dismissible, 在child前加如下代碼:
key: Key(item.id), // unique id
direction: DismissDirection.endToStart,
background: Container(),
secondaryBackground: const SizedBox(
child: Row( // 其實(shí)就起個(gè)背景的作用
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.delete),
],
), ),
onDismissed: (direction) { // 從參數(shù)名就看出, 它是自動(dòng)處理了ui的移除的
setState(() {
widget.cartManager.removeItem(item.id); // 同步數(shù)據(jù)源
});
widget.didUpdate();
},
Networking, Psesistence, State
save to:
- shared preferences
- sqflite
- file system
- cloud storage
Shared Preferences
- key-value
-
SharedPreferences
on Android,Userdefaults
on iOS - 不要存敏感數(shù)據(jù)
-
Android Keystore
oriOS Keychain
- or
flutter_secure_storage
-
final prefs = await SharedPreferences.getInstance();
prefs.setString('key', 'value'); // bool, int, double, stringList
prefs.getString('key');
prefs.remove('key');
prefs.clear();
prefs.containsKey('key');
這就是全部了, 注意一個(gè)await
就行
Provider
use Riverpod
library to provide resources to other parts of the app.
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 直接初始化provider
final sharedPrefProvider = Provider((ref) async {
return await SharedPreferences.getInstance();
});
// 或
final sharedPrefProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError(); // 延遲初始化
});
// runApp方法也要改寫
final sharedPrefs = await SharedPreferences.getInstance();
runApp(ProviderScope(overrides: [ // 依賴注入
sharedPrefProvider.overrideWithValue(sharedPrefs),
], child: const MyApp()));
// ref是riverpod里的, 直接用
final String? name = ref.watch(sharedPrefProvider).getString('name');
// 用riverpod取shared preference
final prefs = ref.read(sharedPrefProvider);
prefs.setStringList(prefSearchKey, previousSearches);
如果是web平臺(tái)上debug, 因?yàn)槊看螁?dòng)端口不一樣, 所以固定端口, 添加--web-port=8080
到flutter run
命令或Additional run args
配置項(xiàng)里
SQFLite
- SQLite is a lightweight, embedded database engine that provides a relational database management system.
- SQFLite is a Dart package that provides a wrapper around the SQLite database engine.
- SQFLite is available for both Android and iOS platforms.
- SQFLite is a popular choice for mobile app development because it is lightweight, fast, and easy to use.
final database = await openDatabase(
'database.db', // 數(shù)據(jù)庫名
version: 1, // 版本號(hào)
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE IF NOT EXISTS items (id TEXT PRIMARY KEY, name TEXT, price REAL)',
);
},
);
await database.insert(
'items',
{'id': '1', 'name': 'Item 1', 'price': 10.99},
conflictAlgorithm: ConflictAlgorithm.replace,
);
File System
final file = File('path/to/file');
await file.writeAsString('content');
Cloud Storage
final storage = FirebaseStorage.instance;
final ref = storage.ref().child('path/to/file');
final uploadTask = ref.putFile(File('path/to/file'));
final downloadURL = await (await uploadTask).ref.getDownloadURL();
JSON
- 前面一截
encode
,decode
部分由dart:convert
包提供 - 后面一截
serialize
,deserialize
部分由json_serializable
包提供 - 當(dāng)然也可以手動(dòng)寫
fromJSON
和toJSON
, 但如果字段一多編寫和維護(hù)就是個(gè)災(zāi)難,sjon_serializable
包就是幫你來生成這些代碼的, 但是需要- 編寫
fromJSON
和toJSON
的factory
占位方法(而不是真的去綁定字段) - 手動(dòng)運(yùn)行
dart run build_runner build
命令(這樣會(huì)成成.part
為人的不, 包含了你第一步寫的占位方法) - 如果不想每次手動(dòng)運(yùn)行, 就watch起來
dart run build_runner watch
- 編寫
純?cè)鶧emo
class Recipe {
final String uri;
final String label;
Recipe({this.uri, this.label});
}
// 在工廠方法里實(shí)現(xiàn)每個(gè)鍵和屬性的映射
factory Recipe.fromJson(Map<String, dynamic> json) {
return Recipe(json['uri'] as String, json['label'] as String);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{ 'uri': uri, 'label': label}
}
三方庫Demo
先添加依賴并pub get
json_annotation: ^4.8.1
json_serializable: ^6.7.1
import 'package:json_annotation/json_annotation.dart';
// 也是聲明, 這個(gè)文件build前不會(huì)不存在
part 'spoonacular_model.g.dart';
// 聲明一下
@JsonSerializable()
class SpoonacularResult {
int id;
String title;
String image;
String imageType;
SpoonacularResult({
required this.id,
required this.title,
required this.image,
required this.imageType,
});
// 引導(dǎo)兩個(gè)工廠方法去調(diào)用兩個(gè)模板方法(build前會(huì)報(bào)錯(cuò), 因?yàn)檫@兩個(gè)方法還不存在)
factory SpoonacularResult.fromJson(Map<String, dynamic> json)
=>
_$SpoonacularResultFromJson(json);
Map<String, dynamic> toJson() =>
_$SpoonacularResultToJson(this);
}
順便看一眼生成的part
文件:
part of 'spoonacular_model.dart';
SpoonacularResults _$SpoonacularResultsFromJson(Map<String,
dynamic> json) =>
SpoonacularResults(
results: (json['results'] as List<dynamic>)
.map((e) => SpoonacularResult.fromJson(e as
Map<String, dynamic>))
.toList(),
offset: json['offset'] as int,
number: json['number'] as int,
totalResults: json['totalResults'] as int,
);
跟手寫代碼一樣, 從Map
里取出對(duì)應(yīng)的鍵賦值給對(duì)應(yīng)的屬性
使用
// http
final result = await http.get(Uri.parse('https://api.spoonacular.com/recipes/complexSearch?apiKey=YOUR_API_KEY&query=chicken&number=1'));
final json = jsonDecode(result.body);
final spoonacularResults = SpoonacularResults.fromJson(json);
// file
final jsonString = await rootBundle.loadString('assets/
recipes1.json');
final spoonacularResults =
SpoonacularResults.fromJson(jsonDecode(jsonString));
生產(chǎn)中, 會(huì)更多地基于freezed庫來使用
Http Requests
上面有請(qǐng)求url并解析json的例子, 就像Android有Retrofit
, iOS有AlamFire
, 第三方的庫通常會(huì)提供更模塊化和線性的封裝, 常用的庫有dio
, chopper
, requests
, retrofit
等
State
- The main job of a UI is to represent state
- State is when a widget is active and stores its data in memory.
- state management: adopt a pattern that programmatically establishes how to track changes and broadcast details about states to the rest of your app.
- There are two types of state to consider
-
ephemeral state, also known as
local state
, which is limited to the widget,
-
StatefulWidget
can hold state, its children can access it(but need to pass down manually)- When you create a
StatefulWidget
, thecreateState()
method gets called,
- When you create a
-
InheritedWidget
is a built-in class allowing child widgets to access its data- by calling
context.dependOnInheritedWidgetOfExactType<class>().
- by calling
- InheritedWidget is immutable
-
app state, also known as
global state
.
-
ephemeral state, also known as
InheritedWidget
demo:
class RecipeWidget extends InheritedWidget {
final Recipe recipe;
RecipeWidget(Key? key, {required this.recipe, required Widget
child}) :
super(key: key, child: child);
// 包裝一個(gè)of(context:)方法來簡(jiǎn)化使用
@override
bool updateShouldNotify(RecipeWidget oldWidget) => recipe !=
oldWidget.recipe;
static RecipeWidget of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<RecipeWidget>()!;
}
// Then a child widget, like the text field that displays the recipe title, can just use:
RecipeWidget recipeWidget = RecipeWidget.of(context);
print(recipeWidget.recipe.label);
RiverPod
RiverPod
其實(shí)是Provider
的anagram(同字母異序單詞)
aims to:
- Easily access state from anywhere.
- Allow the combination of states.
- Enable override providers for testing.
Keypoints of Riverpod
- ProviderScope: The AppWidget must be wrapped in ProviderScope to use Riverpod.
-
Provider: A provider is a class that provides a value to other classes.
- Provider: 返回一個(gè)value
- StateProvider:
StateNotifierProvider
的簡(jiǎn)版, 與Provider的不同在于它提供了改變?nèi)〕鰜淼闹档姆椒?/li> - FutureProvider: future版Provider
- StreamProvider: when data comes in via streams and values change over time(e.g. connectivity of a device)
- StateNotifierProvider / NotifierProvider: listen to
StateNotifier
- AsyncNotifierProvider / AsyncNotifierProvider: listen to and expose a
Notification
- ChangeNotifierProvider
- Consumer: A consumer is a widget that listens to changes in a provider and rebuilds itself when the value changes.
- Ref: A ref is a reference to a provider. You use it to access other providers.
- Consumer: A consumer is a widget that listens to changes in a provider and rebuilds itself when the value changes. There are two types of consumers: Consumer and ConsumerWidget.
- Ref: A ref is a reference to a provider. You use it to access other providers. You can obtain a ref from providers and ConsumerWidgets.
Provider
// Provider
final myProvider = Provider((ref) { // 一個(gè)全局變量
return MyValue(); // 返回一個(gè)value
});
// State Provider
class Item {
Item({required this.name, required this.title});
final String name;
final String title;
}
final itemProvider = StateProvider<Item>((ref) => Item(name:
'Item1', title: 'Title1'));
// 支持用override來改變值
itemProvider.overrideWithValue(Item(name: 'Item2', title: 'Title2'));
// 取值
final item = ref.watch(itemProvider);
print(item.name);
// 既然是個(gè)State, 那么就是可以改變值, 有兩種方法
ref.read(itemProvider.notifier).state = Item(name: 'Item2',
title: 'Title2'); // 賦值
ref.read(itemProvider.notifier).update((state) => Item(name:
'Item3', title: 'Title3')); // update方法
// FutureProvider
final futureProvider = FutureProvider<Item>((ref) async {
return someLongRunningFunction(); // 多了個(gè)async, 但是沒有await
});
AsyncValue<Item> futureItem = ref.watch(futureProvider);
return futureItem.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (item) {
return Text(item.name);
},
);
// StateNotifierProvider is used to listen to changes in StateNotifier.
class ItemNotifier extends StateNotifier<Item> {
ItemNotifier() : super(Item(name: 'Item1', title: 'Title1'));
void updateItem(Item item) {
state = item;
}
}
final itemProvider = StateNotifierProvider<ItemNotifier,
Item>((ref) => ItemNotifier());
ref.read(itemProvider.notifier).updateItem(Item(name: 'Item2',
title: 'Title2')); // 自定義的updateItem方法
// NotifierProvider and AsyncNotifierProvider
// 與StateProvider的區(qū)別是notifyProvider需要調(diào)用自定義的update方法
class ItemNotifier extends Notifier<Item> {
// 還要提供一個(gè)build方法, 沒有構(gòu)造函數(shù)
@override
Item build(){
return Item(name: 'Item1', title: 'Title1');
}
void updateItem(Item item) {
state = item; // 或 = state.copyWith(name: 'Item2', title: 'Title2');
}
}
final itemNotifierProvider = NotifierProvider<ItemNotifier,
Item>(() => ItemNotifier());
ref.read(itemNotifierProvider.notifier).updateItem(Item(name:
'Item2', title: 'Title2')); // 自定義的方法
// ChangeNotifierProvider (AI填充的)
class ItemNotifier extends ChangeNotifier {
ItemNotifier() {
Item(name: 'Item1', title: 'Title1');
}
void updateItem(Item item) {
notifyListeners(); // 通知
}
}
final itemNotifierProvider = ChangeNotifierProvider<ItemNotifier>(
(ref) => ItemNotifier(),
);
ref.read(itemNotifierProvider.notifier).updateItem(Item(name:
'Item2', title: 'Title2'));
Streams
- sends data events for a listener to grab.
- There are two types of streams in Flutter: single subscription streams and broadcast streams.
- A single subscription stream can only be listened to once. (e.g. notify downloading progress)
- A broadcast stream allows any number of listeners. It fires when its events are ready, whether there are listeners or not.
- In Flutter, some key classes are built on top of
Stream
that simplify programming with streams. - When you create a stream, you usually use
StreamController
- A
sink
is a destination for data, add data to stream means add to sink
- A
final _recipeStreamController =
StreamController<List<Recipe>>();
final _stream = _recipeStreamController.stream;
// add data to a stream
_recipeStreamController.sink.add(_recipesList);
// close
_recipeStreamController.close();
// subscrib
// managing subscriptions manually.
StreamSubscription subscription = stream.listen((value) {
print('Value from controller: $value');
});
...
subscription.cancel();
// use a builder to automate (不需要手動(dòng)subscribe和cancel)
final repository = ref.watch(repositoryProvider);
return StreamBuilder<List<Recipe>>(
stream: repository.recipesStream(),
builder: (context, AsyncSnapshot<List<Recipe>> snapshot) {
// extract recipes from snapshot and build the view
} )
...
Database
sqlbrite
The sqlbrite library is a reactive stream wrapper around sqflite.
add dependencies:
dependencies:
...
synchronized: ^3.1.0 # Helps implement lock mechanisms to prevent concurrent access.
sqlbrite: ^2.6.0
sqlite3_flutter_libs: ^0.5.18 # Native sqlite3 libraries for mobile.
web_ffi: ^0.7.2 # Used for Flutter web databases.
sqlite3: ^2.1.0 # Provides Dart bindings to SQLite via dart:ffi
Drift
Drift is a package that’s intentionally similar to Android’s Room library. (ROM, 實(shí)體映射SQL)
drift: ^2.13.1
drift_dev: ^2.13.2 # drift generator
略
DevTools
- Flutter Inspector: Used to explore and debug the widget tree.
- Performance: Allows you to analyze Flutter frame charts, timeline events and CPU profiler.
- CPU Profiler: Allows you to record and profile your Flutter app session.
- Memory: Shows how objects in Dart are allocated, which helps find memory leaks.
- Debugger: Supports breakpoints and variable inspection on the call stack. Also allows you to step through code right within DevTools.
- Network: Allows you to inspect HTTP, HTTPS and web socket traffic within your Flutter app.
- Logging: Displays events fired on the Dart runtime and app-level log events.
- App Size: Helps you analyze your total app size.
VS Code里用
Dart: Open DevTools
, 可以在chrome里打開
VS Code 技巧
- preference > settings里用關(guān)鍵詞去搜多個(gè)特性
- .vscode > seting.json >
editor.lightbulb.enabled
顯示hint燈泡- 點(diǎn)燈泡彈出菜單的行為叫
code action
, 所以可以為它設(shè)置快捷鍵(具體是editor.action.codeAction
, 默認(rèn)是ctrl + shift + R
) -
cmd + .
一樣能觸發(fā)
- 點(diǎn)燈泡彈出菜單的行為叫