前端工程师的第一个Flutter应用

强烈建议想搞Flutter的朋友,读一遍《Flutter实战》,这是Flutter中文网出的一本书,主要以入门、进阶、实例三大部分进行叙述Flutter。是我目前为数不多的成体系的Flutter中文学习指南。

更新

2019/8/28
Flutter厉害在有渲染引擎直接调用底层绘制,但是用Dart写出来的代码难看且没有可读性。布局全靠嵌套,当然这是性能的代价。

前言


人生苦短,少学一样是一样。 ----鲁迅

曾经我把鲁迅的这句名言作为座右铭,时时刻刻铭记于心。
可是没想到上了前端这条贼船之后,我幸福的留下了泪水,从 jQuery 到 AngularJS,到 Vue、React,跨端的 Weex、RN,最近又开始鼓吹 Flutter 浪潮。

公司内部孵化一个创业项目,需要做 Android 和 iOS 端。我有一个绝佳的 idea,就差一个程序员???在技术选型阶段,从需求复杂度、需求开发周期、成本上考虑我们决定直接由前端组负责这个 App 的开发。接下来就是前端多端框架的选择,综合上手成本、性能、组件库、流行度等因素,最终选择了 uni-app作为我们的多端框架。

多端框架的对比,可以看我转的一篇文章:小程序框架全面测评
这是京东凹凸实验室,做的一份全面的测评,从各方面分析了页面多端框架的现况。

但随着业务的发展,长表单、动画、个性化功能的增加,uni-app 在性能和定制化方面渐渐满足不了产品的需求。我决定调研一下 Flutter。这也是这篇文章的由来,我的第一个 Flutter 应用。

Flutter


what is Flutter?

Flutter 是谷歌的移动 UI 框架,用于在创纪录的时间内在 iOS 和 Android 上制作高质量的原生界面。 Flutter 与现有代码一起使用,由世界各地的开发人员和组织使用,并且是免费和开源的。

why is Flutter?

为什么使用 Flutter?
摸着良心说可能有一部分原因是对 Flutter 比较好奇。但是随着对Flutter的了解,很好奇为什么Weex、RN、uni-app为什么不能像Flutter一样,也搞一套自绘引擎?Flutter算然在性能上有优势,但他的语法、生态跟Web圈子(语法脱离了JS,生态脱离了npm)是脱节的。
这就导致我在使用Flutter的过程中,需要很多新轮子,感觉很浪费时间。
如果Weex、RN、uni-app能有一套自绘引擎,会不会是更好的一个选择呢?

  • 高性能自绘引擎
    对我来说,这是我选择 Flutter 最重要的一个理由。

  • 同时支持 JIT 和 AOT

    Flutter 使用 Dart 语法开发。开发阶段 JIT 模式即时编译,提高开发效率。发布阶段 AOT 模式提前编译,提升应用性能。

  • 开发友好,得益于 JIT

    嗯,热重载。这个。。。可能原生开发会比较爽。作为一个页面仔,前端工程基本都是所见即所得。

  • Dart:强类型语言

    支持类型检查,编译前提前发现错误。

介绍

仓库地址:cnode_flutter
在这里插入图片描述

环境安装

flutter-io.cn是 Flutter 官方的中文站点
安装说明:https://flutter-io.cn/docs/get-started/install
flutterchina.club是 Flutter 中文开发者社区的开源项目。
安装说明:https://book.flutterchina.club/chapter1/install_flutter.html

目录结构

新建完Flutter工程后,有一个默认的计数器Demo,代码在lib/main.dart 文件中。
接下来我们大部分的工作都在lib目录下完成。

cnode_flutter
	|-- android
	|-- build
	|-- ios
	|-- lib
		|-- model
			|-- model.dart // provider的model
		|-- pages
			|-- article.dart // 详情
			|-- drawer.dart // 抽屉
			|-- home.dart // 列表
		|-- services
			|-- apis.dart // httpPath
			|-- index.dart // httpAction
		|-- main.dart
	|-- test
	.
	.
	.

入口页面:lib/main.dart

