1 本节目标

  • 代码规范
  • 业务代码组织
  • 首页代码编写

视频

代码

https://github.com/ducafecat/flutter_learn_news/releases/tag/v1.0.6

2 代码规范

2.1 官方代码规范

https://dart.dev/guides/language/effective-dart

2.3 chrome 插件 <彩云小译 - 网页翻译插件>

https://chrome.google.com/webstore/detail/lingocloud-web-translatio/jmpepeebcbihafjjadogphmbgiffiajh

2.4 阿里项目规范

https://github.com/alibaba/flutter-go/blob/master/Flutter_Go%20%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83.md

3 业务界面代码组织

3.1 redux、fish-redux

  • redux 架构
  • fish-redux 架构

进一步的细分,进行规范

https://github.com/alibaba/fish-redux/tree/master/doc
https://medium.com/@dave790602/flutter-architecture-fish-redux-9b753912224a

  • fish-redux 代码

3.2 bloc

  • 架构

https://bloclibrary.dev/#/

  • 代码组织

3.3 简单就是美

3.4 如何平衡

  • 是否团队开发
  • 是否简单业务(20 页面)
  • 是否重交互(视频社交、聊天 A)

4 新闻首页实现

4.1 界面组成分析

  • 分类导航、推荐新闻、频道导航
  • 新闻列表、广告 ad、邮件订阅

4.2 代码框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
...
class _MainPageState extends State<MainPage> {

@override
void initState() {
super.initState();
_loadAllData();
}

// 读取所有数据
_loadAllData() async {
}

// 分类菜单
Widget _buildCategories() {
return Container();
}

// 推荐阅读
Widget _buildRecommend() {
return Container();
}

// 频道
Widget _buildChannels() {
return Container();
}

// 新闻列表
Widget _buildNewsList() {
return Container();
}

// ad 广告条
// 邮件订阅
Widget _buildEmailSubscribe() {
return newsletterWidget();
}

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
_buildCategories(),
Divider(height: 1),
_buildRecommend(),
Divider(height: 1),
_buildChannels(),
Divider(height: 1),
_buildNewsList(),
Divider(height: 1),
_buildEmailSubscribe(),
],
),
);
}
}

4.3 实现业务

  • 创建 widget 单独文件
  • 分类导航

lib/pages/main/categories_widget.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Widget newsCategoriesWidget({
List<CategoryResponseEntity> categories,
String selCategoryCode,
Function(CategoryResponseEntity) onTap,
}) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: categories.map<Widget>((item) {
return Container(
alignment: Alignment.center,
height: duSetHeight(52),
padding: EdgeInsets.symmetric(horizontal: 8),
child: GestureDetector(
child: Text(
item.title,
style: TextStyle(
color: selCategoryCode == item.code
? AppColors.secondaryElementText
: AppColors.primaryText,
fontSize: duSetFontSize(18),
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
),
),
onTap: () => onTap(item),
),
);
}).toList(),
),
);
}
  • 频道导航

lib/pages/main/channels_widget.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Widget newsChannelsWidget({
List<ChannelResponseEntity> channels,
Function(ChannelResponseEntity) onTap,
}) {
return Container(
height: duSetHeight(137),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: channels.map<Widget>((item) {
return Container(
width: duSetWidth(70),
height: duSetHeight(97),
margin: EdgeInsets.symmetric(horizontal: duSetWidth(10)),
child: InkWell(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 图标
Container(
height: duSetWidth(64),
margin: EdgeInsets.symmetric(horizontal: duSetWidth(3)),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 0,
top: 0,
right: 0,
child: Container(
height: duSetWidth(64),
decoration: BoxDecoration(
color: AppColors.primaryBackground,
boxShadow: [
Shadows.primaryShadow,
],
borderRadius:
BorderRadius.all(Radius.circular(32)),
),
child: Container(),
),
),
Positioned(
left: duSetWidth(10),
top: duSetWidth(10),
right: duSetWidth(10),
child: Image.asset(
"assets/images/channel-${item.code}.png",
fit: BoxFit.none,
),
),
],
),
),
// 标题
Text(
item.title,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
maxLines: 1,
style: TextStyle(
color: AppColors.thirdElementText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(14),
height: 1,
),
),
],
),
onTap: () => onTap(item),
),
);
}).toList(),
),
),
);
}
  • 新闻行 Item

