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: 由多個(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 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那樣, 用通知和觀察機(jī)制, 但說不上誰更好. 通知機(jī)制需要在定義的時(shí)候就修改, setState機(jī)制不需要改定義的地方, 只要修改觸發(fā)的地方
  • SwiftUI的padding是一個(gè)修飾器, 意思是你先寫視圖, 我再來修飾你
    • 而FlutterrPadding則是一個(gè)組件, 而且是一個(gè)容器, 先寫容器再寫child
    • 同理, 你要align一個(gè)widget, 也是往外包裝, 而不是寫它的屬性
  • 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),)
    ),
  ],
)
image.png

如上圖, 在UI上是沒區(qū)別的, 在Widget樹上層級(jí)也是一樣的, 區(qū)別就在于Padding不能提供任何繪制, 只提供layout, 而Container則提供了繪制能力(比如背景色)

Dart

enum

  • Dart的枚舉是int類型, 從0開始
  • enumclass的子類, 所以可以定義方法
  • 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
}

constfinal

  • 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;
  • 構(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*):

  • 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"),
)

其實(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 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 // 跨平臺(tái)打開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

用起來很復(fù)雜:

  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 (個(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)

跳頁有兩種寫法:

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

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 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:)
      • 刷新時(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()
    • 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.

All widgets are immutable(可變的是state)

永遠(yuǎn)不要在build()里做復(fù)雜的計(jì)算

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有點(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ú)立使用.
  • 圓頭像也有組件: 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]]專題吧

\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: 指定列數(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的一組屬性.

但是SliverListdelegate是可以傳入一個(gè)build方法的:
SliverChildBuilderDelegate((context, index) => widget, childCount:)

StaggeredGridView, 一個(gè)任意cell大小的grid view

DecorationBox

  • 注意DecorationBoxBoxDecoration的區(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組件了

image.png

看圖:

  1. 裝飾屬性是用BoxDecoation來描述的, 這里是添加了漸變
  2. 裝飾的是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 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');

這就是全部了, 注意一個(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=8080flutter 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

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

  • 前面一截encode, decode部分由dart:convert包提供
  • 后面一截serialize, deserialize部分由json_serializable包提供
  • 當(dāng)然也可以手動(dòng)寫fromJSONtoJSON, 但如果字段一多編寫和維護(hù)就是個(gè)災(zāi)難, sjon_serializable包就是幫你來生成這些代碼的, 但是需要
    1. 編寫fromJSONtoJSONfactory占位方法(而不是真的去綁定字段)
    2. 手動(dòng)運(yùn)行dart run build_runner build命令(這樣會(huì)成成.part為人的不, 包含了你第一步寫的占位方法)
    3. 如果不想每次手動(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
    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);
  
  // 包裝一個(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:

  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: 返回一個(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.
    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 (不需要手動(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ā)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,367評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,001評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,213評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,535評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,317評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,868評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,963評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,090評(píng)論 0 285
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,599評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,549評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,712評(píng)論 1 367
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,233評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,353評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,607評(píng)論 1 281
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,321評(píng)論 3 389
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,686評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容