赞
踩
强烈建议想搞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 是谷歌的移动 UI 框架,用于在创纪录的时间内在 iOS 和 Android 上制作高质量的原生界面。 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
知识点:
package:provider/provider.dart
状态管理package:flutter/material.dart
UI组件应用pub
资源包使用// 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());
// 应用入口 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
知识点:
ListView
;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 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
知识点:
HomeDrawer
抽屉的使用;Listener
、GestureDetector
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
知识点:
markdown
的使用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
http://dart.goodev.org/guides/language/language-tour
1.使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。
2.选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。
3.在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。
上面的内容摘自以下链接,该作者就状态管理方案问题,做了详细的解答。
https://juejin.im/post/5d00a84fe51d455a2f22023f
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。