知识点:

  1. package:provider/provider.dart状态管理
  2. package:flutter/material.dartUI组件应用
  3. pub资源包使用
  • 引入资源
  • pub是Flutter的资源管理器,类似于node的npm。
// material 组件库
import 'package:flutter/material.dart';
// 列表页部件
import 'package:cnode_flutter/pages/home.dart';
// provider组件
import 'package:provider/provider.dart';
// model
import './model/model.dart';
  • 添加应用入口
// 应用入口
void main() => runApp(MyApp());
  • 创建Material应用,设置首页
// 应用入口
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      // 状态共享 https://book.flutterchina.club/chapter7/provider.html
      providers: [
        ChangeNotifierProvider(builder: (_) => Counter()),
      ],
      // Consumer 消费者 https://book.flutterchina.club/chapter7/provider.html
      // 这里强行用了一下~ 作为示例而
      child: Consumer<Counter>(
        builder: (context, counter, _) {
          /// [Consumer]可以通过[counter]访问到[Counter]这个model下的状态
          print(counter);
          // MaterialApp 是Material库中提供的Flutter APP框架
          // https://docs.flutter.cn/flutter/material/MaterialApp-class.html
          return MaterialApp(
            // 应用名称
            title: 'CNode',
            // 主题
            theme: ThemeData(
              // 定义主题色 Colors 是MaterialApp中的颜色部件,里面定义了很多颜色
              primaryColor: Colors.blue,
            ),
            // 首页
            home: Home(),
          );
        },
      ),
    );
  }
}

列表页面:lib/pages/home.dart

知识点:

  1. 可滑动列表ListView
  2. 上拉加载新数据;
  3. 文字过长省略显示;
  4. 路由跳转;
  5. 列表中的子项Card布局;
  • 创建状态组件
// 首页(列表) 继承 StatefulWidget(有状态模型?)
class Home extends StatefulWidget {
  // Home({Key: key}) :super(Key key);

  @override
  _HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {}
  • 创建页面
class _HomeState extends State<Home> {
  // Scaffold 部件的key
  static GlobalKey<ScaffoldState> _globalKey = new GlobalKey();
  // List 不免的key
  static GlobalKey<ListState> _listKey = new GlobalKey();

  @override
  Widget build(BuildContext context) {
    // 页面脚手架 https://docs.flutter.cn/flutter/material/Scaffold-class.html
    return Scaffold(
      // 部件的key主要用来提升diff算法性能,跟前端概念中的key是类似的
      // https://my.oschina.net/u/4082889/blog/3031508
      key: _globalKey,
      appBar: new AppBar(
        title: const Text('list'),
        leading: IconButton(
          icon: const Icon(Icons.menu),
          onPressed: () {
            // Scaffold.of(context).openDrawer();
            _globalKey.currentState.openDrawer();
          },
          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
        ),
      ),
      // new抽屉实例,并将更新列表的方法传递给drawer页面调用(也可以用eventbus)
      drawer: new HomeDrawer(getListFn: () {
        _listKey.currentState.curPage = 1;
        _listKey.currentState.getListFn(
            loadMoreBool: false,
            tab: Provider.of<Counter>(context).tab,
            page: 1);
      }),
      body: new List(key: _listKey),
    );
  }
}
  • 创建页面中的列表内容
// 产生列表widge
class List extends StatefulWidget {
  List({Key key}) : super(key: key);
  @override
  ListState createState() => new ListState();
}

class ListState extends State<List> {
  var list = <dynamic>['loading']; // 数据数组
  var curPage = 1; // 当前页数
  var loadingBool = false; // 是否正在加载中,避免多次请求阻塞
  ScrollController _controller = ScrollController(); // list scroll controller

  /// 通过http请求获取列表数据
  /// [loadMoreBool]:是否是加载更多 示例:true
  /// [tab]:话题类型 示例:good
  /// [page]:第几页 示例:1

  // ListState() {}
  @override
  void initState() {
    super.initState();
    curPage = 1;
    getListFn(loadMoreBool: false, tab: '', page: curPage);
  }

