flutter text 最大长度_Flutter状态State的5种应对方法

1195bfb86a77963f0b9d76950f9738fa.png 作者 | Ryan Edge 译者 | 王强 策划 | 李俊辰

本文最初发布于 poetryincode.dev 网站,经原作者授权由 InfoQ 中文站翻译并分享。

通过下方链接查看本文中示例的代码:

https://github.com/chimon2000/flutter_state_five_ways

不管你是刚开始了解 Flutter,还是已经接触 Flutter 有了一段时间,你都可能已经知道有很多方法可以处理应用程序状态。我可以肯定,每个月都会冒出来一些新的途径。因为没有太多可以直接对比的例子,所以想要了解这些方法之间的差异和各自的权衡可能会很困难。

我认为,学习如何使用一个库的最佳方法是用它来构建项目。对比两个库的最佳方法是用它们来执行相同的任务:构建相同的功能,从而更好地了解彼此的权衡所在。

在本文中,我将使用同样的应用程序作为基础,用我很喜欢的几位作者制作的 5 个库,使用相同的模式来实现共享状态。这些库有的很流行,有的罕见,有的很新,有的相对老旧。

  1. Riverpod(带有 StateNotifier)

  2. Bloc

  3. flutter_command

  4. MobX

  5. binder

我会尽力找出各个库之间的差异,并对每种方法做一个总结。为了演示各个库的 API,我们将实现一个笔记应用,它会显示一个输入字段以创建新的笔记,还会显示已创建笔记的列表。你可以在这里观看演示效果:

https://flutter-state.codemagic.app/

在开始之前我想澄清一下,就算我可能会批评其中的某些库,但它们仍然值得大家使用。我发现所有这些库都可以为你完成大部分工作,而且我的观点会有一些主观偏见。选择它们本来也是个人喜好的结果。同样,本文没有介绍的库也并不是不好。我的观点也不是真理,而且就算我的看法是正确的,我也没那么勤奋来选出所有好用的库来。另外,这毕竟是一篇博客文章,不是什么大部头。

准备工作 如果你决定跟我一起研究,请创建一个新的 Flutter 应用来测试这几种方法:
flutter create state_examples
要运行这个应用的时候,请在项目根目录中执行 run 命令。
flutter run
我们将在示例中重用一些类,因此接下来会定义它们。

// A simple helper function to allow us to immutably add to lists.
extension ImmutableList on List {List concat(T item) => List.from([...this, item]);
}

// A simple widget for displaying individual notes.class Note extends StatelessWidget {final String text;const Note({Key key, this.text}) : super(key: key);@overrideWidget build(BuildContext context) {return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Text('Note: $text'),
    );
  }
}

// You can add equatable as a dependency to your pubspec.yaml
// https://pub.dev/packages/equatable/install
class NotesState extends Equatable {
  final List<String> notes;
  final String input;
  NotesState(
    this.notes,
    this.input,
  );
  @override
  List<Object> get props => [notes, input];
  @override
  bool get stringify => true;
  NotesState copyWith({
    List<String> notes,
    String input,
  }) {
    return NotesState(
      notes ?? this.notes,
      input ?? this.input,
    );
  }
  NotesState.initial()
      : notes = [],
        input = '';
}
我们还希望重构 main.dart 文件,以便轻松换掉页面。
void main() {
  runApp(App());
}
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        // home: SomePage(), //This is where our page will go,
      ),
    );
  }
}
Riverpod

Riverpod 文档:

https://riverpod.dev/

Riverpod 是 Remi Rousselet 创建的一个相当新的库,也是他对自己另一个库 provider 遇到的一些最常见问题的直接回应,其中很重要的一个问题是当开发人员无法提供依赖项时会引发 ProviderNotFound 异常。Riverpod 解决了这个问题,还有更简单的 API,我觉得这是它的两个最大优势。

与其他选项相比,我的看法是如果你只使用 StateNotifier,那么用起来就没那么头疼,因为你要处理的是一个简单的 API,这个 API 实现了 MVU 模式,还有一大堆文档支持。如果你正在使用完整的 API,我觉得你会遇到的麻烦会比其他选项平均来说高很多。。

Riverpod 实践 要开始使用 Riverpod,请将这个库安装为依赖项:
dependencies:
  # ...other dependencies
  flutter_riverpod: ^0.12.1
  riverpod: ^0.12.1
