Riverpod2.x中的 Provider

原文来自:codewithandrea.com/articles/fl...

相关代码在:github.com/huangyuanlo...

是什么

Riverpod是一个响应式缓存和数据绑定框架,由Provider软件包演化而来。根据官方文档的说法,riverpod 是把 provider 重写了,实现了原来无法实现的功能。

为什么使用 riverpod

从设计上来说,Provider是对InheritedWidget的改进,因此它依赖于小部件树。 这是一个不幸的设计决策,可能会导致常见的ProviderNotFoundException 另一方面,Riverpod是编译时安全的,因为所有的 provider 都被声明为全局的,可以在任何地方访问,这也意味着这些 provider 可以不依赖于widget tree 来存储应用状态和业务逻辑。由于 Riverpod 是一个反应式框架,因此只需在需要时重建providers和widgets即可。

安装

3.0 已经有 preview 版本了,但还没有出正式版,这里使用稍微旧一点的版本

yaml 复制代码
dependencies:
  flutter_riverpod: ^2.4.10

为了简单起见,这里没有使用hooks_riverpod,至于使用注解来生成 provier,会在后续介绍。

使用

首先我们需要改造一下程序入口,提供一个ProviderScope

dart 复制代码
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

接着声明一个全局的Provider,和这个 main 方法平级就可以

dart 复制代码
final authorNameProvider = Provider<String>(
  (ref) => "xuan",
);

然后改造一下MyApp,让它可以读取到数据。改造前是继承StatelessWidget,这里需要改成继承自ConsumerWidget.

dart 复制代码
class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title:  Text('ProviderWidget')),
        body: Center(
          child: Text(value),
        ),
      ),
    );
  }
}

可以看到,继承ConsumerWidget之后,build方法的签名变了,多了一个WidgetRef对象,我们就是使用这个对象来读取、观察、监听各种 provider。

除了继承ConsumerWidget之外,还有其他方案可供使用

使用 Consumer

在旧代码中新增使用 riverpod,但又不打算大面积重构时,可以考虑这种方案:使用Consumer包裹需要更新的控件。

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget/third_part/riverpod/riverpod_widget.dart';
import 'package:widget_with_codeview/widget_with_codeview.dart';

class ProviderWithConsumer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ProviderWithConsumer"),
        centerTitle: true,
      ),
      body: WidgetWithCodeView(
        child: Column(children: [
          Text(
            "有一个包含复杂布局的大型小部件类,您可以使用 Consumer 来仅重新构建依赖于提供者的小部件\n创建小而可重用的小部件有利于组合,使代码更加简洁、性能更高,更易于理解。\n如果您遵循这一原则并创建小而可重用的小部件,那么大部分时间您将自然而然地使用 ConsumerWidget。",
            style: TextStyle(color: Colors.blue, fontSize: 16),
          ),
          Consumer(builder: (_, ref, __) {
            return Text(ref.read(helloWorldProvider));
          })
        ]),
        filePath: "lib/third_part/riverpod/provider_with_consumer.dart",
      ),
    );
  }
}

上述代码中的WidgetWithCodeView是三方库widget_with_codeview用来展示代码的控件。去除后没有影响。

使用ConsumerStatefulWidget

相对于ConsumerWidget来代替StatelessWidget,使用ConsumerStatefulWidget是用来代替StatefulWidget.

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget/third_part/riverpod/riverpod_widget.dart';
import 'package:widget_with_codeview/widget_with_codeview.dart';

class ProviderWithConsumerStatefulWidget extends ConsumerStatefulWidget {
  @override
  ConsumerState<ProviderWithConsumerStatefulWidget> createState() {
    return ProviderWithConsumerStatefulWidgetState();
  }
}

