寫在前面
上篇文章我們實現了首頁和文章詳情頁,今天我們繼續。
正式開始
一. 實現發現頁
開啟 discovery.dart
,可以刪掉之前寫的程式碼,或者在原來的基礎上改造也可以,看大家喜歡,首先在頂部引入需要用的包和其他檔案:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../utils/countTime.dart';
import '../config/httpHeaders.dart';
複製程式碼
在這裡我引入了一個 countTime.dart
檔案,這個是我們用來計算文章釋出時間與當前的差值的,我們先把這個小工具實現一下。在 lib
資料夾下新建 utils
資料夾,並在其中新建 countTime.dart
檔案,寫入以下程式碼:
//計算髮布時間間隔
String countTime(String timestamp) {
var now = new DateTime.now();
var publicTime = DateTime.parse(timestamp);
var diff = now.difference(publicTime);
if (diff.inDays > 0) {
return '${diff.inDays}天前';
} else if (diff.inHours > 0) {
return '${diff.inHours}小時前';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}分鐘前';
} else if (diff.inSeconds > 0) {
return '${diff.inSeconds}秒前';
}
return timestamp.substring(0, timestamp.indexOf('T'));
}
複製程式碼
上面的程式碼通過傳入的時間戳來計算差值,並返回不同的文字,比較簡單,只要小夥伴們熟悉一下語法就會了。
回到 discovery.dart
繼續我們的程式碼,將上一篇文章中網路請求的寫法改一下:
/*接著寫*/
class DiscoveryPage extends StatefulWidget {
@override
DiscoveryPageState createState() => new DiscoveryPageState();
}
class DiscoveryPageState extends State<DiscoveryPage> {
List hotArticles;
Future getHotArticles() {
return http.get(Uri.encodeFull(
'https://timeline-merger-ms.juejin.im/v1/get_entry_by_rank?src=${httpHeaders['X-Juejin-Src']}&uid=${httpHeaders['X-Juejin-Uid']}&device_id=${httpHeaders['X-Juejin-Client']}&token=${httpHeaders['X-Juejin-Token']}&limit=20&category=all&recomment=1'));
}
@override
void initState() {
super.initState();
this.getHotArticles().then((response) {
setState(() {
hotArticles = json.decode(response.body)['d']['entrylist'];
});
}, onError: (e) {
throw Exception('Failed to load data');
});
}
}
複製程式碼
initState
用來做初始化,寫過 react
的同志應該很熟悉了。接著是 then
,是不是和 Promise
很像?
上一篇文章中我們構建頁面用的主要是 ListView
,既然是入門教程,我們今天就用新的元件,多熟悉一些東西。接著寫:
class DiscoveryPageState extends State<DiscoveryPage> {
/*接著寫*/
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
pinned: true,
title: new Card(
color: new Color.fromRGBO(250, 250, 250, 0.6),
child: new FlatButton(
onPressed: () {
Navigator.pushNamed(context, '/search');
},
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Icon(
Icons.search,
color: Colors.black,
),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text('搜尋')
],
),
)),
titleSpacing: 5.0,
backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
),
new SliverList(
delegate: new SliverChildBuilderDelegate((context, index) {
return new Container(
color: Colors.white,
padding: new EdgeInsets.only(top: 15.0,bottom: 15.0),
margin: new EdgeInsets.only(bottom: 20.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new FlatButton(
onPressed: null,
child: new Column(
children: <Widget>[
new Icon(
Icons.whatshot,
color: Colors.red,
size: 30.0,
),
new Text('本週最熱')
],
)),
new FlatButton(
onPressed: null,
child: new Column(
children: <Widget>[
new Icon(
Icons.collections,
color: Colors.green,
size: 30.0,
),
new Text('收藏集')
],
)),
new FlatButton(
onPressed: () {
Navigator.pushNamed(context, '/activities');
},
child: new Column(
children: <Widget>[
new Icon(
Icons.toys,
color: Colors.yellow,
size: 30.0,
),
new Text('活動')
],
)),
],
),
);
}, childCount: 1)),
new SliverList(
delegate: new SliverChildBuilderDelegate((context, index) {
return new Container(
padding: new EdgeInsets.all(10.0),
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(width: 0.2, color: Colors.grey)),
color: Colors.white),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Icon(
Icons.whatshot,
color: Colors.red,
),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(
'熱門文章',
style: new TextStyle(fontSize: 14.0),
)
],
),
new Row(
children: <Widget>[
new Icon(
Icons.settings,
color: Colors.grey,
),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(
'定製熱門',
style: new TextStyle(fontSize: 14.0, color: Colors.grey),
)
],
)
],
),
);
}, childCount: 1)),
new SliverFixedExtentList(
itemExtent: 100.0,
delegate: new SliverChildBuilderDelegate((context, index) {
var itemInfo = hotArticles[index];
return createItem(itemInfo);
}, childCount: hotArticles == null ? 0 : hotArticles.length)),
],
);
}
}
複製程式碼
這裡我們用的 CustomScrollView
和 Sliver
,語法啥的小夥伴們自己看文件了哈,就不解釋了。對於搜尋按鈕和活動按鈕,我這裡已經寫了跳轉路由,不急,我們一會兒就去實現。我們把單個文章的構建程式碼提出來,讓整體簡潔一點。
class DiscoveryPageState extends State<DiscoveryPage> {
/*接著寫*/
//單個熱門文章
Widget createItem(itemInfo) {
var publicTime = countTime(itemInfo['createdAt']);
return new Container(
padding: new EdgeInsets.only(top: 10.0, bottom: 10.0),
decoration: new BoxDecoration(
color: Colors.white,
border: new Border(
bottom: new BorderSide(width: 0.2, color: Colors.grey))),
child: new FlatButton(
onPressed: null,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Expanded(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
itemInfo['title'],
textAlign: TextAlign.left,
style: new TextStyle(
color: Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
new Text(
'${itemInfo['collectionCount']}人喜歡 · ${itemInfo['user']['username']} · $publicTime',
textAlign: TextAlign.left,
style: new TextStyle(color: Colors.grey, fontSize: 12.0),
softWrap: true,
)
],
),
),
itemInfo['screenshot'] != null
? new Image.network(
itemInfo['screenshot'],
width: 100.0,
)
: new Container(
width: 0.0,
height: 0.0,
)
],
)),
);
}
}
複製程式碼
這裡的單個文章有可能沒有截圖,所以寫個判斷。現在執行一下,如果你看到的介面長這樣,就OK了:
二. 實現搜尋頁
我們先實現搜尋頁,在 pages
下新建 search.dart
,寫入下列程式碼:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import '../utils/countTime.dart';
class SearchPage extends StatefulWidget {
@override
SearchPageState createState() => new SearchPageState();
}
class SearchPageState extends State<SearchPage> {
String searchContent;
List searchResult;
Future search(String query) {
return http.get(
'https://search-merger-ms.juejin.im/v1/search?query=$query&page=0&raw_result=false&src=web');
}
final TextEditingController controller = new TextEditingController();
}
複製程式碼
這裡我們申明兩個變數 searchContent
和 searchResult
,前者是搜尋內容,後者是結果列表,再申明一個 controller
用於控制輸入框。
看文件啊,同志們!
接著構建頁面:
class SearchPageState extends State<SearchPage> {
/*接著寫*/
@override
Widget build(BuildContext context) {
// TODO: implement build
return new CustomScrollView(
slivers: <Widget>[
new SliverAppBar(
pinned: true,
leading: new IconButton(
icon: new Icon(Icons.chevron_left),
onPressed: () {
Navigator.pop(context);
}),
title: new Text(
'搜尋',
style: new TextStyle(fontWeight: FontWeight.normal),
),
centerTitle: true,
iconTheme: new IconThemeData(color: Colors.blue),
backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
bottom: new PreferredSize(
child: new Container(
color: Colors.white,
padding: new EdgeInsets.all(5.0),
child: new Card(
color: new Color.fromRGBO(252, 252, 252, 0.6),
child: new Padding(
padding: new EdgeInsets.all(5.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Expanded(
child: new TextField(
autofocus: true,
style: new TextStyle(
fontSize: 14.0, color: Colors.black),
decoration: new InputDecoration(
contentPadding: new EdgeInsets.all(0.0),
border: InputBorder.none,
hintText: '搜尋',
prefixIcon: new Icon(
Icons.search,
size: 16.0,
color: Colors.grey,
),
),
onChanged: (String content) {
setState(() {
searchContent = content;
});
},
onSubmitted: (String content) {
search(content).then((response) {
setState(() {
searchResult =
json.decode(response.body)['d'];
});
}, onError: (e) {
throw Exception('Failed to load data');
});
},
controller: controller,
),
),
searchContent == ''
? new Container(
height: 0.0,
width: 0.0,
)
: new InkResponse(
child: new Icon(
Icons.close,
),
onTap: () {
setState(() {
searchContent = '';
controller.text = '';
});
})
],
),
)),
),
preferredSize: new Size.fromHeight(40.0))),
searchResult == null
? new SliverFillRemaining(
child: new Container(
color: Colors.white,
),
)
: new SliverList(
delegate: new SliverChildBuilderDelegate((context, index) {
var resultInfo = searchResult[index];
return showResult(resultInfo);
}, childCount: searchResult.length))
],
);
}
}
複製程式碼
這裡沒什麼特別的,小夥伴們看看程式碼就懂了,我們還是把搜尋結果單獨提出來:
class SearchPageState extends State<SearchPage> {
/*接著寫*/
//顯示搜尋結果
Widget showResult(resultInfo) {
var publicTime = countTime(resultInfo['createdAt']);
return new Container(
alignment: Alignment.centerLeft,
padding: new EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.white,
border: new Border(
bottom: new BorderSide(width: 0.2, color: Colors.grey))),
child: new FlatButton(
onPressed: null,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Text(
resultInfo['title'],
style: new TextStyle(color: Colors.black),
),
new Text(
'${resultInfo['collectionCount']}人喜歡 · ${resultInfo['user']['username']} · $publicTime',
textAlign: TextAlign.left,
style: new TextStyle(color: Colors.grey, fontSize: 12.0),
softWrap: true,
)
],
)),
);
}
}
複製程式碼
至此,搜尋頁面寫完了,別忙執行啊,還沒寫路由呢。開啟 main.dart
,引入 search.dart
,然後配置一下路由:
import 'package:flutter/material.dart';
import 'pages/index.dart';
import 'pages/search.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new IndexPage(),
theme: new ThemeData(
highlightColor: Colors.transparent,
//將點選高亮色設為透明
splashColor: Colors.transparent,
//將噴濺顏色設為透明
bottomAppBarColor: new Color.fromRGBO(244, 245, 245, 1.0),
//設定底部導航的背景色
scaffoldBackgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
//設定頁面背景顏色
primaryIconTheme: new IconThemeData(color: Colors.blue),
//主要icon樣式,如頭部返回icon按鈕
indicatorColor: Colors.blue,
//設定tab指示器顏色
iconTheme: new IconThemeData(size: 18.0),
//設定icon樣式
primaryTextTheme: new TextTheme(
//設定文字樣式
title: new TextStyle(color: Colors.black, fontSize: 16.0))),
routes: <String, WidgetBuilder>{
'/search': (BuildContext context) => SearchPage()
},
);
}
}
複製程式碼
現在可以執行了,效果如下:點選進入搜尋詳情頁我就不做了,這些都留給小夥伴們練手吧:
三. 實現活動頁
活動頁的實現和首頁一模一樣,程式碼我就不貼了,在 main.dart
配置一下就行:
import 'package:flutter/material.dart';
import 'pages/index.dart';
import 'pages/search.dart';
import 'pages/activities.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new IndexPage(),
theme: new ThemeData(
highlightColor: Colors.transparent,
//將點選高亮色設為透明
splashColor: Colors.transparent,
//將噴濺顏色設為透明
bottomAppBarColor: new Color.fromRGBO(244, 245, 245, 1.0),
//設定底部導航的背景色
scaffoldBackgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
//設定頁面背景顏色
primaryIconTheme: new IconThemeData(color: Colors.blue),
//主要icon樣式,如頭部返回icon按鈕
indicatorColor: Colors.blue,
//設定tab指示器顏色
iconTheme: new IconThemeData(size: 18.0),
//設定icon樣式
primaryTextTheme: new TextTheme(
//設定文字樣式
title: new TextStyle(color: Colors.black, fontSize: 16.0))),
routes: <String, WidgetBuilder>{
'/search': (BuildContext context) => SearchPage(),
'/activities': (BuildContext context) => ActivitiesPage(),
},
);
}
}
複製程式碼
效果如下:
結尾叨叨
今天的內容不多,主要使用了新的元件和新的請求寫法,小夥伴們想實現什麼功能就動手吧,多多練習,今天就到這裡了。原始碼點這裡。