  @override
  void dispose() {
    //内存泄露,可以调用_controller.dispose,释放
    // _controller.dispose();
    super.dispose();
  }

  // _ListState({Key:key}):super(Key:key)
  Widget build(BuildContext context) {
    // list scroll controller
    _controller.addListener(() async {
      // 获取页面长度 和 当前滚动条所在位置
      var maxScroll = _controller.position.maxScrollExtent;
      var pixels = _controller.position.pixels;

      // 滑动到底部加载更多
      if (!loadingBool && maxScroll == pixels) {
        /// [loadingBool] 正在加载中状态,避免重复请求
        loadingBool = true;
        await getListFn(
            loadMoreBool: true,
            tab: Provider.of<Counter>(context).tab,
            page: curPage);
        loadingBool = false;
      }
    });

    // 列表
    // ListView部件说明:https://book.flutterchina.club/chapter6/listview.html
    return ListView.builder(
      /// 总长度,例如为50,第一屏显示五项,那么[itemBuilder]会创建第一屏需要的部件,而不是将列表中的50个部件都创建出来
      itemCount: list.length,
      padding: const EdgeInsets.only(top: 0, left: 0, right: 0, bottom: 20),
      // 按需创建部件
      itemBuilder: (BuildContext _context, int i) {
        // 如果这一项为 String,带着这一项是特殊的部件,比如 loading(加载中)、noMore(没有更多)、none(暂无数据)
        if (list[i] is String) {
          if (list[i] == 'loading') {
            // 部件:加载中
            return Container(
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.center,
              child: SizedBox(
                  width: 24.0,
                  height: 24.0,
                  child: CircularProgressIndicator(strokeWidth: 2.0)),
            );
          } else if (list[i] == 'noMore') {
            // 部件:没有更多
            return Container(
                alignment: Alignment.topCenter,
                padding: EdgeInsets.all(16.0),
                child: Text(
                  "没有更多了",
                  style: TextStyle(color: Colors.grey),
                ));
          } else if (list[i] == 'none') {
            // 部件:暂无数据
            return Container(
                alignment: Alignment.topCenter,
                padding: EdgeInsets.all(16.0),
                child: Text(
                  "暂无数据",
                  style: TextStyle(color: Colors.grey),
                ));
          }
        }
        // 创建item部件,并返回给列表
        return buildItem(list[i]);
      },
      controller: _controller,
    );
  }
  • 从http接口中获取数据:
// http apis
class Apis {
  // get /topics 主题首页
  static const String topicList = '$_domain/topics';
}

// http actions
class HttpActions {
  // 获取话题列表
  static Future getTopicList(
      {int limit = 20, int page, bool mdrender = false, String tab}) {
    return Dio().get(
        '${Apis.topicList}?mdrender=$mdrender&limit=$limit&page=$page&tab=$tab');
  }
}

  /// 调用http请求获取列表数据
  /// [loadMoreBool] Bool 加载更多标志
  /// [tab] String 主题分类。目前有 ask share job good
  /// [page] Number 页数
  Future getListFn({bool loadMoreBool, String tab, int page}) {
    // print('$loadMoreBool,$tab,$page');
    return HttpActions.getTopicList(page: page, tab: tab).then((res) {
      var data = res.data['data'];
      var l = data.length;
      setState(() {
        if (loadMoreBool) {
          // 加载更多逻辑
          if (l > 0) {
            // 有数据,向list中添加新数据
            curPage++;
            list.insertAll(list.length - 1, data);
          } else {
            // 无数据,向list中添加'noMore'标识
            list[list.length - 1] = 'noMore';
          }
        } else {
          // 第一次获取数据逻辑
          // 清楚list原有数据
          list = <dynamic>['loading'];
          // 滚动列表页到顶部
          _controller.animateTo(.0,
              duration: Duration(milliseconds: 300),
              curve: Curves.easeInOutExpo);
          if (l > 0) {
            // 有数据,向list中添加新数据
            list.insertAll(list.length - 1, data);
            curPage++;
          } else {
            // 无数据,向list中添加'noMore'标识
            list[list.length - 1] = 'none';
          }
        }
      });
    });
  }

抽屉页面:lib/pages/drawer.dart

知识点:

