最近Flutter一直比較火,我也它也是很是感興趣,看了下官網的基礎教程後我決定直接上手作一個App,一是這樣學的比較快印象更加深入,二是能夠記錄其中遇到的一些坑,幫助你們少走一些彎路.本篇文章我會盡量詳細的講到每個點上.css
下載項目後報錯是由於沒有添加依賴,在pubspec.yaml文件中點擊Packages get下載依賴,有時候會在這裏出現卡死的狀況,能夠配置一下環境變量,詳情請看修改Flutter環境變量.html
1.下載項目後報錯是由於沒有添加依賴,在pubspec.yaml文件中點擊Packages get下載依賴,有時候會在這裏出現卡死的狀況,能夠配置一下環境變量.在終端執行
vi ~/.bash_profile
,再添加export PUB_HOSTED_URL=https://pub.flutter-io.cn
和export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
.詳情請看修改Flutter環境變量.java
2.須要將File Encodings裏的
Project Encoding設置爲UTF-8
,不然有時候安卓會報錯ios
3.若是cocoapods不是最新可能會出現
Error Running Pod Install
,請更新cocoapods.git
4.因爲flutter_webview_plugin這個插件只支持加載url,因而就須要作一些修改.github
- iOS 在
FlutterWebviewPlugin.m
文件中的- (void)navigate:(FlutterMethodCall*)call
方法中的最後一排,將[self.webview loadRequest:request]
方法改成[self.webview loadHTMLString:url baseURL:nil]
- Android 在
WebViewManager.java
文件中webView.loadUrl(url)
方法改成webView.loadData(url, "text/html", "UTF-8")
,以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }
改成void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
先看看效果圖吧.
iOS效果圖 web
Android效果圖 json
怎麼搭建Flutter環境我就很少說了,官網上講的很詳細,尚未搭建開發環境的能夠看看這個Flutter中文網.數組
DefaultTabController
這個控件,使用
DefaultTabController
包裹須要用到Tab的頁面便可,它的child爲Scaffold,Scaffold有個appBar屬性,在AppBar中設置具體的樣式,你們看代碼會更加清楚.相關注釋也都寫上了.
home: new DefaultTabController(
length: titleList.length,
child: new Scaffold(
appBar: new AppBar(
elevation: 0.0,//導航欄下面那根線
title: new TabBar(
isScrollable: false,//是否可滑動
unselectedLabelColor: Colors.black26,//未選中按鈕顏色
labelColor: Colors.black,//選中按鈕顏色
labelStyle: TextStyle(fontSize: 18),//文字樣式
indicatorSize: TabBarIndicatorSize.label,//滑動的寬度是根據內容來適應,仍是與整塊那麼大(label表示根據內容來適應)
indicatorWeight: 4.0,//滑塊高度
indicatorColor: Colors.yellow,//滑動顏色
indicatorPadding: EdgeInsets.only(bottom: 1),//與底部距離爲1
tabs: titleList.map((String text) {//tabs表示具體的內容,是一個數組
return new Tab(
text: text,
);
}).toList(),
),
),
//body表示具體展現的內容
body:TabBarView(children: [News(url: 'http://app3.qdaily.com/app3/homes/index_v2/'),News(url: 'http://app3.qdaily.com/app3/papers/index/')]) ,
),
),
複製代碼
你們也能夠看看官網的示例Flutter官網示例bash
注意這裏圖片是緊貼着右邊屏幕的,因此這裏須要用到
Expanded
控件,用於自動填充子控件.
這裏須要注意的是,那個你猜這個圖片是堆疊在整個大圖上面的,因此須要用到
Stack
這個控件,其中Stack中有個屬性const FractionalOffset(double dx, double dy)
用於表示子控件相對於父控件的位置
用青花瓷抓取了好奇心數據.青花瓷使用教程
首先在pubspec.yaml中導入
dependencies: json_annotation: ^2.0.0 dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0
建立一個model.dart文件 引入文件
import 'package:json_annotation/json_annotation.dart'; part 'model.g.dart';
其中這個model.g.dart等會兒會自動生成.這裏須要掌握兩個知識點
1.@JsonSerializable() 這是表示告訴編譯器這個類是須要生成Model類的 2,@JsonKey 因爲服務器返回的部分數據名稱在Dart語言中是不被容許的,好比has_more,Dart中命名不能出現下劃線,因此就須要用到@JsonKey來告訴編譯器這個參數對於json中的哪一個字段
@JsonSerializable()
class Feed {
String image;
int type;
@JsonKey(name: 'index_type')
int indexType;
Post post;
@JsonKey(name: 'news_list')
List<News> newsList;
Feed(this.image,this.type,this.post,this.indexType,this.newsList);
factory Feed.fromJson(Map<String,dynamic> json) => _$FeedFromJson(json);
Map<String, dynamic> toJson() => _$FeedToJson(this);
}
複製代碼
好了,寫完後會報錯,由於FeedFromJson
和FeedToJson
沒有找到,這個時候在控制到輸入flutter packages pub run build_runner build
指令後會自動生成一個moded.g.dart
文件,因而在網絡請求下來數據後就能夠用Feed feed = Feed.fromJson(data)
這個方法來將Json中數據轉換保存在Feed這個實例中了.在model類中還有些複雜的Json嵌套,可是也都很簡單,你們看一眼應該就會了,哈哈.JSON和序列化具體教程
Flutter中的輪播圖我用到了Flutter_Swiper這個組件,這裏設置小圓點屬性的時候稍微麻煩了點,網上好像也沒有講到,我這裏講一下. 首先要建立DotSwiperPaginationBuilder
DotSwiperPaginationBuilder builder = DotSwiperPaginationBuilder(
color: Colors.white,//未選中圓點顏色
activeColor: Colors.yellow,//選中圓點顏色
size:7,//未選中大小
activeSize: 7,//選中圓點大小
space: 5//圓點間距
);
複製代碼
而後在Swiper中的pagination屬性中設置它
pagination: new SwiperPagination(
builder: builder,
),
複製代碼
StatefulWidget
,由於須要動態更新數據和列表. 網絡請求插件我用的Dio,很是好用. 在initState
方法中請求數據表示剛加載頁面的時候進行網絡請求,請求數據方法以下void getData()async{
if (lastKey == '0'){
dataList = [];//下拉刷新的時候將DataList制空
}
Dio dio = new Dio();
Response response = await dio.get("$url$lastKey.json");
Reslut reslut = Reslut.fromJson(response.data);
if(!reslut.response.hasMore){
return;//若是沒有數據就不繼續了
}
if(reslut.response.columns != null) {
columnList = reslut.response.columns;
}
lastKey = reslut.response.lastKey;//更新lastkey
setState(() {
if (reslut.response.banners != null){
banners = reslut.response.banners;//給輪播圖賦值
}
dataList.addAll(reslut.response.feeds);//給數據源賦值
});
}
複製代碼
由於用到了setState()方法,因此在該方法中改變了的數據會對其相應的地方進行刷新,好比設置了ListView的itemCount個數爲dataList.length,若是在SetState方法中dataList.length改變了,那麼ListView的itemCount樹也會自動改變並刷新ListView.
Flutter中有RefreshIndicator
用於下拉刷新,它有個onRefresh
閉包方法,表示下拉的時候執行的方法,通常用於網絡請求.onRefresh方法以下
Future<void> _handleRefresh() {
final Completer<void> completer = Completer<void>();
Timer(const Duration(seconds: 1), () {
completer.complete();
});
return completer.future.then<void>((_) {
lastKey = '0';
getData();
});
}
複製代碼
下拉加載的話須要初始化一個ScrollController
,將它設爲ListView的controller,並對其進行監聽,當滑動到最底部的時候進行網絡請求.
@override
void initState() {
url = widget.url;
getData();
_scrollController.addListener(() {
///判斷當前滑動位置是否是到達底部,觸發加載更多回調
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
getData();
}
});
}
final ScrollController _scrollController = new ScrollController();
複製代碼
上拉加載loading框用到了flutter_spinkit插件,提供了大量的加載樣式.
///上拉加載更多
Widget _buildProgressIndicator() {
///是否須要顯示上拉加載更多的loading
Widget bottomWidget = new Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
///loading框
new SpinKitThreeBounce(color: Color(0xFF24292E)),
new Container(
width: 5.0,
),
]);
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new Center(
child: bottomWidget,
),
);
}
複製代碼
因爲最上面有一個輪播圖,最下面有加載框,因此ListView的itemCount個數爲dataList.length+2,又由於每一個item之間都有一個淺灰色的風格線,因此須要用到ListView.separated
,具體代碼以下:
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh:(()=> _handleRefresh()),
color: Colors.yellow,//刷新控件的顏色
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _getListCount(),//item個數
controller: _scrollController,//用於監聽是否滑到最底部
itemBuilder: (context,index){
if(index == 0){
return SwiperWidget(context, banners);//若是是第一個,則展現banner
}else if(index < dataList.length + 1){
return WidgetUtils.GetListWidget(context, dataList[index - 1]);//展現數據
}else {
return _buildProgressIndicator();//展現加載loading框
}
},
separatorBuilder: (context,idx){//分割線
return Container(
height: 5,
color: Color.fromARGB(50,183, 187, 197),
);
},
),
);
}
複製代碼
這種的話也稍微複雜一點,有兩種樣式.而且到滑到最右邊的時候能夠繼續請求並加載數據.
data.insert(colunm.location, {'id':colunm.id,'showType':colunm.showType});
複製代碼
,再建立一個ColumnsListWidget
類,繼承自StatefulWidget,是一個新item,在滑動到該列表的位置的時候,會將該Map數據傳給ColumnsListWidget
,這個時候ColumnsListWidget
就會加載數據並展現出來了,滑到最右邊的時候加載和滑到最底部加載的方法同樣,就很少說了.具體能夠查看源碼,關鍵代碼以下:
static Widget GetListWidget(BuildContext context, dynamic data) {
Widget widget;
if(data.runtimeType == Feed) {
if (data.indexType != null) {
widget = NewsListWidget(context, data);
} else if (data.type == 2) {
widget = ListImageTop(context, data);
} else if (data.type == 0) {
widget = ActivityWidget(context, data);
} else if (data.type == 1) {
widget = ListImageRight(context, data);
}
}else{
widget = ColumnsListWidget(id: data['id'],showType: data['showType'],);
}
複製代碼
1.橫向ListView外須要用
Flexible
包裹,Flexible組件可使Row、Column、Flex等子組件在主軸方向有填充可用空間的能力(例如,Row在水平方向,Column在垂直方向),可是它與Expanded組件不一樣,它不強制子組件填充可用空間。 2.ListView初始位置用到padding: new EdgeInsets.symmetric(horizontal: 12.0)
,用padding: EdgeInsets.only(left: 12)
的話會讓ListView和最左邊一直有條線
FlutterWebviewPlugin.m
文件中的- (void)navigate:(FlutterMethodCall*)call
方法中的最後一排,將[self.webview loadRequest:request]
方法改成[self.webview loadHTMLString:url baseURL:nil]
WebViewManager.java
文件中webView.loadUrl(url)
方法改成webView.loadData(url, "text/html", "UTF-8")
,以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }
改成void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
因爲服務器端返回的Html中的css和js文件地址是/assets/app3
開頭的,因此須要替換成絕對路徑,因此要用到這個方法htmlBody.replaceAll( '/assets/app3','http://app3.qdaily.com/assets/app3')
好了,這下就能夠呈現出漂亮的網頁了.在點擊橫向滑動列表的總標題的時候,會進入到相關欄目的詳情頁,如圖
下面就是一個GridView,不過有時候下面會是ListView,根據showType字段來判斷,GridView的代碼以下:
Widget ColumnsDetailTypeTwo(BuildContext context,List<Feed> feesList){
return GridView.count(
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 2,
shrinkWrap: true,
mainAxisSpacing: 10.0,
crossAxisSpacing: 15.0,
childAspectRatio: 0.612,
padding: new EdgeInsets.symmetric(horizontal: 20.0),
children: feesList.map((Feed feed) {
return ColumnsTypeTwoTile(context, feed);
}).toList()
);
}
複製代碼
其中 childAspectRatio
表示寬高比.
圓角頭像須要用到
CircleAvatar(backgroundImage:NetworkImage(url),),
這個控件
在切換頂部tab的時候會發現下面的界面會自動滑動到頂(位置重置)並執行initState,同時每次滑到橫向ListView的時候,它也會執行initState而且位置也會重置,要讓它只執行一次initState方法的話須要這麼作.
class _XXXState extends State<XXX> with AutomaticKeepAliveClientMixin{
@override
bool get wantKeepAlive => true;
複製代碼
這樣它就會只執行一次initState方法了.
作了這個項目最大的感覺就是界面佈局是真的很方便很簡單,由於作了一遍對不少知識點也理解的更深了.若是以爲有幫助到你的話,但願能夠給個 Star