class ProviderWithConsumerStatefulWidgetState
    extends ConsumerState<ProviderWithConsumerStatefulWidget> {
  @override
  void initState() {
    super.initState();
    // 3. if needed, we can read the provider inside initState
    final helloWorld = ref.read(helloWorldProvider);
    print(helloWorld); // "Hello world"
  }

  @override
  Widget build(BuildContext context) {
    // 4. use ref.watch() to get the value of the provider
    final helloWorld = ref.watch(helloWorldProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text("ProviderWithConsumerStatefulWidget"),
        centerTitle: true,
      ),
      body: WidgetWithCodeView(
        child: Column(
          children: [
            Text(
              "ConsumerStatefulWidget可以用来代替StatefulWidget\n当我们从ConsumerState进行子类化时,我们可以在所有小部件生命周期方法中访问ref对象。这是因为ConsumerState声明WidgetRef作为属性,类似于Flutter的State类声明BuildContext作为可以在所有小部件生命周期方法中直接访问的属性。",
              style: TextStyle(color: Colors.blue, fontSize: 16),
            ),
            Text(helloWorld),
          ],
        ),
        filePath:
            "lib/third_part/riverpod/provider_with_consumerStatefulWidget.dart",
      ),
    );
  }
}

同样的需要使用ConsumerState代替原来的State.需要注意的是,在这里面我们并没有看到WidgetRef,但我们依旧可以在整个页面声明周期内访问WidgetRef对象的实例ref,这是因为ConsumerStateWidgetRef声明为属性。

WidgetRef是什么

官方文档定义WidgetRef为允许Widgets和Provider交互的对象。可以用来观察(watch)Provider 值的改变,也可以作为ConsumerConsumerWidget的参数,同样也可以作为ConsumerState的属性。 将WidgetRefBuildContext做一下比较:我们可以通过BuildContext在 Widget tree 中访问祖先节点,比如Theme.of(context)MediaQuery.of(context)WidgetRef可以让我们在 app 中访问任意provider,前提是provider被声明为全局的。这也是设计的初衷。 当然,将 provider 声明在类内部,只允许该类访问在编译、运行时也不会报错,但这会影响关注点分离的原则。

八中不同的 Provider

  • Provider
  • StateProvider (被NotifierProvider代替)
  • StateNotifierProvider (被NotifierProvider代替)
  • FutureProvider
  • StreamProvider
  • ChangeNotifierProvider (被NotifierProvider代替)
  • NotifierProvider (Riverpod 2.0新增)
  • AsyncNotifierProvider (Riverpod 2.0新增)

以后会提到使用riverpod_generator包中的@riverpod注解来代替我们的手动声明provider

Provider

上面示例中的代码就是用的这一个,更适合提供一些不会变的常量值,比如 Dio 实例、版本号、格式化对象、数据库对象等

dart 复制代码
final dateFormatterProvider = Provider<DateFormat>((ref) {
  return DateFormat.MMMEd();
});

class SomeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // retrieve the formatter
    final formatter = ref.watch(dateFormatterProvider);
    // use it
    return Text(formatter.format(DateTime.now()));
  }
}

StateProvider

可以用来存储一些简单的变量,比如计数器

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final globalStateProvider = StateProvider((ref) => 1);
class StateProviderDemoWidget extends ConsumerWidget {
  //不推荐
  final localStateProvider = StateProvider((ref) => 1);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text("StateProviderDemoWidget"),
        centerTitle: true,
      ),
      body: Center(
          child: Flex(
        direction: Axis.vertical,
        children: [
          Text("globalStateProvider count ${ref.watch(globalStateProvider)}"),
          Text("localStateProvider count ${ref.watch(localStateProvider)}"),
          Text(
            "StateProvider.state标记为过时,并将在3.0移除",
            style: TextStyle(color: Colors.red, fontSize: 14),
          ),
          Text("声明为全局,则状态一直被保存,直到应用重启。",
              style: TextStyle(color: Colors.blue, fontSize: 14)),
          Text("声明为类成员,则页面重启则重新初始化。",
              style: TextStyle(color: Colors.blue, fontSize: 14)),
          Text("StateProvider适用于存储简单的状态变量,如枚举、字符串、布尔值和数字",
              style: TextStyle(color: Colors.blue, fontSize: 14)),
          IconButton(
            icon: Icon(Icons.plus_one_sharp),
            onPressed: () {
              ref.read(globalStateProvider.notifier).state++;
              ref.read(localStateProvider.notifier).state++;
            },
          ),
        ],
      )),
    );
  }
}

