- 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
: 由多个Element
组成 -
RenderObjectElement
: 持有一个RenderObejct
-
- RenderObject: Drawing, layout, handle interactions
runApp() -> root widget -> build() -> widget tree -> element tree -> render object tree
先来点前菜, 随便看几个例子有如下笔记:
- 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那样, 用通知和观察机制, 但说不上谁更好. 通知机制需要在定义的时候就修改, setState机制不需要改定义的地方, 只要修改触发的地方
- SwiftUI的
padding
是一个修饰器, 意思是你先写视图, 我再来修饰你- 而Flutterr
Padding
则是一个组件, 而且是一个容器, 先写容器再写child - 同理, 你要align一个widget, 也是往外包装, 而不是写它的属性
- 而Flutterr
-
List
只接受count, 不接受数组- 有lazy特性, 不会实例化全部子组件
-
Material Design
的Card组件自带了圆角和阴影-
elevation
的意思是卡片离屏幕的高度(控制阴影)
-
- 固定的间距用
SizedBox
- 可充满parent的组件是
Expanded
, 类似SwiftUI里VStack后面给你自动加了个Spacer()
- 我一开始还以为是可以点击缩放的组件
- Flutter也有个独立的
Spacer
组件,Spacer = Expanded(child: Container())
- 给视图加交互特性跟给视图加padding是一样的, 要在外面包(
GestureDetector
)- 或者用各种Butotn
-
appBar
会自己加back - 检查暗黑模式
Theme.of(context).brightness == Brightness.light
-
Function
的定义里并不约束入参:Function func;
-
func(1)
,func('1')
,func(1, 2, '3')
都是可以的
-
- 主题:
Theme.of(context)
- 屏幕:
MediaQuery.of(context)
思考:
Padding(padding:child:)
和Container(padding:child:)
有什么区别?
实验下:
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上是没区别的, 在Widget树上层级也是一样的, 区别就在于Padding不能提供任何绘制, 只提供layout, 而Container则提供了绘制能力(比如背景色)
Dart
enum
- Dart的枚举是int类型, 从0开始
-
enum
是class
的子类, 所以可以定义方法 -
enum
可以定义values
属性, 返回所有枚举值 -
enum
可以定义index
属性, 返回枚举值对应的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';
}
}
// 简洁写法
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
是编译时常量,final
是运行时常量 -
const
可以用于class
的成员变量, 构造函数和构造函数的参数,final
不行
class Color {
final String label;
final Color color;
const Color(this.label, this.color)
}
copyWith
基本上等于定义了一个构造函数, 只不过入参都是可选的, 使用的时候全部先判断一下
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
- 构造函数
MyClass({Key? key}) : super(key: key);
, 简化为MyClass({super.key})
点击事件
一些博客里说Flutter中有三种方式提供点击事件: InkWell
, GestureDetector
, RaisedButton
(准确地说应该是各种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"),
)
其实还有更多:
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, 因为配置了图标和文字
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 // 跨平台打开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.
用起来很复杂:
- 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
(个人开发, Flutter团队接管)
import 'package:go_router/go_router.dart';
late final _router = GoRouter( // 注意是个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'; // 任意指定路径, 如果用外部存储的变量的话, 那么等于也是个动态路径了, 即当前取值是什么, 就能跳到那个页面
}
// no redirect
return null;
}
改造MaterialApp
的构造函数:
return MaterialApp.router( // 这里
debugShowCheckedModeBanner: false,
routerConfig: _router, // 三方库设置delegate, parser, provider等的地方
// TODO: Add Custom Scroll Behavior
title: 'Yummy',
scrollBehavior: CustomScrollBehavior(),
themeMode: themeMode,
theme: ...,
darkTheme: ..., );
- 跳页:
context.go('/home')
- 取路径参数:
state.pathParameters['id']
context
, state
都是GoRoute
的构造函数里builder的入参
GoRoute(
path: '/home/:id',
builder: (context, state) {
final id = state.pathParameters['id'];
return HomePage(id: id);
},
routes: [...] // 如果有子页面, 在这里配置
),
上图我们看到了route是可以嵌套的, 所以才叫"声明式"的, 跟UI树一样. 但就引出了一个问题, 如果要在别的tree里引用这个页面呢?
其实嵌套声明的结果就是路径能拼接起来, 所以只要路径拼接对, 就能引用到子页面.
弹出并跳到另一个页面, 是需要自己分别手写的
context.pop();
context.go('/${YummyTab.orders.value}');
登出后跳转到登录页面:
widget.auth.signOut().then((value) => context.go('/login'));
上面解释redirect
的时候提到过, 所以我们其实可以用redirect
来做登出后的跳转而不是手写跳转
跳页有两种写法:
context.go(path)
-
context.goNamed(name)
一般建议尽量用后面那种, 因为路径可能会变, 以及name
会检查错误, 写错了path
是不会检查的.
其它库:
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
用一个链接, 就能直接跳到某个页面:
- 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>
注册了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>
测试:
# 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:)
- 刷新时机:(insert into widget tree; ancestor nodes changes)
-
StatefulWidget: A widget that has mutable state.
- constructor -> createState() -> 反复调用
build(context:)
- initState(): 安卓的
onCreate()
, iOS的viewDidLoad()
- constructor -> createState() -> 反复调用
-
InheritedWidget: A widget that can pass data to its descendants. 能访问父层级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...
- 高级framework:
Riverpod
-
- State: The mutable state of a widget.
-
StatelessWidget:
All widgets are immutable(可变的是
state
)
永远不要在
build()
里做复杂的计算
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有点重合
- 事实上是
Positioned(left:top:right:bottom:child:)
这种类似于在元素内绝对定位的组件
一些需要注意的:
-
RotatedBox
属于哪一种组件? (根据quarterTurns
的值决定转多少个90度) - 上一行title, 下一行内容的结构居然也有个组件, 叫
ListTile(tilte:subtitle:)
- 后来发现跟iOS的
UITableViewCell
一样, 有leading
,title
,subtitle
,trailing
四个部分, 只不过它可以独立使用.
- 后来发现跟iOS的
- 圆头像也有组件:
CircleAvatar(radius:backgroundImage:)
-
AspectRatio(aspectRatio:child:)
里的比率指的是宽高比 -
Divider(thickness:height:color:)
, 对, 分割线也给你做好了 -
Container
用来group widgets, 但SwiftUI中Group是不生成View的
切换安卓和苹果主题:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
自定义组件内字体的主题颜色(apply
):
final textTheme = Theme.of(context).textTheme
.apply(
bodyColor: Colors.white, // 纯自定义
displayColor: Theme.of(context).colorTheme.onSurface, // 使用主题里定义好的别的颜色
);
Text(
'Hello World',
style: textTheme.titleMedium,
)
延伸 字体参考:
- 字体大小级别: Display > Headline > Title > Label > Body
- 每个级别下有: Large, Medium, Small
- 组合起来, 比如:
DisplayLarge
所以上文中更改字体颜色的bodyColor
, displayColor
就是分别针对的不同类别(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
里是显示不出来的
// 固定数目的写死children
ListView(
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
),
...,
],
)
// 用List.generate动态生成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只能在小区域内滚动
- 如果是List + List, 设置parent的
shrinkWrap
为true, 则parent的list高度会以childern的高度为高度(也即没有滚动区域)- 其实就是child的list有多高, 那么parent就把它包起来, 一起在parent的parent里滚动(如果parent不支持滚动那就是超出部分不可见)
- child需要设置的:
-
shrinkWrap
: true, 说明是固定高度的scrollable list -
primary
: false, 告知系统这个list不是主要的scrll view, 往外层找 -
physics
:NeverScrollableScrollPhysics
, 这个是双保险, 也是设置无需滚动(其它滚动效果, 如反弹, 翻页等也是这个属性)
-
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
: 指定列数. -
GridView.builder
: 滚动到的时候就build一个cell -
GridView.custom
: 使用sliverGridDelegate
来定制 -
GridView.extent
: 能定制每一行的列数
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数量而增高
physics: const NeverScrollableScrollPhysics(), // 不滚动, 意思是它的容器空间足够, 也意味着容器本身是可以滚动的, 所以子列表不需要在有限空间里滚动
);
}
注意
gridDelegate
, 跟iOS的delegate
还是有区别的, iOS的代理是一个实时计算的方法, 而flutter里显然更像一组options
, 或者说是一个colection的一组属性.
但是
SliverList
的delegate
是可以传入一个build方法的:
SliverChildBuilderDelegate((context, index) => widget, childCount:)
StaggeredGridView, 一个任意cell大小的grid view
DecorationBox
- 注意
DecorationBox
和BoxDecoration
的区别, 一个是widget, 一个是class -
BoxDecoration
is a class that provides a variety of properties for styling a box, such as color, border, and background image. - 一般要给视图一个背景图什么的, 与其自己去stack, flutter也给你提供了这个
DecorationBox
, 还是那句话, flutter给你提供的太多了, 我不认为这是好事
下例中, 原始代码是stack > image > text
, 因为发现文字在图片上, 有时候难以阅读, 就想给文字加渐变(加渐变就能让文字容易阅读? 我一般是选择描边或阴影), 这里就用到decoration组件了
看图:
- 装饰属性是用
BoxDecoation
来描述的, 这里是添加了渐变 - 装饰的是image, 而不是文字,
- 这里解释了我的疑问, 为什么给文字加渐变就能易读, 原来它是在图片和文字中间加了一个中心渐变的层, 就这么简单
Drawer
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: scaffoldKey, // 使用这个key
endDrawer: _buildEndDrawer(), // 左右两个抽屉
drawer: _buildDrawer(),
floatingActionButton: _buildFloatingActionButton(),
body: Center(
child: SizedBox(
width: constrainedWidth,
child: _buildCustomScrollView(),
),
),
)
// 触发:
scaffoldKey.currentState.openEndDrawer();
注意, 仍旧是Flutter的尿性, 你的Drawer必须是一个Drawer
, 任意容器Widget是不行的:
Widget _buildEndDrawer() {
return SizedBox(
width: drawerWidth,
child: Drawer( // Here
child: // 你真实的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;
});
}
看到了吗? 打开窗体, 获取选中值, 是用一个await
来实现的, 并不需要回调或代理什么的, 同理, 时间选择:
final picked = await showTimePicker(
context: context,
initialEntryMode: TimePickerEntryMode.input, // 默认是输入模式, 不是时钟模式
initialTime: selectedTime ?? TimeOfDay.now(),
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat: true, // 覆盖一个设备/媒体属性, 通通用24小时制(惯用手法, 用QediaQuery包了一层)
),
child: child!,
);
},
);
if (picked != null && picked != selectedTime) {
setState(() {
selectedTime = picked;
});
}
Dismissible
左滑删除也做了组件, 直接用就是了, 就是如果左滑出一个自定义的菜单, 那就得自行配置菜单项, 动画也得自己写了, 系统提供的是会清掉这个UI元素, 不要看触发的动作是一样的就以为也能通过别的配置支持 (看看flutter_slidable
)
// 比如原始视图是一个ListTile, 现在就包一层Dismissible, 在child前加如下代码:
key: Key(item.id), // unique id
direction: DismissDirection.endToStart,
background: Container(),
secondaryBackground: const SizedBox(
child: Row( // 其实就起个背景的作用
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.delete),
],
), ),
onDismissed: (direction) { // 从参数名就看出, 它是自动处理了ui的移除的
setState(() {
widget.cartManager.removeItem(item.id); // 同步数据源
});
widget.didUpdate();
},
Networking, Psesistence, State
save to:
- shared preferences
- sqflite
- file system
- cloud storage
Shared Preferences
- key-value
-
SharedPreferences
on Android,Userdefaults
on iOS - 不要存敏感数据
-
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');
这就是全部了, 注意一个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平台上debug, 因为每次启动端口不一样, 所以固定端口, 添加--web-port=8080
到flutter run
命令或Additional run args
配置项里
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', // 数据库名
version: 1, // 版本号
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
包提供 - 当然也可以手动写
fromJSON
和toJSON
, 但如果字段一多编写和维护就是个灾难,sjon_serializable
包就是帮你来生成这些代码的, 但是需要- 编写
fromJSON
和toJSON
的factory
占位方法(而不是真的去绑定字段) - 手动运行
dart run build_runner build
命令(这样会成成.part
为人的不, 包含了你第一步写的占位方法) - 如果不想每次手动运行, 就watch起来
dart run build_runner watch
- 编写
纯原生Demo
class Recipe {
final String uri;
final String label;
Recipe({this.uri, this.label});
}
// 在工厂方法里实现每个键和属性的映射
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';
// 也是声明, 这个文件build前不会不存在
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,
});
// 引导两个工厂方法去调用两个模板方法(build前会报错, 因为这两个方法还不存在)
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
里取出对应的键赋值给对应的属性
使用
// 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));
生产中, 会更多地基于freezed库来使用
Http Requests
上面有请求url并解析json的例子, 就像Android有Retrofit
, iOS有AlamFire
, 第三方的库通常会提供更模块化和线性的封装, 常用的库有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);
// 包装一个of(context:)方法来简化使用
@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
其实是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: 返回一个value
- StateProvider:
StateNotifierProvider
的简版, 与Provider的不同在于它提供了改变取出来的值的方法 - 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) { // 一个全局变量
return MyValue(); // 返回一个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);
// 既然是个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(); // 多了个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的区别是notifyProvider需要调用自定义的update方法
class ItemNotifier extends Notifier<Item> {
// 还要提供一个build方法, 没有构造函数
@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 (不需要手动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, 实体映射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里用关键词去搜多个特性
- .vscode > seting.json >
editor.lightbulb.enabled
显示hint灯泡- 点灯泡弹出菜单的行为叫
code action
, 所以可以为它设置快捷键(具体是editor.action.codeAction
, 默认是ctrl + shift + R
) -
cmd + .
一样能触发
- 点灯泡弹出菜单的行为叫