  1. HomeDrawer抽屉的使用;
  2. 点击抽屉里面 tab标签 切换 列表页面 内容;
  3. 手势部件的使用ListenerGestureDetector
import 'package:cnode_flutter/services/index.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model/model.dart';

class HomeDrawer extends StatefulWidget {
  final getListFn;

  HomeDrawer({this.getListFn});
  _HomeDrawerState createState() => new _HomeDrawerState();
}

class _HomeDrawerState extends State<HomeDrawer> {
  var userInfo = <String, dynamic>{
    'avatar_url': '',
    'loginname': '北京吴彦祖',
    'score': '0',
  };

  // 获取用户信息
  void getUserInfoFn() async {
    var res = await HttpActions.getUserInfo();
    setState(() {
      userInfo = res.data['data'];
    });
  }

  @override
  // 生命周期钩子
  void initState() {
    super.initState();
    print('drawer initState');
    // 获取用户信息
    getUserInfoFn();
  }

  @override
  // 生命周期钩子
  void dispose() {
    print('drawer dispose');
    super.dispose();
  }

  Widget build(BuildContext context) {
    // Drawer 抽屉部件 https://docs.flutter.cn/flutter/material/Drawer/Drawer.html
    return new Drawer(
      child: Column(children: generateListFn(context)),
    );
  }

  // 生成抽屉列表部件
  List<Widget> generateListFn(context) {
    var children = <Widget>[];
    // 添加用户信息部件
    children.add(generateUserBoxFn(userInfo, context));
    // 根据数组信息,生成可以点击的tab分类
    [
      {'label': '全部', 'id': '', 'icon': Icons.border_all},
      {'label': '精华', 'id': 'good', 'icon': Icons.thumb_up},
      {'label': '分享', 'id': 'share', 'icon': Icons.share},
      {'label': '问答', 'id': 'ask', 'icon': Icons.question_answer},
      {'label': '招聘', 'id': 'job', 'icon': Icons.work},
    ].forEach((item) {
      /// 依次将 按钮部件 推入[children]
      children.add(
        ListTile(
          title: new Text(item['label']),
          leading: Icon(item['icon']),
          trailing: Icon(Icons.keyboard_arrow_right),
          selected: item['id'] == Provider.of<Counter>(context).tab,
          onTap: () {
            /// 通过调用[rovider.of<Counter>]的change方法,来改变tab的值
            Provider.of<Counter>(context).change(item['id']);
            // 这里没有将 item['id'] 传递下去,是为了强行体现一下 provider 的作用:)
            widget.getListFn();
          },
        ),
      );
    });
    return children;
  }
}

// 生成用户信息盒子的方法
Widget generateUserBoxFn(userInfo, context) {
  return Container(
      // 内边距
      padding: EdgeInsets.only(top: 60, right: 20, bottom: 10, left: 20),
      // Container 部件颜色
      color: Colors.blue,
      child: Column(
        children: <Widget>[
          // 第一行:头像,夜间模式
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              // 头像
              userInfo['avatar_url'].length > 0
                  ? CircleAvatar(
                      backgroundImage: NetworkImage(userInfo['avatar_url']),
                      backgroundColor: Colors.blue,
                      radius: 20,
                    )
                  : new Icon(
                      Icons.person,
                      size: 40,
                      color: Colors.white,
                    ),
              // 夜间模式
              Listener(
                child: new Icon(Icons.brightness_2),
                onPointerDown: (PointerDownEvent event) {
                  print(event);
                  // 弹窗 配置如key名称所示,title:标题,titlePadding:标题的内边距,等等等
                  showDialog(
                    context: context,
                    builder: (BuildContext context) => SimpleDialog(
                      title: Text("提示"),
                      titlePadding: EdgeInsets.all(10),
                      backgroundColor: Colors.white,
                      elevation: 5,
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(6))),
                      children: <Widget>[
                        ListTile(
                          title: Center(
                            child: Text("女朋友召唤,来不及写了。"),
                          ),
                        ),
                      ],
                    ),
                  ).then<void>((value) {
                    // The value passed to Navigator.pop() or null.
                    print(value);
                  });
                },
              ),
            ],
          ),
          // 第二行:昵称、注销按钮
          Padding(
            padding: EdgeInsets.only(top: 20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    // 昵称、积分
                    Container(
                      height: 20,
                      child: new Text(
                        userInfo['loginname'],
                        style: TextStyle(
                          color: Colors.white,
                        ),
                      ),
                    ),
                    Text.rich(new TextSpan(
                      text: '积分:',
                      children: <InlineSpan>[
                        new TextSpan(text: userInfo['score'].toString())
                      ],
                      style: TextStyle(
                        color: Colors.white60,
                      ),
                    ))
                  ],
                ),
                // 注销按钮,并监听点击事件
                Listener(
                    child: Text(
                      "注销",
                      style: TextStyle(
                        color: Colors.white60,
                      ),
                    ),
                    onPointerUp: (PointerUpEvent event) {
                      // 弹窗 配置如key名称所示,title:标题,titlePadding:标题的内边距,等等等
                      showDialog(
                        context: context,
                        builder: (BuildContext context) => SimpleDialog(
                          title: Text("提示"),
                          titlePadding: EdgeInsets.all(10),
                          backgroundColor: Colors.white,
                          elevation: 5,
                          shape: RoundedRectangleBorder(
                              borderRadius:
                                  BorderRadius.all(Radius.circular(6))),
                          children: <Widget>[
                            ListTile(
                              title: Center(
                                child: Text("女朋友召唤,来不及写了。"),
                              ),
                            ),
                          ],
                        ),
                      ).then<void>((value) {
                        // The value passed to Navigator.pop() or null.
                        print(value);
                      });
                    }),
              ],
            ),
          ),
        ],
      ));
}