这里有一些有意思的事情。我们声明了两个 Provider,一个是全局的,一个是类内部的。 当我们打开页面点击几次按钮之后,页面内容更新,这时候我们返回上一页再次打开这个页面,会发现globalStateProvider提供的值还是上一次返回时候的值,而localStateProvider提供的值是初始值。因为localStateProvider在页面关闭时就被销毁了,这种情况似乎更符合我们没有使用 riverpod 的习惯。这里可以使用 autoDispose 让globalStateProvider在所有 watcher 被销毁也可以重置。

StateNotifierProvider

可以用它来监听和导出一个 StateNotifier ,StateNotifierProviderStateNotifier 非常适合管理因事件或用户交互而改变的状态。这里有一个 todolist示例。 我们先定义一个 todo 的对象,实现一个 copyWith 方法来复制当前对象。

dart 复制代码
@immutable
class Todo {
  const Todo(
      {required this.id, required this.description, required this.completed});

  final String id;
  final String description;
  final bool completed;
  Todo copyWith({String? id, String? description, bool? completed}) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }
}

然后我们继承StateNotifier实现自己的TodosNotifier类,并完善所需逻辑

  1. 统计已完成的待办
  2. 添加待办
  3. 删除待办
  4. 完成、取消完成待办
  5. 全选,反全选
  6. 是否选中全部
dart 复制代码
class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);

  void addTodo(Todo todo) {
    state = [...state, todo];
  }

  int completedCount() {
    int count = 0;
    for (final todo in state) {
      if (todo.completed) {
        count++;
      }
    }
    return count;
  }

  void removeTodo(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }

  void toggle(String todoID) {
    state = [
      for (final todo in state)
        if (todo.id == todoID)
          todo.copyWith(completed: !todo.completed)
        else
          todo
    ];
  }

  void selectAll() {
    state = [for (final todo in state) todo.copyWith(completed: true)];
  }

  void unSelectAll() {
    state = [for (final todo in state) todo.copyWith(completed: false)];
  }

  bool isSelectAll() {
    bool result = true;
    for (final todo in state) {
      if (!todo.completed) {
        result = false;
        break;
      }
    }
    return result;
  }
}

接着声明一个全局的StateNotifierProvider

dart 复制代码
final todoProvider =
    StateNotifierProvider<TodosNotifier, List<Todo>>((ref) => TodosNotifier());

最后我们就可以在页面中使用了

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class StateNotifierProviderWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    List<Todo> todos = ref.watch(todoProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text("State Notifier Provider Widget"),
        centerTitle: true,
      ),
      body: Column(
        children: [
          Text("StateNotifierProvider和StateNotifier非常适合管理可能会因事件或用户交互而发生变化的状态。"),
          Text("注意StateNotifier中通知更新时是比较的对象内存地址"),
          Divider(),
          Container(
            child: Text(
                "共${todos.length.toString()},完成-》${ref.read(todoProvider.notifier).completedCount()}"),
          ),
          CheckboxListTile(
            value: ref.read(todoProvider.notifier).isSelectAll(),
            onChanged: (value) {
              if (value ?? false) {
                ref.read(todoProvider.notifier).selectAll();
              } else {
                ref.read(todoProvider.notifier).unSelectAll();
              }
            },
            title: Text("全选"),
          ),
          Expanded(
              child: ListView(
            shrinkWrap: true,
            children: [
              for (final todo in todos)
                Dismissible(
                  key: Key(todo.id),
                  child: CheckboxListTile(
                    value: todo.completed,
                    onChanged: (value) =>
                        ref.read(todoProvider.notifier).toggle(todo.id),
                    title: Text(todo.description),
                  ),
                  onDismissed: (direction) =>
                      {ref.read(todoProvider.notifier).removeTodo(todo.id)},
                ),
            ],
          )),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          final notifier = ref.read(todoProvider.notifier);
          notifier.addTodo(Todo(
              id: DateTime.now().microsecond.toString(),
              description: "${DateTime.now().microsecondsSinceEpoch}",
              completed: false));
        },
      ),
    );
  }
}

