强烈建议想搞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:强类型语言
支持类型检查,编译前提前发现错误。
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]的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'(?
// 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'(?), (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
https://flutter-io.cn/docs
https://docs.flutter.cn/
http://dart.goodev.org/guides/language/language-tour
1.使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。
2.选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。
3.在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。
上面的内容摘自以下链接,该作者就状态管理方案问题,做了详细的解答。
https://juejin.im/post/5d00a84fe51d455a2f22023f