详情页面:lib/pages/article.dart

知识点:

  1. markdown 的使用
  2. 常用部件的使用,头像、文字、等
import 'package:flutter/material.dart';
import 'package:cnode_flutter/services/index.dart';
import 'package:flutter_markdown/flutter_markdown.dart';

class ArticleDetail extends StatefulWidget {
  // 接受列表页传过来的参数
  final data;
  ArticleDetail(this.data);
  _ArticleDetailState createState() => new _ArticleDetailState(data);
}

class _ArticleDetailState extends State<ArticleDetail> {
  var data;
  // 存放整个页面的widgets
  var listViewChildren = <Widget>[];
  // 获取文章的内容信息
  _ArticleDetailState(this.data);

  @override
  initState() {
    super.initState();
    // avatar_url值为 '//www.baidu.com', //开头flutter的image部件会报错,需要处理一下数据
    // 这里没有处理的原因是,数据在列表页面已经处理过
    // data['author']['avatar_url'] = data['author']['avatar_url']
    //     .replaceAllMapped(new RegExp(r'(?<!https:|http:)//'), (hasil) {
    //   return 'https://';
    // });

    // 初始化话题详情内容信息
    initPageWidgetsFn();

    // 调取详情接口获取文章的详细信息(比如回复)
    HttpActions.getTopicDetail(id: data['id']).then((res) {
      print(res);
      // 添加评论
      addReplyWidgetsFn(res.data['data']['replies']);
    });
  }

  Widget build(BuildContext context) {
    // 页面脚手架 https://docs.flutter.cn/flutter/material/Scaffold-class.html
    return Scaffold(
        appBar: new AppBar(title: Text('话题')),
        body: Padding(
          padding: EdgeInsets.all(12),
          child: ListView.builder(
              itemCount: listViewChildren.length,
              itemBuilder: (context, index) {
                return listViewChildren[index];
              }),
        ));
  }