这样我们就完成了一个简易版的待办页面

FutureProvider

我们经常会遇到一些耗时操作,比如请求服务器接口获取到数据后展示在页面上;比如读取配置文件等。这种情况我们就需要 FutureProvider了。 这里以读取 assets 中的配置文件为例: 首先声明一个FutureProvider泛型中填入返回值,使用rootBundle.loadString来读取资源文件的内容。

dart 复制代码
final getConfigFutureProvider =
    FutureProvider<Map<String, dynamic>>((ref) async {
  String configContant =
      await rootBundle.loadString("assets/fake_configurations.json");
  final configMap = jsonDecode(configContant) as Map<String, dynamic>;
  return configMap;
});

然后再声明一个用来模拟请求服务端的FutureProvider

dart 复制代码
final wordPairProvider = FutureProvider<String>((ref)  async{
  final wordPair = generateWordPairs().first;
  await Future.delayed(Duration(seconds: 2));
  return "${wordPair.first} - ${wordPair.second}";
});

注意这里的generateWordPairs是三方包english_words: ^4.0.0提供的。在wordPairProvider中我们延迟 2 秒返回了结果,用以模拟网络耗时。 在页面中我们可以这么使用:

dart 复制代码
class FutureProviderWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<Map<String, dynamic>> getConfigResult =
        ref.watch(getConfigFutureProvider);
    final AsyncValue<String> wordPairResult = ref.watch(wordPairProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text("FutureProvider"),
        centerTitle: true,
      ),
      body: RefreshIndicator(
        onRefresh: () => ref.refresh(wordPairProvider.future),
        child: ListView(
          children: [
            getConfigResult.when(
                data: (data) {
                  return Text(data.toString());
                },
                error: (error, stack) {
                  return Text(error.toString());
                },
                loading: () => const CircularProgressIndicator()),
            wordPairResult.when(data: (data) {
              return Text(data);
            }, error: (error, stack) {
              return ElevatedButton(
                  onPressed: () {
                    ref.refresh(wordPairProvider.future);
                  },
                  child: Text("点击刷新"));
            }, loading: () {
              return CircularProgressIndicator();
            }),
          ],
        ),
      ),
    );
  }
}

注意,ref.watch(FutureProvider)的返回值是AsyncValue类型,我们可以使用模式匹配的方式来更新UI。这里会有三个状态:data,loading,error。 我们在页面上添加了一个下拉刷新的控价来刷新模拟网络请求的 provider:ref.refresh(wordPairProvider.future)

  • FutureProvider功能非常强大,可以用它来
  • 执行和缓存异步操作(如网络请求)
  • 处理异步操作的错误和加载状态
  • 将多个异步值合并为另一个值
  • 重新获取和刷新数据(适用于拉到刷新操作) 如果退出页面需要重新进行刷新,我们可以配合autoDispose使用:FutureProvider.autoDispose<String>((ref) async {}

StreamProvider

这里例子不大好写,它和 FutureProvider 类似,被 watch 时返回的也是 AsyncValue 类型,这里是一个计时器的示例。 同样的,先声明一个StreamProvider

dart 复制代码
final streamProvider = StreamProvider.autoDispose<int>((ref) {
    ref.onDispose(() {
      debugPrint("streamProvider autoDispose");
    });

    return Stream.periodic(Duration(seconds: 1), (number) {
      return number + 1;
    });
  });

然后在页面中使用

dart 复制代码
class StreamProviderWidget extends ConsumerWidget {

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<int> streamResult = ref.watch(streamProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text("StreamProviderWidget"),
        centerTitle: true,
      ),
      body: Column(
        children: [
          Text(streamResult.toString()),
          streamResult.when(
                  data: (data) {
                    return Text(data.toString());
                  },
                  error: (error, stack) {
                    return Text(error.toString());
                  },
                  loading: () => CircularProgressIndicator())
        ],
      ),
    );
  }
}