接下来,将 ProviderScope 添加到应用的根 / 入口点:
void main() {
  // For widgets to be able to read providers, we need to wrap the entire
  // application in a "ProviderScope" widget.
  // This is where the state of our providers will be stored.
  runApp(ProviderScope(child: App()));
}
然后,要创建和更改状态,我们将创建一个 NotesController,它扩展一个 StateNotifier 和一个 StateNotifierProvider,来存储对 NotesController 的引用,并将其提供给我们的应用程序和设置一些初始状态:
// We create a "provider", which will store a reference to NotesController.
final notesProvider = StateNotifierProvider((ref) => NotesController());
class NotesController extends StateNotifier<NotesState> {
  NotesController() : super(NotesState.initial());
  void addNote() {
    var notes = state.notes.concat(state.input);
    state = state.copyWith(notes: notes, input: '');
  }
  void updateInput(String input) => state = state.copyWith(input: input);
}
现在,我们可以在 BuildContext 和 Consumer 上使用 read 扩展方法来访问应用中任何位置的 NotesController。read 扩展使我们能够执行突变函数,而 Consumer 小部件使我们能够订阅状态更改。这是在 Riverpod 中实现的 Notes UI:
class RiverpodPage extends StatefulWidget {
  const RiverpodPage({Key key}) : super(key: key);
  @override
  _RiverpodPageState createState() => _RiverpodPageState();
}
class _RiverpodPageState extends State<RiverpodPage> {
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My notes app')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            FlatButton(
                onPressed: () {
                  // Get a reference of the NotesController
                  // and add a note.
                  context.read(notesProvider).addNote();
                  _controller.clear();
                },
                child: Text('Create Note')),
            TextField(
              controller: _controller,
              // Get a reference of the NotesController
              // and update the input value.
              onChanged: (value) =>
                  context.read(notesProvider).updateInput(value),
              decoration: InputDecoration.collapsed(hintText: 'Add a note'),
            ),
            Divider(),
            Expanded(
              child: Consumer(
                builder: (context, watch, child) {

                  // Subscribe to the NotesController's state
= var state = watch(notesProvider.state);
                  return ListView.builder(
                    itemBuilder: (context, index) =>
                        Note(text: state.notes[index]),
                    itemCount: state.notes.length,
                  );
                },
              ),
            )
          ],
        ),
      ),
    );
  }
}

最后,我们将反注释 home: SomePage(),并使用上面定义的小部件换掉 SomePage。

评价

这个示例可能会让大家觉得 Riverpod 用起来平平无奇,但这是设计使然。Riverpod 是一个功能非常强大的库,提供了状态管理和依赖项注入。本文介绍的其他选项并不是都有这样的能力。但是强大的力量意味着更大的……算了不说了。这里的权衡是,这种能力造就了一个相当大的 API,这可能会令人生畏。Riverpod 有丰富的文档支持,但是你可能会发现自己要在文档里翻来翻去,具体取决于要学习的内容。不要因为这个就放弃使用它,因为大多数用例只需使用 StateNotifier 就能解决了。

Riverpod 非常顺滑,也许有点难以入门,但它解决了 provider 这个库的主要问题。我目前没在使用它,但是如果我开始了一个项目,并且很熟悉 provider,需要在两者之间做出选择,那么我会毫不犹豫地选择 Riverpod。

  Bloc  

Bloc 文档:

https://bloclibrary.dev/

Bloc 库是这个列表中最古老的一员,足足有两年的历史!它最初是由 Felix Angelov 创建的,是一种基于事件驱动的 Bloc 模式,最近它经过了重构,以同时支持事件驱动和函数驱动的状态更新。这听起来似乎是微不足道的变化,但是作为在多个框架中用过事件驱动模式的程序员,我可以肯定这种改变是很有价值的。我已经见识过了事件驱动的大型应用程序中会冒出来多少样板和技术债。能够选择何时使用这种模式是非常重要的。

Bloc 有两种类型的状态对象:Blocs 是事件驱动的,继承自 Cubits,后者是函数驱动的。两者都继承自 Stream,并使开发人员免受其某些复杂性的影响。实质上,它们的区别就这些了。由于 Blocs 是 Cubits 的扩展,因此生态系统的其余部分(BlocProvider、BlocListener、BlocBuilder 等)都可以被两者共享。

Bloc 也是这个列表上所有库中文档做得最好的榜样。它的整个 API 都在 bloclibrary.dev 上提供了文档支持,以及你可能要解决的每一个重要问题的示例,从使用 RESTful/GraphQL/Firebase API 到处理导航,无所不包。他们的文档写得太好了,怎么夸都不为过。

与其他选项相比,我想说的是这个库用起来要轻松一些,因为你要处理的是一个简单的 API,这个 API 实现了 MVU 模式,有一大堆文档支持。

