1. 程式人生 > >Flutter實戰詳解--高仿好奇心日報

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

前言

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

專案地址

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

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

先看看效果圖吧.

  • iOS效果圖

    iOS效果圖.gif

  • Android效果圖

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

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等會兒會自動生成.這裡需要掌握兩個知識點

[email protected]() 這是表示告訴編譯器這個類是需要生成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中的輪播圖我用到了Fluuter_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,根據shouwType欄位來判斷,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),),這個控制元件

總結

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

專案地址,歡迎Star

Github