ChangeNotifierProvider

建议使用 NotifierProvider 代替。 这里是一个计数器示例。

dart 复制代码
class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() {
    _count++;
    notifyListeners(); // 当计数器值改变时通知所有监听者
  }
}

// 定义ChangeNotifierProvider
final counterProvider = ChangeNotifierProvider<CounterNotifier>((ref) {
  return CounterNotifier();
});

class ChangeNotifierProviderWidget extends ConsumerWidget{
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return  Scaffold(
        appBar: AppBar(title: Text('ChangeNotifierProviderWidget')),
        body: Center(
          child: Consumer(builder: (context, ref, _) { // 使用Consumer读取并显示状态
            final counter = ref.watch(counterProvider);
            return Text('Count: ${counter.count}');
          }),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // 在点击按钮时,通过ref调用notifier的方法更新状态
            ref.read(counterProvider.notifier).increment();
          },
          child: Icon(Icons.add),
        ),
      );
  }
}

NotifierProvider

用来代替 StateProvider 和 ChangeNotifierProvider。 同样的,我们先定义一个数据类 Student

dart 复制代码
class Student {
  Student({required this.name, required this.id});
  String name;
  String id;

  Student copyWith({String? copyID, String? copyName}) {
    return Student(name: copyName ?? this.name, id: copyID ?? this.id);
  }
  String toString(){
    return "$id : $name";
  }
}

定义一个继承自 Notifier 的类

dart 复制代码
class StudentNotifier extends Notifier<Student> {
  @override
  Student build() {
    return generateStudent();
  }

  Student generateStudent() {
    final wordPair = generateWordPairs().first;
    final id = Random().nextInt(100);

    return Student(
        name: "${wordPair.first} - ${wordPair.second}", id: id.toString());
  }

  String toString() {
    return "${state.id} : ${state.name}";
  }

  void change() {
    state = generateStudent();
  }

  void changeNameWithCopy(String name) {
    state = state.copyWith(copyName: name);
  }

  void justChangeName(String name) {
    state.name = name;
  }

  void changeIdWithCopy(String id) {
    state = state.copyWith(copyID: id);
  }

  void justChangeId(String id) {
    state.id = id;
  }
}

注意,这里的 build 方法是必须的。同时为了解释刷新页面的条件,这里定义了几个改变属性的方法。xxxWithCopy是重新生成了对象,justxxx 则是在原对象上直接修改属性值。 然后声明NotifierProvider,这里提供了两种方式,哪一种都可以。

dart 复制代码
final counterProvider = NotifierProvider<StudentNotifier, Student>(() {
  return StudentNotifier();
});
final counterProviderOther =
    NotifierProvider<StudentNotifier, Student>(StudentNotifier.new);

在页面中我们这样使用

dart 复制代码
class CounterNotifierWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final student = ref.watch(counterProvider);
    ref.listen(counterProvider, (oldValue, newValue) {
      logger.d(
          "oldValue--> ${oldValue?.id.toString()} \n newValue--> ${newValue.id}");
    });

    return Scaffold(
      appBar: AppBar(
        title: Text("CounterNotifierWidget"),
        centerTitle: true,
      ),
      body: Column(
        children: [
          Text("student ${student.id.toString()} : ${student.name}  --> ${student.toString()}"),

          ElevatedButton(
              onPressed: () {
                ref
                    .read(counterProvider.notifier)
                    .changeIdWithCopy(Random().nextInt(100).toString());
              },
              child: Text("changeIdWithCopy")),
          ElevatedButton(
              onPressed: () {
                final wordPair = generateWordPairs().first;
                ref.read(counterProvider.notifier).changeNameWithCopy(
                    "${wordPair.first} - -${wordPair.second}");
              },
              child: Text("changeNameWithCopy")),

          ElevatedButton(
              onPressed: () {
                ref
                    .read(counterProvider.notifier)
                    .justChangeId(Random().nextInt(100).toString());
              },
              child: Text("justChangeId")),
          ElevatedButton(
              onPressed: () {
                final wordPair = generateWordPairs().first;
                ref
                    .read(counterProvider.notifier)
                    .justChangeName("${wordPair.first} - -${wordPair.second}");
              },
              child: Text("justChangeName")),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).change();
        },
        child: Icon(Icons.change_circle),
      ),
    );
  }
}

