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

前言

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

項目地址

Github,若是以爲不錯,歡迎Star

下載項目後報錯是由於沒有添加依賴,在pubspec.yaml文件中點擊Packages get下載依賴,有時候會在這裏出現卡死的狀況,能夠配置一下環境變量,詳情請看修改Flutter環境變量.html

注意事項

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環境變量.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

    iOS效果圖.gif

  • Android效果圖 json

    Android效果圖.gif

正題

怎麼搭建Flutter環境我就很少說了,官網上講的很詳細,尚未搭建開發環境的能夠看看這個Flutter中文網.數組

1導航欄Tabbar

這裏我用到了 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

2. 不一樣樣式的item

  • 樣式一
    這種佈局的大概結構以下

注意這裏圖片是緊貼着右邊屏幕的,因此這裏須要用到Expanded控件,用於自動填充子控件.

  • 樣式二
    這個樣式的控件佈局就很簡單了,結構以下
  • 樣式三
    這個和樣式二差很少,只不過最上面多了一塊.

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

  • 樣式四
    這種樣式稍微複雜一點,結構以下

3數據抓取

用青花瓷抓取了好奇心數據.青花瓷使用教程

image.png
簡單分析一下,has_more表示是否能夠加載更多,last_key用於上拉加載的時候請求用的,feeds就是每一條數據,banners就是輪播圖的信息,columns就是橫向滾動的ListView的相關數據,這個後面講.接下來就作json序列化相關的了.

4.Json序列化

首先在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);
}
複製代碼

好了,寫完後會報錯,由於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插件,提供了大量的加載樣式.

代碼以下

///上拉加載更多
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

這種的話也稍微複雜一點,有兩種樣式.而且到滑到最右邊的時候能夠繼續請求並加載數據.

首先來分析一下數據
這個colunmns就是橫向滑動列表的重要數據.
裏面的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字段

獲取到網頁詳情的數據發現是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

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

這個ListView包含上下兩部分.上面這部分爲:
結構以下

下面就是一個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

相關文章
相關標籤/搜索