Flutter實戰詳解–高仿好奇心日報

Lucky_Xu發表於2019-01-06

前言

最近Flutter一直比較火,我也它也是非常感興趣,看了下官網的基礎教程後我決定直接上手做一個App,一是這樣學的比較快印象更加深刻,二是可以記錄其中遇到的一些坑,幫助大家少走一些彎路.本篇文章我會盡可能詳細的講到每一個點上.

專案地址

Github,如果覺得不錯,歡迎Star

下載專案後報錯是因為沒有新增依賴,在pubspec.yaml檔案中點選Packages get下載依賴,有時候會在這裡出現卡死的情況,可以配置一下環境變數,詳情請看修改Flutter環境變數.

注意事項

1.下載專案後報錯是因為沒有新增依賴,在pubspec.yaml檔案中點選Packages get下載依賴,有時候會在這裡出現卡死的情況,可以配置一下環境變數.在終端執行vi ~/.bash_profile,再新增export PUB_HOSTED_URL=https://pub.flutter-io.cnexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn.詳情請看修改Flutter環境變數.

2.需要將File Encodings裡的Project Encoding設定為UTF-8,否則有時候安卓會報錯

3.如果cocoapods不是最新可能會出現Error Running Pod Install,請更新cocoapods.

4.由於flutter_webview_plugin這個外掛只支援載入url,於是就需要做一些修改.

  • 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效果圖

    iOS效果圖.gif
  • Android效果圖

    Android效果圖.gif

正題

怎麼搭建Flutter環境我就不多說了,官網上講的很詳細,還沒有搭建開發環境的可以看看這個Flutter中文網.

1導航欄Tabbar

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官網示例

2. 不同樣式的item

  • 樣式一
    Flutter實戰詳解–高仿好奇心日報

    這種佈局的大概結構如下

    Flutter實戰詳解–高仿好奇心日報

注意這裡圖片是緊貼著右邊螢幕的,所以這裡需要用到Expanded控制元件,用於自動填充子控制元件.

  • 樣式二
    Flutter實戰詳解–高仿好奇心日報

    這個樣式的控制元件佈局就很簡單了,結構如下

    Flutter實戰詳解–高仿好奇心日報
  • 樣式三
    Flutter實戰詳解–高仿好奇心日報

    這個和樣式二差不多,只不過最上面多了一塊.

這裡需要注意的是,那個你猜這個圖片是堆疊在整個大圖上面的,所以需要用到Stack這個控制元件,其中Stack中有個屬性const FractionalOffset(double dx, double dy)用於表示子控制元件相對於父控制元件的位置

  • 樣式四
    Flutter實戰詳解–高仿好奇心日報

    這種樣式稍微複雜一點,結構如下

    Flutter實戰詳解–高仿好奇心日報

3資料抓取

用青花瓷抓取了好奇心資料.青花瓷使用教程

image.png

簡單分析一下,has_more表示是否可以載入更多,last_key用於上拉載入的時候請求用的,feeds就是每一條資料,banners就是輪播圖的資訊,columns就是橫向滾動的ListView的相關資料,這個後面講.接下來就做json序列化相關的了.

4.Json序列化

首先在pubspec.yaml中匯入

dependencies:json_annotation: ^2.0.0dev_dependencies:build_runner: ^1.0.0json_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);

}複製程式碼

好了,寫完後會報錯,因為FeedFromJsonFeedToJson沒有找到,這個時候在控制到輸入flutter packages pub run build_runner build指令後會自動生成一個moded.g.dart檔案,於是在網路請求下來資料後就可以用Feed feed = Feed.fromJson(data)這個方法來將Json中資料轉換儲存在Feed這個例項中了.在model類中還有些複雜的Json巢狀,但是也都很簡單,大家看一眼應該就會了,哈哈.JSON和序列化具體教程

5.輪播圖

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,        ),複製程式碼
  1. 網路請求首先,展示頁面要繼承自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.

7. 上拉重新整理與載入

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外掛,提供了大量的載入樣式.

Flutter實戰詳解–高仿好奇心日報

程式碼如下

///上拉載入更多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, ), );

}複製程式碼

8. ListView賦值

由於最上面有一個輪播圖,最下面有載入框,所以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), );

}, ), );

}複製程式碼

9. ListView巢狀橫向滑動ListView

這種的話也稍微複雜一點,有兩種樣式.並且到滑到最右邊的時候可以繼續請求並載入資料.

Flutter實戰詳解–高仿好奇心日報
Flutter實戰詳解–高仿好奇心日報

首先來分析一下資料

Flutter實戰詳解–高仿好奇心日報

這個colunmns就是橫向滑動列表的重要資料.

Flutter實戰詳解–高仿好奇心日報

裡面的id是請求引數,show_type表示列表的樣式,location表示插入的位置.而且通過抓取介面發現,當橫向列表快要展示出來的時候,才會去請求橫向列表的具體介面.那麼思路就很清晰了,在請求獲得資料後遍歷colunmns,根據每個colunmn的location插入一個Map,如下

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和最左邊一直有條線

10.webview載入複雜的Html欄位

Flutter實戰詳解–高仿好奇心日報

獲取到網頁詳情的資料發現是Html欄位,並且其中的css是url地址,試了很多Flutter載入Html的外掛發現樣式都不正確,最後決定使用原生和Flutter混編,這時候發現flutter_webview_plugin這個外掛是使用原生網頁的,不過它只支援載入url,於是就需要做一些修改.

  • 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");

    }

    由於伺服器端返回的Html中的css和js檔案地址是/assets/app3開頭的,所以需要替換成絕對路徑,所以要用到這個方法htmlBody.replaceAll( '/assets/app3','http://app3.qdaily.com/assets/app3')好了,這下就可以呈現出漂亮的網頁了.

11.ListView巢狀GridView

在點選橫向滑動列表的總標題的時候,會進入到相關欄目的詳情頁,如圖

Flutter實戰詳解–高仿好奇心日報

這個ListView包含上下兩部分.上面這部分為:

Flutter實戰詳解–高仿好奇心日報

結構如下

Flutter實戰詳解–高仿好奇心日報

下面就是一個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),),這個控制元件

12 在切換Tab的時候防止執行initState

在切換頂部tab的時候會發現下面的介面會自動滑動到頂(位置重置)並執行initState,同時每次滑到橫向ListView的時候,它也會執行initState並且位置也會重置,要讓它只執行一次initState方法的話需要這麼做.

class _XXXState extends State<
XXX>
with AutomaticKeepAliveClientMixin{
@override bool get wantKeepAlive =>
true;
複製程式碼

這樣它就會只執行一次initState方法了.

總結

做了這個專案最大的感受就是介面佈局是真的很方便很簡單,因為做了一遍對很多知識點也理解的更深了.如果覺得有幫助到你的話,希望可以給個 Star

專案地址,歡迎Star

Github

來源:https://juejin.im/post/5c31f7236fb9a04a04412d0b

相關文章