运行之后可以看到,只有当我们调用xxxwithCopy时才会刷新页面。戳到NotifierProviderElement源码中查看updateShouldNotify方法,发现是调用的identical(previous, next)方法来做判断的。

dart 复制代码
  @protected
  bool updateShouldNotify(State previous, State next) {
    return !identical(previous, next);
  }

AsyncNotifierProvider

相比NotifierProvider,它是异步而已. 还是先声明一个数据类Student

dart 复制代码
class Student{
  Student({required this.name,required this.id});
  String name;
  String id;

  String toString(){
    return "$id - $name";
  }
}

声明一个继承自AsyncNotifier的类

dart 复制代码
class StudentAsyncNotifier extends AsyncNotifier<Student>{
  @override
  FutureOr<Student> build() async {

      return getInfo();
  }

 Future<Student> getInfo()async{
    
    await  Future.delayed(Duration(seconds: 3));
    final wordPair = generateWordPairs().first;
    return Student(name: "${wordPair.first} - ${wordPair.second}", id: Random().nextInt(100).toString());
    
  } 

  Future<void> refresh() async{
      state = AsyncValue.loading();
      state = await AsyncValue.guard(()async {
        return getInfo();
      });
  }
}

声明一个 Provider

dart 复制代码
final studentAsyncNotifier = AsyncNotifierProvider<StudentAsyncNotifier, Student>(() {
  return StudentAsyncNotifier();
});

在页面中使用

dart 复制代码
class AsyncNotifierProviderWidget extends ConsumerWidget{
  @override
  Widget build(BuildContext context, WidgetRef ref) {

    final asyncValue = ref.watch(studentAsyncNotifier);

    return Scaffold(
      appBar: AppBar(title: Text("AsyncNotifierProviderWidget"),centerTitle: false,),
      body: asyncValue.when(data: (data){
       return Center(child: Text(data .toString()),);
      }, error: (error,stack){
       return Center(child: Text(error.toString()),);
      }, loading: (){
        return Center(child: CircularProgressIndicator());
      }),
      floatingActionButton: FloatingActionButton(onPressed: (){
        ref.read(studentAsyncNotifier.notifier).refresh();
      },child: Icon(Icons.refresh),),
    );
  }
}

这里需要注意的是:使用AsyncNotifier与autoDispose的正确方式是继承 AutoDisposeAsyncNotifier类而不是使用 autoDispose 修饰符。

以上就是 riverpod 中常见的 Provider 的用法示例,后面会介绍一些修饰符、注解相关的信息。


以上

相关推荐
ALLIN15 小时前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei15 小时前
Flutter 国际化
flutter
Dabei16 小时前
Flutter MQTT 通信文档
flutter
Dabei19 小时前
Flutter 中实现 TCP 通信
flutter
孤鸿玉19 小时前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter
前端 贾公子21 小时前
《Vuejs设计与实现》第 16 章(解析器) 上
vue.js·flutter·ios
tangweiguo030519871 天前
Flutter 数据存储的四种核心方式 · 从 SharedPreferences 到 SQLite:Flutter 数据持久化终极整理
flutter
0wioiw01 天前
Flutter基础(②④事件回调与交互处理)
flutter
肥肥呀呀呀1 天前
flutter配置Android gradle kts 8.0 的打包名称
android·flutter
吴Wu涛涛涛涛涛Tao1 天前
Flutter 实现「可拖拽评论面板 + 回复输入框 + @高亮」的完整方案
android·flutter·ios