  // 初始化页面内容,话题的标题、内容、作者信息
  void initPageWidgetsFn() {
    setState(() {
      listViewChildren.addAll([
        // 标题
        Padding(
          padding: EdgeInsets.only(bottom: 10),
          child: Text(
            data['title'],
            style: TextStyle(
                color: Colors.black, fontSize: 17, fontWeight: FontWeight.w500),
          ),
        ),
        // 作者信息
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Row(
              children: <Widget>[
                // 头像
                CircleAvatar(
                  radius: 20,
                  backgroundImage: NetworkImage(data['author']['avatar_url']),
                ),
                // 昵称、浏览量
                Padding(
                  padding: EdgeInsets.only(left: 10),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(data['author']['loginname']),
                      Text.rich(
                        TextSpan(
                            text: data['visit_count'].toString(),
                            children: [TextSpan(text: '次浏览')]),
                      )
                    ],
                  ),
                )
              ],
            ),
            // 是否已经收藏
            data['is_collect'] == true
                ? new Icon(
                    Icons.favorite,
                    color: Colors.green,
                  )
                : new Icon(
                    Icons.favorite_border,
                    color: Colors.grey,
                  )
          ],
        ),
        // 正文
        Padding(
          padding: EdgeInsets.only(top: 15),
          child: new MarkdownBody(
              // 请注意在下面的示例中使用_raw string_(前缀为`r`的字符串)。 使用原始字符串将字符串中的每个字符视为文字字符。
              data: data['content'].replaceAllMapped(
                  new RegExp(r'(?<!http:|https:)//'), (hasil) {
            return 'https://';
          })),
        ),
        new Divider(
          height: 40,
        )
      ]);
    });
  }

  // 添加评论部件
  void addReplyWidgetsFn(repliesList) {
    // 评论部件 生成后一次添加进话题内容,其实刚好的做法是跟话题列表一样,添加上拉加载
    var widgets = <Widget>[];
    if (repliesList.length < 1) {
      // 没有评论的情况
      widgets.add(Text('no replies'));
    } else {
      // 有评论的情况
      /// 很好奇数组的forEach方法为什么不提供索引[index]
      repliesList.asMap().forEach((index, item) => widgets.add(Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  Row(
                    // 头像
                    children: <Widget>[
                      CircleAvatar(
                        radius: 16,
                        backgroundImage:
                            NetworkImage(item['author']['avatar_url']),
                      ),
                      // 昵称、楼层信息
                      Padding(
                        padding: EdgeInsets.only(left: 10),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(item['author']['loginname']),
                            Text.rich(
                              TextSpan(
                                  text: index.toString(),
                                  children: [TextSpan(text: '楼')]),
                              style: TextStyle(color: Colors.green),
                            )
                          ],
                        ),
                      )
                    ],
                  ),
                  // 是否已经收藏
                  item['is_collect'] == true
                      ? new Icon(
                          Icons.favorite,
                          color: Colors.green,
                        )
                      : new Icon(
                          Icons.favorite_border,
                          color: Colors.grey,
                        )
                ],
              ),
              // 评论
              Padding(
                padding: EdgeInsets.symmetric(
                  vertical: 10,
                ),
                child: Text(
                  item['content'],
                ),
              ),
            ],
          )));
      setState(() {
        listViewChildren.addAll(widgets);
      });
    }
  }
}

总结

强烈建议想搞Flutter的朋友,读一遍《Flutter实战》,这是Flutter中文网出的一本书,主要以入门、进阶、实例三大部分进行叙述Flutter。是我目前为数不多的成体系的Flutter中文学习指南。

环境安装

flutter-io.cn是 Flutter 官方的中文站点
安装说明:https://flutter-io.cn/docs/get-started/install
flutterchina.club是 Flutter 中文开发者社区的开源项目。
安装说明:https://book.flutterchina.club/chapter1/install_flutter.html

Flutter开发文档

https://flutter-io.cn/docs

FlutterAPI文档

https://docs.flutter.cn/

Dart 语法

http://dart.goodev.org/guides/language/language-tour

状态管理方案

1.使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。
2.选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。
3.在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。

上面的内容摘自以下链接,该作者就状态管理方案问题,做了详细的解答。
https://juejin.im/post/5d00a84fe51d455a2f22023f

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值