刚刚过去的2018 Google开发者大会上,Flutter 发布预览版2,支持了iOS应用的界面风格。尽管我们并不知道Flutter能走多远,但从技术的角度来讲,业余时间有精力去多掌握一门语言,一门跨平台的方式,对技术本身来说没有什么不好。
readhub客户端
作为一个入门级练手的项目,目前readhub客户端已经实现了包括框架搭建、下拉刷新、加载更多、数据请求等功能,其中界面部分参考点击查看大神开源的项目。代码还是比较粗糙的,后期有时间再去优化…
项目结构
使用Google Material风格搭建,底部tab使用自带的BottomNavigationBar完成,具体代码如下。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
117
118
119
120
121
122
123
124
125void main() => runApp(new App());
class App extends StatelessWidget {
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.blueGrey,
primaryColor:
Theme.of(context).platform == TargetPlatform.iOS
? Colors.white
: Colors.blueGrey),
debugShowCheckedModeBanner: false,
home: MainWidget(),
);
}
}
class MainWidget extends StatefulWidget {
State<StatefulWidget> createState() {
return new MainStateWidget();
}
}
class MainStateWidget extends State<MainWidget> {
int _tabIndex = 0;
var tabImages;
var tabTitles = ['热门话题', '科技动态', '开发者资讯', '区块链快讯', '更多'];
var _bodys;
Image getTabImage(path) {
return new Image.asset(path, width: 20.0, height: 20.0);
}
Image getTabIcon(int curIndex) {
if (curIndex == _tabIndex) {
return tabImages[curIndex][1];
}
return tabImages[curIndex][0];
}
Text getTabTitle(int curIndex) {
if (curIndex == _tabIndex) {
return new Text(tabTitles[curIndex],
style: new TextStyle(color: const Color(0xff617D89)));
} else {
return new Text(tabTitles[curIndex],
style: new TextStyle(color: const Color(0xff888888)));
}
}
void initData() {
tabImages = [
[
getTabImage('images/ic_topic.png'),
getTabImage('images/ic_topic_fill.png')
],
[
getTabImage('images/ic_news.png'),
getTabImage('images/ic_news_fill.png')
],
[
getTabImage('images/ic_code.png'),
getTabImage('images/ic_code_fill.png')
],
[
getTabImage('images/ic_block_chain.png'),
getTabImage('images/ic_block_chain_fill.png')
],
[
getTabImage('images/ic_more.png'),
getTabImage('images/ic_more_fill.png')
]
];
_bodys = [
new TopicPage(),
new NewsPage(),
new CodePage(),
new ChainPage(),
new MorePage()
];
}
var title = '热门话题';
Widget build(BuildContext context) {
initData();
return Scaffold(
appBar: new AppBar(
title: Image.asset(
'images/ic_toolbar_logo.png',
width: 100.0,
height: 25.0,
),
backgroundColor: Colors.white,
centerTitle: true,
),
body: _bodys[_tabIndex],
bottomNavigationBar: new BottomNavigationBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
icon: getTabIcon(0), title: getTabTitle(0)),
new BottomNavigationBarItem(
icon: getTabIcon(1), title: getTabTitle(1)),
new BottomNavigationBarItem(
icon: getTabIcon(2), title: getTabTitle(2)),
new BottomNavigationBarItem(
icon: getTabIcon(3), title: getTabTitle(3)),
new BottomNavigationBarItem(
icon: getTabIcon(4), title: getTabTitle(4)),
],
type: BottomNavigationBarType.fixed,
currentIndex: _tabIndex,
onTap: (index) {
setState(() {
_tabIndex = index;
title = getTabTitle(_tabIndex).data;
});
},
),
);
}
}
下拉刷新
封装下拉刷新的控件RHRefreshState,可以对所有widget实现下拉刷新的操作,并简单的处理了空数据和错误页面。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
105abstract class RHRefreshState<T extends StatefulWidget> extends State<T> {
int pageIndex = 1;
String message = '';
bool _showProgress = false;
List<dynamic> items = new List<dynamic>();
ScrollController scrollController = new ScrollController();
Widget buildRow(BuildContext context, int index);
void initState() {
super.initState();
_showProgress = true;
onRefresh(pageIndex);
}
void dispose() {
scrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
if (_showProgress) {
_showProgress = false;
return Center(
child: new CircularProgressIndicator(),
);
}
return new RefreshIndicator(
onRefresh: _onRefresh,
child: new Stack(
children: <Widget>[_buildMessageWidget(), buildListWidget()],
),
);
}
Future<void> onRefresh(int pageIndex) {}
void onRefreshComplete(List<dynamic> list) {
setState(() {
message = list.isEmpty ? '暂无数据' : '';
items.clear();
items.addAll(list);
});
}
void onRefreshError(Exception e) {
setState(() {
message = e.toString();
});
}
Future<void> _onRefresh() {
pageIndex = 1;
return onRefresh(pageIndex);
}
Widget buildListWidget() {
return new Offstage(
offstage: message.isNotEmpty,
child: ListView.builder(
controller: scrollController,
itemCount: items.length,
itemBuilder: (context, index) {
return buildRow(context, index);
}),
);
}
Widget buildLoadMoreIndicator() {
return null;
}
Widget _buildMessageWidget() {
return new Offstage(
offstage: message.isEmpty,
child: new ListView.builder(
itemCount: 1,
itemBuilder: (context, index) {
return new GestureDetector(
child: new Container(
padding: const EdgeInsets.only(top: 200.0),
child: new Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0,
decoration: TextDecoration.none,
color: Colors.black38),
),
),
);
}));
}
dynamic getItem(int index) {
if (items.isNotEmpty) {
return items.elementAt(index);
}
return null;
}
}
加载更多
在下拉刷新RHRefreshState的基础上对列表做了加载更多RHRefreshPageState的支持,暴露出相同的接口onRefresh(int pageIndex)给外部使用。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
82abstract class RHRefreshPageState<T extends StatefulWidget>
extends RHRefreshState<T> {
bool _isLoadMore = false;
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
setState(() {
_isLoadMore = true;
});
pageIndex++;
onRefresh(pageIndex);
}
});
}
Future<void> onRefresh(int pageIndex) {
super.onRefresh(pageIndex);
if (_isLoadMore) {
return Future.value('');
}
setState(() {
_isLoadMore = true;
});
}
void onRefreshComplete(List<dynamic> list) {
if (pageIndex != 1) {
setState(() {
_isLoadMore = false;
items.addAll(list);
});
} else
super.onRefreshComplete(list);
}
void onRefreshError(Exception e) {
if (_isLoadMore) {
double edge = 50.0;
double offsetFromBottom = scrollController.position.maxScrollExtent -
scrollController.position.pixels;
if (offsetFromBottom < edge) {
scrollController.animateTo(
scrollController.offset - (edge - offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
super.onRefreshError(e);
}
Widget buildLoadMoreIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: 1.0,
child: new CircularProgressIndicator(),
)),
);
}
Widget buildListWidget() {
return new Offstage(
offstage: message.isNotEmpty,
child: ListView.builder(
controller: scrollController,
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return buildLoadMoreIndicator();
}
return buildRow(context, index);
}),
);
}
}
数据请求
数据请求支持返回Future的方式,但也添加了callback的方式,毕竟作为一个安卓开发还是觉得callback比较顺手,哈哈…
返回Future的方式
1 | Future<dynamic> getHttpRequest(String url, Map<String, String> map) async { |
callback方式
1 | void getHttpRequest2(Completer _completer, String url, |
TODO
- 更多页面交互逻辑
- 详情页面