Flutter Apprentice 4th edition笔记

2024-08-20  本文已影响0人  walkerwzy

\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}

Three Trees

runApp() -> root widget -> build() -> widget tree -> element tree -> render object tree

先来点前菜, 随便看几个例子有如下笔记:

思考:

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

// 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

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,
    );
  }
}

技巧

点击事件

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

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

路由

Navigator 1.0

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, 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

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:

Platform Channels

Widgets 概念

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.

按作用分:

一些需要注意的:

切换安卓和苹果主题:

自定义组件内字体的主题颜色(apply):

final textTheme = Theme.of(context).textTheme
        .apply(
            bodyColor: Colors.white, // 纯自定义
            displayColor: Theme.of(context).colorTheme.onSurface, // 使用主题里定义好的别的颜色
        );
Text(
  'Hello World',
  style: textTheme.titleMedium,
)

延伸 字体参考:

所以上文中更改字体颜色的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

// 固定数目的写死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

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 _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

下例中, 原始代码是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

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

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

纯原生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

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

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

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

VS Code里用Dart: Open DevTools, 可以在chrome里打开

VS Code 技巧

上一篇下一篇

猜你喜欢

热点阅读