lib/pages/main/news_item_widget.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Widget newsItem(NewsItem item) {
return Container(
height: duSetHeight(161),
padding: EdgeInsets.all(duSetWidth(20)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 图
imageCached(
item.thumbnail,
width: duSetWidth(121),
height: duSetWidth(121),
),
// 右侧
SizedBox(
width: duSetWidth(194),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 作者
Container(
margin: EdgeInsets.all(0),
child: Text(
item.author,
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.thirdElementText,
fontSize: duSetFontSize(14),
height: 1,
),
),
),
// 标题
Container(
margin: EdgeInsets.only(top: duSetHeight(10)),
child: Text(
item.title,
style: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500,
color: AppColors.primaryText,
fontSize: duSetFontSize(16),
height: 1,
),
overflow: TextOverflow.clip,
maxLines: 3,
),
),
// Spacer
Spacer(),
// 一行 3 列
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 分类
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: duSetWidth(60),
),
child: Text(
item.category,
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.secondaryElementText,
fontSize: duSetFontSize(14),
height: 1,
),
overflow: TextOverflow.clip,
maxLines: 1,
),
),
// 添加时间
Container(
width: duSetWidth(15),
),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: duSetWidth(100),
),
child: Text(
'• ${duTimeLineFormat(item.addtime)}',
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.thirdElementText,
fontSize: duSetFontSize(14),
height: 1,
),
overflow: TextOverflow.clip,
maxLines: 1,
),
),
// 更多
Spacer(),
InkWell(
child: Icon(
Icons.more_horiz,
color: AppColors.primaryText,
size: 24,
),
onTap: () {},
),
],
),
),
],
),
),
],
),
);
}
  • 邮件订阅

lib/pages/main/newsletter_widget.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Widget newsletterWidget() {
return Container(
margin: EdgeInsets.all(duSetWidth(20)),
child: Column(
children: <Widget>[
// newsletter
Row(
children: <Widget>[
Text(
'Newsletter',
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: duSetFontSize(18),
fontWeight: FontWeight.w600,
color: AppColors.thirdElement,
),
),
Spacer(),
IconButton(
icon: Icon(
Icons.close,
color: AppColors.thirdElementText,
size: duSetFontSize(17),
),
onPressed: () {},
),
],
),

// email
inputEmailEdit(
marginTop: 19,
keyboardType: TextInputType.emailAddress,
hintText: "Email",
isPassword: false,
controller: null,
),

// btn subcrible
Padding(
padding: EdgeInsets.only(top: 15),
child: btnFlatButtonWidget(
onPressed: () {},
width: duSetWidth(335),
height: duSetHeight(44),
fontWeight: FontWeight.w600,
title: "Subscribe",
),
),

// disc
Container(
margin: EdgeInsets.only(top: duSetHeight(29)),
width: duSetWidth(261),
child: Text.rich(TextSpan(children: <TextSpan>[
TextSpan(
text: 'By clicking on Subscribe button you agree to accept',
style: new TextStyle(
color: AppColors.thirdElementText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(14),
),
),
TextSpan(
text: ' Privacy Policy',
style: new TextStyle(
color: AppColors.secondaryElementText,
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: duSetFontSize(14),
),
recognizer: TapGestureRecognizer()
..onTap = () {
toastInfo(msg: 'Privacy Policy');
},
),
])),
),
],
),
);
}
  • 推荐阅读

lib/pages/main/recommend_widget.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
Widget recommendWidget(NewsRecommendResponseEntity newsRecommend) {
return Container(
margin: EdgeInsets.all(duSetWidth(20)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 图
imageCached(
newsRecommend.thumbnail,
width: duSetWidth(335),
height: duSetHeight(290),
),
// 作者
Container(
margin: EdgeInsets.only(top: duSetHeight(14)),
child: Text(
newsRecommend.author,
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.thirdElementText,
fontSize: duSetFontSize(14),
),
),
),
// 标题
Container(
margin: EdgeInsets.only(top: duSetHeight(10)),
child: Text(
newsRecommend.title,
style: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
color: AppColors.primaryText,
fontSize: duSetFontSize(24),
height: 1,
),
),
),
// 一行 3 列
Container(
margin: EdgeInsets.only(top: duSetHeight(10)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 分类
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 120,
),
child: Text(
newsRecommend.category,
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.secondaryElementText,
fontSize: duSetFontSize(14),
height: 1,
),
overflow: TextOverflow.clip,
maxLines: 1,
),
),
// 添加时间
Container(
width: duSetWidth(15),
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 120,
),
child: Text(
'• ${duTimeLineFormat(newsRecommend.addtime)}',
style: TextStyle(
fontFamily: 'Avenir',
fontWeight: FontWeight.normal,
color: AppColors.thirdElementText,
fontSize: duSetFontSize(14),
height: 1,
),
overflow: TextOverflow.clip,
maxLines: 1,
),
),
// 更多
Spacer(),
InkWell(
child: Icon(
Icons.more_horiz,
color: AppColors.primaryText,
size: 24,
),
onTap: () {},
),
],
),
),
],
),
);
}

蓝湖设计稿

https://lanhuapp.com/url/lYuz1
密码: gSKl

蓝湖现在收费了,所以查看标记还请自己上传 xd 设计稿
商业设计稿文件不好直接分享, 可以加微信联系 ducafecat

YAPI 接口管理

http://yapi.demo.qunar.com/

参考

VSCode 插件

视频


© 猫哥

https://ducafecat.tech