Flutter Apprentice 4th edition笔记

\tt \begin{array}{l} \tt Flutter\\ \tt Architeture \end{array} \begin{cases} \tt Top \begin{cases} \begin{array}{l} \tt Framework\\ \small \tt(Dart) \end{array} \begin{cases} \tt UI\ Theme-\small Cuperitino\ /\ Material \\ \tt Widgets-Styling,\ Text,\ Controls \\ \tt Rending-Layout\ abstraction \\ \tt Foundation(dart\tt:ui)-Paiting,\ Animation,\ Gestures \\ \end{cases} \\ \begin{array}{l} \tt Pluging\\ \end{array} \\ \end{cases} \\ \tt Engine(C/C\texttt{++}) \\ \tt Embered(Platform\ specific) \end{cases}

  • 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 by createState() method)
  • setState(() {}) trigger a rebuild of the widget tree (by rerun build())
    • The Flutter framework has been optimized to make rerunning build methods fast
    • 而不是像rxSwift或SwiftUI那样, 用通知和观察机制, 但说不上谁更好. 通知机制需要在定义的时候就修改, setState机制不需要改定义的地方, 只要修改触发的地方
  • SwiftUI的padding是一个修饰器, 意思是你先写视图, 我再来修饰你
    • 而FlutterrPadding则是一个组件, 而且是一个容器, 先写容器再写child
    • 同理, 你要align一个widget, 也是往外包装, 而不是写它的属性
  • 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),)
    ),
  ],
)
image.png

如上图, 在UI上是没区别的, 在Widget树上层级也是一样的, 区别就在于Padding不能提供任何绘制, 只提供layout, 而Container则提供了绘制能力(比如背景色)

Dart

enum

  • Dart的枚举是int类型, 从0开始
  • enumclass的子类, 所以可以定义方法
  • 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
}

constfinal

  • 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;
  • 构造函数MyClass({Key? key}) : super(key: key);, 简化为MyClass({super.key})

点击事件

一些博客里说Flutter中有三种方式提供点击事件: InkWell, GestureDetector, RaisedButton(准确地说应该是各种button*):

  • InkWellMaterial组件, 所以只能在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 extends WidgetsApp.
  • 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.

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 // 声明式路由语法糖包
  1. A user taps a button.
  2. The button handler tells the app state to update.
  3. The router is a listener of the state.
  4. Based on the new state changes, the router reconfigures the list of pages for the navigator.
  5. The navigator detects if there’s a new page in the list and handles the transitions to show the page.
    image.png

用起来很复杂:

  1. you must create your RouterDelegate, bundle your app state logic with your navigator and configure when to show each route.
  2. To support the web platform or handle deep links, you must implement RouteInformationParser to parse route information.
  3. 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.(三方库解君愁) \Rarr 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来做登出后的跳转而不是手写跳转

跳页有两种写法:

  1. context.go(path)
  2. 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

用一个链接, 就能直接跳到某个页面:

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 a RouteMatchList.
  • 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()
    • 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.

All widgets are immutable(可变的是state)

永远不要在build()里做复杂的计算

Asynchronous code should ALWAYS check if the mounted property is true before calling setstate(), 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四个部分, 只不过它可以独立使用.
  • 圆头像也有组件: 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]]专题吧

\tt Scrolling \begin{cases} \tt wrap Box \begin{cases} \tt ListView \\ \tt GridView \\ \tt PageView \end{cases} \\ \tt wrap Sliver \gets \tt CustomScrollView \\ \tt mixture \larr \tt NestedScrollView \end{cases}

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的一组属性.

但是SliverListdelegate是可以传入一个build方法的:
SliverChildBuilderDelegate((context, index) => widget, childCount:)

StaggeredGridView, 一个任意cell大小的grid view

DecorationBox

  • 注意DecorationBoxBoxDecoration的区别, 一个是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组件了

image.png

看图:

  1. 装饰属性是用BoxDecoation来描述的, 这里是添加了渐变
  2. 装饰的是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 or iOS 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=8080flutter 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

\tt JSON\ string \xrightleftharpoons[decode]{encode} \tt Map \xrightleftharpoons[serialize]{deserialize} \tt Object

  • 前面一截encode, decode部分由dart:convert包提供
  • 后面一截serialize, deserialize部分由json_serializable包提供
  • 当然也可以手动写fromJSONtoJSON, 但如果字段一多编写和维护就是个灾难, sjon_serializable包就是帮你来生成这些代码的, 但是需要
    1. 编写fromJSONtoJSONfactory占位方法(而不是真的去绑定字段)
    2. 手动运行dart run build_runner build命令(这样会成成.part为人的不, 包含了你第一步写的占位方法)
    3. 如果不想每次手动运行, 就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
    1. 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, the createState() method gets called,
    • InheritedWidget is a built-in class allowing child widgets to access its data
      • by calling context.dependOnInheritedWidgetOfExactType<class>().
    • InheritedWidget is immutable
    1. app state, also known as global state.

InheritedWidgetdemo:

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:

  1. Easily access state from anywhere.
  2. Allow the combination of states.
  3. 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.
    1. A single subscription stream can only be listened to once. (e.g. notify downloading progress)
    2. 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
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 + .一样能触发
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容