Flutter应用篇:ReadHub客户端

刚刚过去的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
125
void main() => runApp(new App());

class App extends StatelessWidget {
@override
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 {
@override
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 = '热门话题';
@override
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
105
abstract 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);

@override
void initState() {
super.initState();
_showProgress = true;
onRefresh(pageIndex);
}

@override
void dispose() {
scrollController.dispose();
super.dispose();
}

@override
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
82
abstract class RHRefreshPageState<T extends StatefulWidget>
extends RHRefreshState<T> {
bool _isLoadMore = false;

@override
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
setState(() {
_isLoadMore = true;
});
pageIndex++;
onRefresh(pageIndex);
}
});
}

@override
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);
}

@override
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<dynamic> getHttpRequest(String url, Map<String, String> map) async {
var httpClient = new HttpClient();
try {
Uri uri = Uri.parse(url);
Uri newUri = new Uri.https(uri.authority, uri.path, map);

var request = await httpClient.getUrl(newUri);
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
var json = await response.transform(Utf8Decoder()).join();
return Future.value(json);
} else {
return Future.error(new NetworkAPIException('server error'));
}
} catch (exception) {
return Future.error(new NetworkAPIException(exception.toString()));
}
}

callback方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void getHttpRequest2(Completer _completer, String url,
Map<String, String> map, OnRequestCallback callback) async {
var httpClient = new HttpClient();
try {
Uri uri = Uri.parse(url);
Uri newUri = new Uri.https(uri.authority, uri.path, map);

var request = await httpClient.getUrl(newUri);
var response = await request.close();

_completer.complete();
if (response.statusCode == HttpStatus.ok) {
var json = await response.transform(Utf8Decoder()).join();
if (callback != null) callback.onSuccess(json);
} else {
callback.onError(new NetworkAPIException('server error'));
}
} catch (exception) {
callback.onError(new NetworkAPIException(exception.toString()));
}
}

TODO

  • 更多页面交互逻辑
  • 详情页面
您的支持是我原创的动力