以前发了一篇文章——《用Flutter山寨一下掘金》,因为是本身学习Flutter时的练手项目,文中彻底没有写过程,只将源码上传到了GitHub。现受掘金邀请,将文章写成入门教程,让对Flutter感兴趣的小伙伴都能看懂。html
我把项目分红四个小节,按照改版后的掘金app从新写成了教程,供你们一块儿学习和交流。因为是入门教程,文中内容不会很深刻,对于已经学习过Flutter一段时间的小伙伴,略过就好啦。我是前端开发一枚,也是初学Flutter,文中出现的错误,还请小伙伴们指出。前端
如今开始今天的学习。ios
生成的项目结构以下:git
在此项目中,咱们的业务代码都在 lib
下,包配置都在 pubspec.yaml
中。github
点击右上角的模拟器按钮,选择已经配置好的模拟器,再点击旁边的绿色三角形,稍等片刻,当你在模拟器中看到下面的效果,恭喜,项目跑起来了:web
screenshots
和 articles
文件夹是我写文章用的,小伙伴们不用看。打开 lib
中的 main.dart
文件,会看到已经有一些代码,有注释,小伙伴们能够阅读一下(截图有点长,贴代码有点多,小伙伴们就本身看了哈)。删掉原有的代码,咱们开始写本身的代码:json
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
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))),
);
}
}
复制代码
小伙伴们会发现和以前的代码有些不同,不用惊讶,写法能够有不少种,之后就明白了。你要是如今点底部的 Hot Reload
或者 Hot Restart
会发现啥也没有,固然啦,咱们啥都还没写呢:api
import
是引入咱们须要用的包等东西,这里引入了 material.dart
,这是一个包含了大量 material
风格的组件的包。Widget
(组件)有两类, StatelessWidget
是无状态的, StatefulWidget
是有状态的,当你的页面会随着状态的改变发生变化时使用。二者中必有 build
方法,用于建立内容。MaterialApp
是应用的根组件,这是实现了 material
风格的WidgetsApp
,后面全部的页面、组件都会在其中。theme
中是对组件作一些全局配置。小伙伴们必定要多看文档哈,虽然文档不少,但要是你不看,你可能会懵逼的,尤为是作前端开发的同志,dart是新语言,语法这些是必需要学习的,我不可能在文中逐行解释哈,切记!数组
在 lib
文件夹下新建 pages
文件夹,用于存放咱们的页面。而后再 pages
文件夹下新建 index.dart
、 home.dart
、 discovery.dart
、 hot.dart
、 book.dart
、 mine.dart
,对应底部的每一个tab,这是咱们项目中主要会用到的文件了。网络
在 index.dart
文件中写入以下内容:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'home.dart';
import 'hot.dart';
import 'discovery.dart';
import 'book.dart';
import 'mine.dart';
class IndexPage extends StatefulWidget {
@override
createState() => new IndexPageState();
}
class IndexPageState extends State<IndexPage> {
// 定义底部导航列表
final List<BottomNavigationBarItem> bottomTabs = [
new BottomNavigationBarItem(
icon: new Icon(CupertinoIcons.home),
title: new Text('首页'),
),
new BottomNavigationBarItem(
icon: new Icon(CupertinoIcons.conversation_bubble),
title: new Text('沸点')),
new BottomNavigationBarItem(
icon: new Icon(CupertinoIcons.search), title: new Text('发现')),
new BottomNavigationBarItem(
icon: new Icon(CupertinoIcons.book), title: new Text('小册')),
new BottomNavigationBarItem(
icon: new Icon(CupertinoIcons.profile_circled), title: new Text('我'))
];
final List<Widget> tabBodies = [
new HomePage(),
new HotPage(),
new DiscoveryPage(),
new BookPage(),
new MinePage()
];
int currentIndex = 0; //当前索引
Widget currentPage; //当前页面
@override
void initState() {
super.initState();
currentPage = tabBodies[currentIndex];
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex,
items: bottomTabs,
onTap: (index) {
setState(() {
currentIndex = index;
currentPage = tabBodies[currentIndex];
});
}),
body: currentPage,
);
}
}
复制代码
上面的代码建立了一个即底部有tab按钮的基本页面结构,用于切换不一样页面。经过点击事件改变当前索引,来显示相应的页面。bottomTabs
能够封装一下,就留给小伙伴们本身弄了哈,当是练习。
顶部咱们引入了一个 Cupertino.dart
,这是ios风格的组件,咱们还用到了ios的图标,引入前咱们须要到 pubspec.yaml
中配置一下,而后点击 Packages get
:
StatefulWidget
。final
关键字用于申明一个常量,List<BottomNavigationBarItem>
中的 List
用于申明一个数组,至关于js中的 Array
,后面的 BottomNavigationBarItem
指的是元素的类型。Scaffold
多是用得最多的组件了,它对页面实现了一些结构划分,其他的属性部分,小伙伴们就本身看文档了哈,不难,记住就行。再次强调,小伙伴们必定要多看文档,否则你真的会懵逼的!
接着咱们在其他文件中写入下面的代码,只修改页面名字:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class HotPage extends StatefulWidget {
@override
HotPageState createState() => new HotPageState();
}
class HotPageState extends State<HotPage> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Center(child: new Text('沸点'),);
}
}
复制代码
保存一下,若是你在模拟器上看到下面的内容,就成功了:
tabs也能够用ios风格的CupertinoTabBar
实现,此组件的表现和ios原生的如出一辙,留给小伙伴们当练习哈。
如今咱们来实现首页,先在 lib
文件夹下新建一个 config
文件夹,并在其中建立 httpHeaders.dart
文件,写入下列代码:
const httpHeaders = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Host': 'gold-tag-ms.juejin.im',
'Origin': 'https://juejin.im',
'Referer': 'https://juejin.im/timeline',
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
'X-Juejin-Client': '1532136021731',
'X-Juejin-Src': 'web',
'X-Juejin-Token':
'eyJhY2Nlc3NfdG9rZW4iOiJWUmJ2dDR1RFRzY1JUZXFPIiwicmVmcmVzaF90b2tlbiI6IjBqdXhYSzA3dW9mSTJWUEEiLCJ0b2tlbl90eXBlIjoibWFjIiwiZXhwaXJlX2luIjoyNTkyMDAwfQ==',
'X-Juejin-Uid': '59120a711b69e6006865dd7b'
};
复制代码
这是掘金的请求头信息,后面会用到,先定义在这里,须要注意的是其中的 X-Juejin-Client
会变化,若是小伙伴们在看文章的时候发现值变了,改一下就行(好像不改也仍是能用)。
打开 home.dart
,在顶部写入下列代码:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import '../config/httpHeaders.dart';
复制代码
咱们新引入了三个包,用来作网络请求。dart:convert
用来作数据转换,dart:async
作异步,package:http/http.dart
作请求。接着:
/*接着写*/
class HomePage extends StatefulWidget {
@override
HomePageState createState() => new HomePageState();
}
class HomePageState extends State<HomePage> {
// 获取分类
Future getCategories() async {
final response = await http.get(
'https://gold-tag-ms.juejin.im/v1/categories',
headers: httpHeaders);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load categories');
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return FutureBuilder(
future: getCategories(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var tabList = snapshot.data['d']['categoryList'];
return new CreatePage(
tabList: tabList,
);
} else if (snapshot.hasError) {
return Text("error1>>>>>>>>>>>>>>>:${snapshot.error}");
}
return new Container(
color: new Color.fromRGBO(244, 245, 245, 1.0),
);
},
);
}
}
复制代码
这部分咱们先获取获取掘金顶部的分类列表,Future
相似于 Promise
,用来作异步请求, FutureBuilder
函数用来在请求返回后构建页面,返回的状态、数据等信息都在 snapshot
中(前端的同志们看到 async
和 await
是否是很眼熟?)。这里咱们把构建页面的代码提取出来,否则嵌套太多让人崩溃,并把获取的tabs传下去。我这里用的 FutureBuilder
,小伙伴们也能够用文档中的写法,看上去还会更简洁,不过既然是学习嘛,写写也无妨。
/*接着写*/
//建立页面
class CreatePage extends StatefulWidget {
final List tabList;
@override
CreatePage({Key key, this.tabList}) : super(key: key);
CreatePageState createState() => new CreatePageState();
}
class CreatePageState extends State<CreatePage> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
//TODO: implement build
return new DefaultTabController(
length: widget.tabList.length,
child: new Scaffold(
appBar: new AppBar(
backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
automaticallyImplyLeading: false,
titleSpacing: 0.0,
title: new TabBar(
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
labelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
tabs: widget.tabList.map((tab) {
return new Tab(
text: tab['name'],
);
}).toList()),
actions: <Widget>[
new IconButton(
icon: new Icon(
Icons.add,
color: Colors.blue,
),
onPressed: () {
Navigator.pushNamed(context, '/shareArticle');
})
],
),
body: new TabBarView(
children: widget.tabList.map((cate) {
return ArticleLists(
categories: cate,
);
}).toList()),
));
}
}
复制代码
这部分用于建立tab选项和tab页面,DefaultTabController
是建立 tabBarView
的一个简单组件,之后小伙伴们能够本身实现个性化的 tabBarView
,action
里我已经把路由写进去了,等咱们把页面写完,再去实现路由。咱们把构建文章列表的代码也提出来,当每点击一个tab,就把对应的tab信息传入,查询文章会须要tab项中的 id
。
/*接着写*/
class ArticleLists extends StatefulWidget {
final Map categories;
@override
ArticleLists({Key key, this.categories}) : super(key: key);
ArticleListsState createState() => new ArticleListsState();
}
class ArticleListsState extends State<ArticleLists> {
List articleList;
Future getArticles({int limit = 20, String category}) async {
final String url =
'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=$limit&category=$category';
final response = await http.get(Uri.encodeFull(url));
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load post');
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return new FutureBuilder(
future: getArticles(category: widget.categories['id']),
builder: (context, snapshot) {
if (snapshot.hasData) {
articleList = snapshot.data['d']['entrylist'];
return new ListView.builder(
itemCount: articleList.length,
itemBuilder: (context, index) {
var item = articleList[index];
return createItem(item);
});
} else if (snapshot.hasError) {
return new Center(
child: new Text("error2>>>>>>>>>>>>>>>:${snapshot.error}"),
);
}
return new CupertinoActivityIndicator();
});
}
}
复制代码
咱们把单个文章的构建代码也提出来,让代码看着舒服点。
class ArticleListsState extends State<ArticleLists> {
/*接着写*/
//单个文章
Widget createItem(articleInfo) {
var objectId = articleInfo['originalUrl']
.substring(articleInfo['originalUrl'].lastIndexOf('/') + 1);
var tags = articleInfo['tags'];
return new Container(
margin: new EdgeInsets.only(bottom: 10.0),
padding: new EdgeInsets.only(top: 10.0, bottom: 10.0),
child: new FlatButton(
padding: new EdgeInsets.all(0.0),
onPressed: () {
Navigator.push(
context,
new CupertinoPageRoute(
builder: (context) => ArticleDetail(
objectId: objectId,
articleInfo: articleInfo,
)));
},
child: new Column(
children: <Widget>[
new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new FlatButton(
onPressed: null,
child: new Row(
children: <Widget>[
new CircleAvatar(
backgroundImage: new NetworkImage(
articleInfo['user']['avatarLarge']),
),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(
articleInfo['user']['username'],
style: new TextStyle(color: Colors.black),
)
],
)),
//控制是否显示tag,及显示多少个
tags.isNotEmpty
? (tags.length >= 2
? new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new FlatButton(
onPressed: null,
child: new Text(
tags[0]['title'].toString(),
style: new TextStyle(fontSize: 14.0),
)),
new Text('/'),
new FlatButton(
onPressed: null,
child: new Text(
tags[1]['title'].toString(),
style: new TextStyle(fontSize: 14.0),
))
],
)
: new FlatButton(
onPressed: null,
child: new Text(
tags[0]['title'].toString(),
style: new TextStyle(fontSize: 14.0),
)))
: new Container(
width: 0.0,
height: 0.0,
)
],
),
new ListTile(
title: new Text(articleInfo['title']),
subtitle: new Text(
articleInfo['summaryInfo'],
maxLines: 2,
),
),
new Row(
children: <Widget>[
new FlatButton(
onPressed: null,
child: new Row(
children: <Widget>[
new Icon(Icons.favorite),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(articleInfo['collectionCount'].toString())
],
)),
new FlatButton(
onPressed: null,
child: new Row(
children: <Widget>[
new Icon(Icons.message),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(articleInfo['commentsCount'].toString())
],
))
],
)
],
),
),
color: Colors.white,
);
}
}
复制代码
每一个文章中的交互我这里就不作那么全了,否则篇幅太大,样式小伙伴们也本身调吧,这个花时间。
在单个文章的按钮里我已经写好了跳转函数,就是 onPressed
中的代码,里面用到的 CupertinoPageRoute
主要是ios风格的滑动动画,咱们来实现详情页。
在 pages
文件夹下新建 articleDetail.dart
文件,flutter目前还不支持渲染 html
,所以咱们这里须要引入一个插件 flutter_html_view
,这个插件支持的标签也不是不少,但目前差很少够用了,为做者点个赞。打开 pubspec.yaml
文件,在 dependencies
下写入依赖:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 flutter_html_view: "^0.5.1" 复制代码
而后在 articleDetail.dart
顶部引入:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import '../config/httpHeaders.dart';
import 'package:flutter_html_view/flutter_html_view.dart';
复制代码
接着就是写页面了:
class ArticleDetail extends StatefulWidget {
final String objectId;
final Map articleInfo;
@override
ArticleDetail({Key key, this.objectId, this.articleInfo}) : super(key: key);
@override
ArticleDetailState createState() => new ArticleDetailState();
}
class ArticleDetailState extends State<ArticleDetail> {
Future getContent() async {
final String url =
'https://post-storage-api-ms.juejin.im/v1/getDetailData?uid=${httpHeaders['X-Juejin-Src']}&device_id=${httpHeaders['X-Juejin-Client']}&token=${httpHeaders['X-Juejin-Token']}&src=${httpHeaders['X-Juejin-Src']}&type=entryView&postId=${widget .objectId}';
final response = await http.get(Uri.encodeFull(url));
if (response.statusCode == 200) {
return json.decode(response.body)['d'];
} else {
throw Exception('Failed to load content');
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
var articleInfo = widget.articleInfo;
return new FutureBuilder(
future: getContent(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var content = snapshot.data['content'];
return new Scaffold(
appBar: new AppBar(
backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
leading: new IconButton(
padding: new EdgeInsets.all(0.0),
icon: new Icon(
Icons.chevron_left,
),
onPressed: () {
Navigator.pop(context);
}),
title: new Row(
children: <Widget>[
new CircleAvatar(
backgroundImage: new NetworkImage(
articleInfo['user']['avatarLarge']),
),
new Padding(padding: new EdgeInsets.only(right: 5.0)),
new Text(articleInfo['user']['username'])
],
),
actions: <Widget>[
new IconButton(
icon: new Icon(
Icons.file_upload,
color: Colors.blue,
),
onPressed: null)
],
),
bottomNavigationBar: new Container(
height: 50.0,
padding: new EdgeInsets.only(left: 10.0, right: 10.0),
decoration: new BoxDecoration(
color: new Color.fromRGBO(244, 245, 245, 1.0),
border: new Border(
top: new BorderSide(width: 0.2, color: Colors.grey))),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Row(
children: <Widget>[
new Icon(
Icons.favorite_border,
color: Colors.green,
size: 24.0,
),
new Padding(
padding: new EdgeInsets.only(right: 20.0)),
new Icon(
Icons.message,
color: Colors.grey,
size: 24.0,
),
new Padding(
padding: new EdgeInsets.only(right: 20.0)),
new Icon(
Icons.playlist_add,
color: Colors.grey,
size: 24.0,
)
],
),
new Text(
'喜欢 ${articleInfo['collectionCount']} · 评论 ${articleInfo['commentsCount']}')
],
),
),
);
} else if (snapshot.hasError) {
return new Container(
color: Colors.white,
child: new Text("error2>>>>>>>>>>>>>>>:${snapshot.error}"),
);
}
return new Container(
color: new Color.fromRGBO(244, 245, 245, 1.0),
child: new CupertinoActivityIndicator(),
);
});
}
}
复制代码
将html
写入页面的就是下面这段代码:
body: new ListView(
children: <Widget>[
new Container(
color: Colors.white,
child: new HtmlView(
data: content,
))
],
)
复制代码
细心的小伙伴会发现,bottomNavigationBar
中传入的是一个有高度的 Container
,这个很重要,flutter中的组件实际上是很灵活的,不要被官网提供的组件限制了,只要知足条件(好比bottomNavigationBar
必须传入 PreferredSizeWidget
),各类各样的自定义组件均可以用。
点赞、评论啥的咱们先不作,用过掘金app的小伙伴都知道,这些功能是须要登陆后才能用的,因此咱们放到后面来实现。
本身是初学flutter,最近也很忙,文中的错误和不足,还请你们原谅,欢迎指出,一块儿学习,今天就到这里了。源码点这里。