Bloc 实践 要开始使用 Bloc,请将这个库安装为依赖项:
dependencies:
  # ...other dependencies
  bloc: ^6.1.0
  flutter_bloc: ^6.0.6
接下来,要创建和更改状态,我们将使用来自 Bloc 的 Cubit,使用一些初始状态初始化我们的类,并创建几个突变器(mutator)函数:
class NotesCubit extends Cubit<NotesState> {
  NotesCubit(): super(NotesState.initial());

  void addNote() {
    emit(state.copyWith(notes: state.notes.concat(state.input), input: ''));
  }
  void updateInput(String input) => emit(state.copyWith(input: input));
}
我们将使用 BlocProvider 类在整个应用中共享我们的 NotesCubit。为了保持一致,我们将在小部件树中 App 的上方添加 BlocProvider。
void main() {
  // For widgets to be able to read providers, we need to wrap the entire
  // application in a "BlocProvider" widget.
  // This is where the state of our NotesCubit will be stored.
  runApp(BlocProvider(create: (_) => NotesCubit(), child: App()));
}
现在,我们可以在 BuildContext 和 BlocBuilder 上使用 bloc 扩展方法来访问应用中的 NotesCubit。这是在 Bloc 中实现的 Notes UI:
class BlocPage extends StatefulWidget {
  const BlocPage({Key key}) : super(key: key);
  @override
  _BlocPageState createState() => _BlocPageState();
}
class _BlocPageState extends State<BlocPage> {
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My notes app: Bloc')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            FlatButton(
                onPressed: () {
                  // Get a reference of the NotesCubit
                  // and add a note.
                  context.bloc().addNote();
                  _controller.clear();
                },
                child: Text('Create Note')),
            TextField(
              controller: _controller,// Get a reference of the NotesCubit // and update the input value.
              onChanged: (value) =>
                  context.bloc().updateInput(value),
              decoration: InputDecoration.collapsed(hintText: 'Add a note'),
            ),
            Divider(),// Subscribe to the NotesCubit's state
            BlocBuilder(
              builder: (context, state) => Expanded(
                child: ListView.builder(
                  itemBuilder: (context, index) =>
                      Note(text: state.notes[index]),
                  itemCount: state.notes.length,
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

同样,我们将反注释 home: SomePage()(如果尚未反注释),并用上面定义的小部件换掉 SomePage。

评价

实际上,我经手的几个项目已经从 provider 迁移到了 Bloc。这不是说 Riverpod 就不行,因为用 Bloc 重构要比 Riverpod 简单得多。此外,在使用 Firebase 的情况下,由于 Firebase 和 Bloc 都依赖 Streams,因此我们重构起来会很简单。

目前来说,我发现自己使用 Bloc 的效率最高,因为它没那么复杂,而且文档很丰富。再次强调,我能充分理解 API 是非常值得夸奖的优点。

我不会说未来五年内我都会继续使用 Bloc,但是对我来说,这绝对是现在的合适之选。

flutter_command

flutter_command 文档:

https://pub.dev/packages/flutter_command

flutter_command 实际上是 Thomas Burkhart 对 rx_command 库的重新构想。两者之间的主要区别在于,前者基于 ValueNotifier 和 ValueListenableBuilder 构建,而后者基于 Streams 构建。两者都是从.Net 的 ReactiveCommands 派生的,因此如果你熟悉这个模式,也许你会发现自己很容易就能上手这个库。

与其他选项相比,设置大致上没什么区别,而如果你从未使用过 ReactiveCommands,我觉得它用起来会更难一些。

flutter_command 实践 要开始使用 flutter_command,请将这个库安装为依赖项:
dependencies:
  # ...other dependencies
  flutter_command: ^0.9.3
接下来,要创建和改变状态,我们将创建一个 NotesViewModel 类来定义我们的状态,以及来自 flutter_command 的 Command(s),用于突变这个状态:
class NotesViewModel {
  NotesState state = NotesState.initial();
  Command<String, String> inputChangedCommand;
  Command<String, NotesState> updateNotesCommand;
  NotesViewModel() {
    inputChangedCommand = Command.createSync((x) => x, '');
    updateNotesCommand = Command.createSync((input) {
      state = state.copyWith(notes: state.notes.concat(input));
      inputChangedCommand.execute('');
      return state;
    }, NotesState.initial());
  }
}
现在,我们可以使用创建的 NotesViewModel 订阅状态(使用 ValueListenableBuilder),并执行 inputChangedCommand 和 updateNotesCommand 来突变状态。这是在 flutter_command 中实现的 Notes UI:
class FlutterCommandPage extends StatefulWidget {
  const FlutterCommandPage({Key key}) : super(key: key);
  @override
  _FlutterCommandPageState createState() => _FlutterCommandPageState();
}
class _FlutterCommandPageState extends State<FlutterCommandPage> {
  final _notesViewModel = NotesViewModel();
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My notes app')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            FlatButton(
                onPressed: () {
                  // Execute updateNotesCommand
                  // to add a note.
                  _notesViewModel.updateNotesCommand
                      .execute(_notesViewModel.inputChangedCommand.value);
                  _controller.clear();
                },
                child: Text('Create Note')),
            TextField(
              controller: _controller,
              // Execute inputChangedCommand
              // to update the input value.
              onChanged: _notesViewModel.inputChangedCommand,
              decoration: InputDecoration.collapsed(hintText: 'Add a note'),
            ),
            Divider(),
            // Use ValueListenableBuilder
            // to subscribe to state.
            ValueListenableBuilder(
              valueListenable: _notesViewModel.updateNotesCommand,
              builder: (context, state, _) => Expanded(
                child: ListView.builder(
                  itemBuilder: (context, index) =>
                      Note(text: state.notes[index]),
                  itemCount: state.notes.length,
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

我们将反注释 home: SomePage()(如果尚未操作),并使用上面定义的小部件换掉 SomePage。

评价

我对这个库的感受很复杂。对于熟悉其概念的人来说,它可能是一个完美的选择。对于像我这样来自 React/Angular 社区的人来说,它并不会给人浑然天成的感受。文档来说,应付这个示例是足够了。

我可能不会使用 flutter_command,但是我觉得这个库在别人手中的效果和列表中的其他库相比并不会落下风。我肯定会向已经熟悉这些模式的人,或想尝试一些异国风情的人们推荐它。

 MobX 

MobX 文档:

https://mobx.netlify.app/

MobX.dart 已经出来一段时间——快两年了!只要你有一定 React 经验,肯定会立刻知道 MobX.dart 是 MobX.js 的一个端口。概念是相同的,只是实践中有所区别:你需要使用代码生成来复现运行时在后台通过动态语言(如 JavaScript)执行的神奇功能。

与其他选项相比,我的看法是如果你从未用过可观察对象(observable),那么这个库设置起来要比其他选项复杂一些,并且用起来会更难一点。

MobX 实践 要开始使用 MobX,请将这个库安装为依赖项:
dependencies:
  # ...other dependencies
  mobx: ^1.2.1+4
  flutter_mobx: ^1.1.0+2
dev_dependencies:
  build_runner: ^1.10.4
  mobx_codegen: ^1.1.1+3
接下来,要创建和更改状态,我们将使用来自 mobx 的一个 Store,使用一些初始状态初始化我们的类,并创建几个突变器函数:
part 'notes.g.dart';
class Notes = NotesBase with _$Notes;
abstract class NotesBase with Store {
  @observable
  String input = '';
  @observable
  ObservableList<String> notes = ObservableList();
  @action
  void updateInput(String val) {
    input = val;
  }
  @action
  void addNote() {
    notes.add(input);
    input = '';
  }
}
你可能会马上注意到到处都是红色的花体。这是因为我们尚未生成 store 所依赖的文件。我们运行以下命令来做到这一点。
flutter packages pub run build_runner build
然后,我们可以使用 flutter_mobx 中的 Observer 和 store 中定义的动作来引用和更新状态。这是在 mobx 中实现的 Notes UI:
class MobxPage extends StatefulWidget {
  const MobxPage({Key key}) : super(key: key);
  @override
  _MobxPageState createState() => _MobxPageState();
}
class _MobxPageState extends State<MobxPage> {
  var _notesStore = Notes();
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My notes app')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            FlatButton(
                onPressed: () {
                  // Execute addNote action
                  // to add a note.
                  _notesStore.addNote();
                  _controller.clear();
                },
                child: Text('Create Note')),
            TextField(
              controller: _controller,
              // Execute updateInput action
              // to update the input value.
              onChanged: _notesStore.updateInput,
              decoration: InputDecoration.collapsed(hintText: 'Add a note'),
            ),
            Divider(),

            // Use Observer to subscribe
            // to updates to the NotesStore.
            Observer(
              builder: (_) => Expanded(
                child: ListView.builder(
                  itemBuilder: (context, index) =>
                      Note(text: _notesStore.notes[index]),
                  itemCount: _notesStore.notes.length,
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

同样,我们将反注释 home: SomePage()(如果尚未操作),并用上面定义的小部件换掉 SomePage。

评价

尽管我从未在 JavaScript 中使用过 mobx,但由于它们的本质是相同的,因此对我来说它的概念很浅显易懂。它简化了很多事情这一点我很喜欢,虽然我发现装饰器 + 构建生成用起来比较奇妙。我个人还认为,依赖状态管理层的构建生成可能会很烦人。在 UI 层之后,我发现这是自己更改最多的层。

mobx 最大的优势就是它的易用性和完善的文档。它在这份列表中仅次于 Bloc,因为你应该能从 mobx.netlify.app 了解所需的一切。

 binder 

binder 文档:

https://pub.dev/packages/binder

刚诞生不久的 binder 是由 Romain Rastel 创造的。它受到了 Recoil 和 Riverpod 的深刻影响,并试图创建一个易于学习且 API 表面积较小的库。

与其他选项相比,我觉得这个库的上手难度要低很多,因为你要处理的是非常小的 API,而这个 API 还有着丰富的文档支持。

binder 实践 要开始使用 binder,请将这个库安装为依赖项:
dependencies:
  # ...other dependencies
  binder: ^0.1.5
接下来,将 BinderScope 添加到应用的根 / 入口点:
void main() {
  // For widgets to be able to read app state, we need to wrap the entire
  // application in a "BinderScope" widget.
  runApp(BinderScope(child: App()));
}
为了创建一些状态,我们将使用一个来自 binder 的 StateRef,并设置一些初始状态:
// We create a "state ref", which will store a reference to NotesState.
final notesRef = StateRef(NotesState.initial());
接下来,要更改状态,我们将创建一个 Logic 类和一个来自 binder 的 LogicRef:
// We create a "logic ref", which will store a reference to NotesViewLogic.
final notesViewLogicRef = LogicRef((scope) => NotesViewLogic(scope));
class NotesViewLogic with Logic {
  const NotesViewLogic(this.scope);
  @override
  final Scope scope;
  void addNote() {
    var state = read(notesRef);
    write(notesRef,
        state.copyWith(notes: state.notes.concat(state.input), input: ''));
  }
  void updateInput(String input) =>
      write(notesRef, read(notesRef).copyWith(input: input));
}
现在,我们可以使用 BuildContext 上的 read、use 和 watch 扩展方法来访问应用程序中任何位置的 StateRef 和 LogicRef。这是在 binder 中实现的 Notes UI:
class BinderPage extends StatefulWidget {
  const BinderPage({Key key}) : super(key: key);
  @override
  _BinderPageState createState() => _BinderPageState();
}
class _BinderPageState extends State<BinderPage> {
  TextEditingController _controller;
  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    // Subscribe to NotesState
    final state = context.watch(notesRef);
    return Scaffold(
      appBar: AppBar(title: Text('My notes app')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            FlatButton(
                onPressed: () {
                  // Get a reference of NotesViewLogic
                  // and add a note.
                  context.use(notesViewLogicRef).addNote();
                  _controller.clear();
                },
                child: Text('Create Note')),
            TextField(
              controller: _controller,
              // Get a reference of NotesViewLogic
              // and update the input value.
              onChanged: (value) =>
                  context.use(notesViewLogicRef).updateInput(value),
              decoration: InputDecoration.collapsed(hintText: 'Add a note'),
            ),
            Divider(),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) => Note(text: state.notes[index]),
                itemCount: state.notes.length,
              ),
            )
          ],
        ),
      ),
    );
  }
}

同样,我们将反注释 home: SomePage()(如果尚未操作),并用上面定义的小部件换掉 SomePage。

评价

binder 是很新的库,在本文发表前一周在刚刚发布。因此,指望这个库与其他库具有相同级别的文档是不公平的。就它的当前状态来说,文档已经很够用了。另外,由于 binder 从 provider 和 Riverpod 借鉴了很多东西,以尽可能接近 Recoil,所以我很容易就可以上手并用起来这个库。

我对这个库寄予很高的期望,并期待看到更多示例来凸显其优点。如果你没什么选择可用,那么它通过减小 API 表面积可以提供与 Riverpod 相似的优势。

    总结    

公平起见,我想重申一下,我的偏好是很主观的,因为它们是基于我个人的经验。另外,这是一个基础示例,其中不包含副作用 / 异步性或测试,因此没有充分利用这些库的功能。

希望本文能帮助大家基本了解如何使用这些库完成相同的任务,并看明白它们之间的异同。尽管我有自己的偏好,但我敢肯定,你使用这个列表中的任何选项都可以做得很好。

作者介绍

我叫 Ryan Edge,是 Flutter 项目的谷歌开发人员专家、Superformula 的软件工程师和半专业的开源贡献者。

延伸阅读

https://poetryincode.dev/flutter-state-5-ways

23827b2b69adad108e867304b67c9647.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值