深度解析Flutter開發大廠App(強烈推薦,值得收藏)

之前,也寫過幾篇關於 Flutter 的博文,最近,又花了一些時間學習研究 Flutter,完成了高仿大廠 App 項目 (項目使用的接口都是來自線上真實App抓包而來,可以做到和線上項目相同的效果),也總結積累了一些小技巧和知識點,所以,在這裏記錄分享出來,也希望 Flutter 生態越來越好 (flutter開發App效率真的很高,開發體驗也是很好的 🙂)

以下博文會分爲3個部分概述:

  • 項目結構分析
  • 項目功能詳細概述(所用知識點)
  • 小技巧積累總結

項目結構分析

其次,梳理下項目的目錄結構,理解每個文件都是幹什麼的,我們先來看看一級目錄,如下:

├── README.md  # 描述文件
├── android    # android 宿主環境
├── build      # 項目構建目錄,由flutter自動完成
├── flutter_ctrip.iml
├── fonts      # 自己創建的目錄,用於存放字體
├── images     # 自己創建的目錄,用於存放圖片
├── ios        # iOS 宿主環境
├── lib        # flutter 執行文件,自己寫的代碼都在這
├── pubspec.lock # 用來記錄鎖定插件版本
├── pubspec.yaml # 插件及資源配置文件
└── test       # 測試目錄

這個就不用多解釋,大多是 flutter 生成及管理的,我們需要關注的是 lib 目錄。

我們再來看看二級目錄,如下 (重點關注下lib目錄)

├── README.md
├── android
│   ├── android.iml
  ...
│   └── settings.gradle
├── build
│   ├── app
  ...
│   └── snapshot_blob.bin.d.fingerprint
├── flutter_ctrip.iml
├── fonts
│   ├── PingFang-Italic.ttf
│   ├── PingFang-Regular.ttf
│   └── PingFang_Bold.ttf
├── images
│   ├── grid-nav-items-dingzhi.png
  ...
│   └── yuyin.png
├── iOS
│   ├── Flutter
  ...
│   └── ServiceDefinitions.json
├── lib
│   ├── dao           # 請求接口的類
│   ├── main.dart     # flutter 入口文件
│   ├── model         # 實體類,把服務器返回的 json 數據,轉換成 dart 類
│   ├── navigator     # bottom bar 首頁底部導航路由
│   ├── pages         # 所以的頁面
│   ├── plugin        # 封裝的插件
│   ├── util          # 工具類,避免重複代碼,封裝成工具類以便各個 page 調用
│   └── widget        # 封裝的組件
├── pubspec.lock
├── pubspec.yaml
└── test
    └── widget_test.dart

再來看看,lib 目錄下二級目錄,看看整個項目創建了多少個文件,寫了多少代碼,如下 (其實,並不是很多)

├── dao/
│   ├── destination_dao.dart*
│   ├── destination_search_dao.dart*
│   ├── home_dao.dart
│   ├── search_dao.dart*
│   ├── trave_hot_keyword_dao.dart*
│   ├── trave_search_dao.dart*
│   ├── trave_search_hot_dao.dart*
│   ├── travel_dao.dart*
│   ├── travel_params_dao.dart*
│   └── travel_tab_dao.dart*
├── main.dart
├── model/
│   ├── common_model.dart
│   ├── config_model.dart
│   ├── destination_model.dart
│   ├── destination_search_model.dart
│   ├── grid_nav_model.dart
│   ├── home_model.dart
│   ├── sales_box_model.dart
│   ├── seach_model.dart*
│   ├── travel_hot_keyword_model.dart
│   ├── travel_model.dart*
│   ├── travel_params_model.dart*
│   ├── travel_search_hot_model.dart
│   ├── travel_search_model.dart
│   └── travel_tab_model.dart
├── navigator/
│   └── tab_navigater.dart
├── pages/
│   ├── destination_page.dart
│   ├── destination_search_page.dart
│   ├── home_page.dart
│   ├── my_page.dart
│   ├── search_page.dart
│   ├── speak_page.dart*
│   ├── test_page.dart
│   ├── travel_page.dart
│   ├── travel_search_page.dart
│   └── travel_tab_page.dart*
├── plugin/
│   ├── asr_manager.dart*
│   ├── side_page_view.dart
│   ├── square_swiper_pagination.dart
│   └── vertical_tab_view.dart
├── util/
│   └── navigator_util.dart*
└── widget/
    ├── grid_nav.dart
    ├── grid_nav_new.dart
    ├── loading_container.dart
    ├── local_nav.dart
    ├── sales_box.dart
    ├── scalable_box.dart
    ├── search_bar.dart*
    ├── sub_nav.dart
    └── webview.dart

整個項目就是以上這些文件了 (具體的就不一個一個分析了,如,感興趣,大家可以 clone 源碼運行起來,自然就清除了)

項目功能詳細概述(所用知識點)

首先,來看看首頁功能及所用知識點,首頁重點看下以下功能實現:

  • 漸隱漸現的 appBbar
  • 搜索組件的封裝
  • 語音搜索頁面
  • banner組件
  • 浮動的 icon 導航
  • 漸變不規則帶有背景圖的網格導航

漸隱漸現的 appBbar

先來看看具體效果,一睹芳容,如圖:

image

滾動的時候 appBar 背景色從透明變成白色或白色變成透明,這裏主要用了 flutterNotificationListener 組件,它會去監聽組件樹冒泡事件,當被它包裹的的組件*(子組件)* 發生變化時,Notification 回調函數會被觸發,所以,通過它可以去監聽頁面的滾動,來動態改變 appBar 的透明度*(alpha)*,代碼如下:

NotificationListener(
  onNotification: (scrollNotification) {
    if (scrollNotification is ScrollUpdateNotification &&
        scrollNotification.depth == 0) {
      _onScroll(scrollNotification.metrics.pixels);
    }
    return true;
  },
  child: ...

Tips:
scrollNotification.depth 的值 0 表示其子組件*(只監聽子組件,不監聽孫組件)*;
scrollNotification is ScrollUpdateNotification 來判斷組件是否已更新,ScrollUpdateNotification 是 notifications 的生命週期一種情況,分別有一下幾種:

  • ScrollStartNotification 組件開始滾動
  • ScrollUpdateNotification 組件位置已經發生改變
  • ScrollEndNotification 組件停止滾動
  • UserScrollNotification 不清楚

這裏,我們不探究太深入,如想了解可多查看源碼。

_onScroll 方法代碼如下:

  void _onScroll(offset) {
    double alpha = offset / APPBAR_SCROLL_OFFSET;  // APPBAR_SCROLL_OFFSET 常量,值:100;offset 滾動的距離

    //把 alpha 值控制值 0-1 之間
    if (alpha < 0) {
      alpha = 0;
    } else if (alpha > 1) {
      alpha = 1;
    }
    setState(() {
      appBarAlpha = alpha;
    });
    print(alpha);
  }

搜索組件的封裝

搜索組件效果如圖:

以下是首頁調用 searchBar 的代碼:

SearchBar(
  searchBarType: appBarAlpha > 0.2  //searchBar 的類:暗色、亮色
      ? SearchBarType.homeLight
      : SearchBarType.home,
  inputBoxClick: _jumpToSearch,     //點擊回調函數
  defaultText: SEARCH_BAR_DEFAULT_TEXT,   // 提示文字
  leftButtonClick: () {},           //左邊邊按鈕點擊回調函數
  speakClick: _jumpToSpeak,         //點擊話筒回調函數
  rightButtonClick: _jumpToUser,    //右邊邊按鈕點擊回調函數
),

其實就是用 TextField 組件,再加一些樣式,需要注意點是:onChanged,他是 TextField 用來監聽文本框是否變化,通過它我們來監聽用戶輸入,來請求接口數據;
具體的實現細節,請查閱源碼: 點擊查看searchBar源碼

語音搜索頁面

語音搜索頁面效果如圖:由於模擬器無法錄音,所以無法展示正常流程,如果錄音識別成功後會返回搜索頁面,在項目預覽視頻中可以看到正常流程。

[圖片上傳失敗…(image-190ee2-1589860267422)]

語音搜索功能使用的是百度的語言識別SDK,原生接入之後,通過 MethodChannel 和原生Native端通信,這裏不做重點講述(這裏會涉及原生Native的知識)。

重點看看點擊錄音按鈕時的動畫實現,這個動畫用了 AnimatedWidget 實現的,代碼如下:

class AnimatedWear extends AnimatedWidget {
  final bool isStart;
  static final _opacityTween = Tween<double>(begin: 0.5, end: 0); // 設置透明度變化值
  static final _sizeTween = Tween<double>(begin: 90, end: 260);   // 設置圓形線的擴散值

  AnimatedWear({Key key, this.isStart, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;  // listenable 繼承 AnimatedWidget,其實就是控制器,會自動監聽組件的變化
    return Container(
      height: 90,
      width: 90,
      child: Stack(
        overflow: Overflow.visible,
        alignment: Alignment.center,
        children: <Widget>[
          ...
          // 擴散的圓線,其實就是用一個圓實現的,設置圓爲透明,設置border
          Positioned(
            left: -((_sizeTween.evaluate(animation) - 90) / 2), // 根據 _sizeTween 動態設置left偏移值
            top: -((_sizeTween.evaluate(animation) - 90) / 2), //  根據 _sizeTween 動態設置top偏移值
            child: Opacity(
              opacity: _opacityTween.evaluate(animation),      // 根據 _opacityTween 動態設置透明值
              child: Container(
                width: isStart ? _sizeTween.evaluate(animation) : 0, // 設置 寬
                height: _sizeTween.evaluate(animation),              // 設置 高
                decoration: BoxDecoration(
                    color: Colors.transparent,
                    borderRadius: BorderRadius.circular(
                        _sizeTween.evaluate(animation) / 2),
                    border: Border.all(
                      color: Color(0xa8000000),
                    )),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

其他細節,如:點擊時提示錄音,錄音失敗提示,點擊錄音按鈕出現半透明黑色圓邊框,停止後消失等,請查看源碼

banner組件

效果如圖:

banner使用的是flutter的 flutter_swiper 插件實現的,代碼如下:

Swiper(
  itemCount: bannerList.length,              // 滾動圖片的數量
  autoplay: true,                            // 自動播放
  pagination: SwiperPagination(              // 指示器
      builder: SquareSwiperPagination(
        size: 6,                             // 指示器的大小
        activeSize: 6,                       // 激活狀態指示器的大小
        color: Colors.white.withAlpha(80),   // 顏色
        activeColor: Colors.white,           // 激活狀態的顏色
      ),
    alignment: Alignment.bottomRight,        // 對齊方式
    margin: EdgeInsets.fromLTRB(0, 0, 14, 28), // 邊距
  ),
  itemBuilder: (BuildContext context, int index) { // 構造器
    return GestureDetector(
      onTap: () {
        CommonModel model = bannerList[index];
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => WebView(
              url: model.url,
            ),
          ),
        );
      },
      child: Image.network(
        bannerList[index].icon,
        fit: BoxFit.fill,
      ),
    );
  },
),

具體使用方法,可以去 flutter的官方插件庫 pub.dev 查看:點擊flutter_swiper查看

Tips:
需要注意的是,我稍改造了一下指示器的樣式,flutter_swiper 只提供了 3 種指示器樣式,如下:

  • dots = const DotSwiperPaginationBuilder(),圓形
  • fraction = const FractionPaginationBuilder(),百分數類型的,如:1/6,表示6頁的第一頁
  • rect = const RectSwiperPaginationBuilder(),矩形

並沒有上圖的激活狀態的長橢圓形,其實就是按葫蘆畫瓢,自己實現一個長橢圓類型,如知詳情,可點擊查看長橢圓形指示器源碼

浮動的 icon 導航

icon導航效果如圖:

icon導航浮動在banner之上,其實用的是 flutterStack 組件,Stack 組件能讓其子組件堆疊顯示,它通常和 Positioned 組件配合使用,佈局結構代碼如下:

ListView(
  children: <Widget>[
    Container(
      child: Stack(
        children: <Widget>[
          Container( ... ), //這裏放的是banner的代碼
          Positioned( ... ), //這個就是icon導航,通過 Positioned 固定顯示位置
        ],
      ),
    ),
    Container( ... ), // 這裏放的網格導航及其他
  ],
),

漸變不規則帶有背景圖的網格導航

網格導航效果如圖:

如圖,網格導航分爲三行四欄,而第一行分爲三欄,每一行的第一欄寬度大於其餘三欄,其餘三欄均等,每一行都有漸變色,而且第一、二欄都有背景圖;
flutterColumn 組件能讓子組件豎軸排列, Row 組件能讓子組件橫軸排列,佈局代碼如下:

Column(                      // 最外面放在 Column 組件
  children: <Widget>[
    Container(               // 第一行包裹 Container 設置其漸變色
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //設置漸變色
          Color(0xfffa5956),
          Color(0xffef9c76).withAlpha(45)
        ]),
      ),
      child: Row( ... ),    // 第一行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),  // 設置行直接的間隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //設置漸變色
          Color(0xff4b8fed),
          Color(0xff53bced),
        ]),
      ),
      child: Row( ... ),  // 第二行
    ),
    Padding(
      padding: EdgeInsets.only(top: 1),   // 設置行直接的間隔
    ),
    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [  //設置漸變色
          Color(0xff34c2aa),
          Color(0xff6cd557),
        ]),
      ),
      child: Row( ... ),  // 第三行
    ),
  ],
),

其實,具體實現的細節還是很多的,比如:

  • 怎麼設置第一欄寬度偏大,其他均等;
  • 第一行最後一欄寬度是其他的2倍;
  • 第一、二欄的別截圖及浮動的紅色氣泡tip等;

在這裏就不細講,否則篇幅太長,如想了解詳情 點擊查看源碼

其次,再來看看目的地頁面功能及所用知識點,重點看下以下功能實現:

  • 左右佈局tabBarListView
  • 目的地搜索頁面

左右佈局tabBarListView

具體效果如圖:點擊左邊標籤可以切換頁面,左右滑動也可切換頁面,點擊展開顯示更多等

其實官方已經提供了 tabBarTabBarView 組件可以實現上下佈局的效果*(旅拍頁面就是用這個實現的)*,但是它無法實現左右佈局,而且不太靈活,所以,我使用的是 vertical_tabs插件, 代碼如下:

VerticalTabView(
    tabsWidth: 88,
    tabsElevation: 0,
    indicatorWidth: 0,
    selectedTabBackgroundColor: Colors.white,
    backgroundColor: Colors.white,
    tabTextStyle: TextStyle(
      height: 60,
      color: Color(0xff333333),
    ),
    tabs: tabs,
    contents: tabPages,
  ),
),

具體使用方法,在這裏就不贅述了,點擊vertical_tabs查看

Tips:
這裏需要注意的是:展開顯示更多span標籤組件的實現,因爲,這個組件在很多的其他組件裏用到而且要根據接口數據動態渲染,且組件自身存在狀態的變化,這種情況下,最好是把他單獨封裝成一個組件*(widget)*,否則,很難控制自身狀態的變化,出現點擊沒有效果,或點擊影響其他組件。

目的地搜索頁面

效果如圖:點擊搜索結果,如:點擊‘一日遊‘,會搜索到‘一日遊‘的相關數據

目的地搜索頁面,大多都是和佈局和對接接口的代碼,在這裏就不再贅述。

然後就是旅拍頁面功能及所用知識點,重點看下以下功能實現:

  • 左右佈局tabBarListView
  • 瀑布流卡片
  • 旅拍搜索頁

左右佈局tabBarListView

效果如圖:可左右滑動切換頁面,上拉加載更多,下拉刷新等

這個是flutter 提供的組件,tabBarTabBarView,代碼如下:

Container(
  color: Colors.white,
  padding: EdgeInsets.only(left: 2),
  child: TabBar(
    controller: _controller,
    isScrollable: true,
    labelColor: Colors.black,
    labelPadding: EdgeInsets.fromLTRB(8, 6, 8, 0),
    indicatorColor: Color(0xff2FCFBB),
    indicatorPadding: EdgeInsets.all(6),
    indicatorSize: TabBarIndicatorSize.label,
    indicatorWeight: 2.2,
    labelStyle: TextStyle(fontSize: 18),
    unselectedLabelStyle: TextStyle(fontSize: 15),
    tabs: tabs.map<Tab>((Groups tab) {
      return Tab(
        text: tab.name,
      );
    }).toList(),
  ),
),
Flexible(
    child: Container(
  padding: EdgeInsets.fromLTRB(6, 3, 6, 0),
  child: TabBarView(
      controller: _controller,
      children: tabs.map((Groups tab) {
        return TravelTabPage(
          travelUrl: travelParamsModel?.url,
          params: travelParamsModel?.params,
          groupChannelCode: tab?.code,
        );
      }).toList()),
)),

瀑布流卡片

瀑布流卡片 用的是 flutter_staggered_grid_view 插件,代碼如下:

StaggeredGridView.countBuilder(
  controller: _scrollController,
  crossAxisCount: 4,
  itemCount: travelItems?.length ?? 0,
  itemBuilder: (BuildContext context, int index) => _TravelItem(
        index: index,
        item: travelItems[index],
      ),
  staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
  mainAxisSpacing: 2.0,
  crossAxisSpacing: 2.0,
),

如下了解更多相關信息,點擊flutter_staggered_grid_view查看

旅拍搜索頁

效果如圖:首先顯示熱門旅拍標籤,點擊可搜索相關內容,輸入關鍵字可搜索相關旅拍信息,地點、景點、用戶等

旅拍搜索頁,大多也是和佈局和對接接口的代碼,在這裏就不再贅述。

小技巧積累總結

以下都是我在項目裏使用的知識點,在這裏記錄分享出來,希望能幫到大家。

PhysicalModel

PhysicalModel 可以裁剪帶背景圖的容器,如,你在一個 Container 裏放了一張圖片,想設置圖片圓角,設置 Container 的 decoration 的 borderRadius 是無效的,這時候就要用到 PhysicalModel,代碼如下:

PhysicalModel(
  borderRadius: BorderRadius.circular(6),  // 設置圓角
  clipBehavior: Clip.antiAlias,            // 裁剪行爲
  color: Colors.transparent,               // 顏色
  elevation: 5,                            // 設置陰影
  child: Container(
        child: Image.network(
          picUrl,
          fit: BoxFit.cover,
        ),
      ),
),

LinearGradient

給容器添加漸變色,在網格導航、appBar等地方都使用到,代碼如下:

Container(
  height: 72,
  decoration: BoxDecoration(
    gradient: LinearGradient(colors: [
      Color(0xff4b8fed),
      Color(0xff53bced),
    ]),
  ),
  child: ...
),

Color(int.parse(‘0xff’ + gridNavItem.startColor))

顏色值轉換成顏色,如果,沒有變量的話,也可直接這樣用 Color(0xff53bced)

  • ox:flutter要求,可固定不變
  • ff:代表透明貼,不知道如何設置的話,可以用取色器,或者 withOpacity(opacity) 、 withAlpha(a)
  • 53bced: 常規的6位RGB值

Expanded、FractionallySizedBox

Expanded 可以讓子組件撐滿父容器,通常和 RowColumn 組件搭配使用;


FractionallySizedBox 可以讓子組件撐滿或超出父容器,可以單獨使用,大小受 widthFactor 和 heightFactor 寬高因子的影響

MediaQuery.removePadding

MediaQuery.removePadding 可以移除組件的邊距,有些組件自帶有邊距,有時候佈局的時候,不需要邊距,這時候就可以用 MediaQuery.removePadding,代碼如下:

MediaQuery.removePadding(
  removeTop: true,
  context: context,
  child: ...
)

MediaQuery.of(context).size.width

MediaQuery.of(context).size.width 獲取屏幕的寬度,同理,MediaQuery.of(context).size.height 獲取屏幕的高度;
如,想一行平均3等分: 0.3 * MediaQuery.of(context).size.width,在目的地頁面的標籤組件就使用到它,代碼如下:

Container(
  alignment: Alignment.center,
  ...
  width: 0.3*MediaQuery.of(context).size.width - 12, // 屏幕平分三等分, - 12 是給每份中間留出空間 
  height: 40,
  ...
  child: ...
),

Theme.of(context).platform == TargetPlatform.iOS

判斷操作系統類型,有時候可能有給 Andorid 和 iOS 做出不同的佈局,就需要用到它。

with AutomaticKeepAliveClientMixin

flutter 在切換頁面時候每次都會重新加載數據,如果想讓頁面保留狀態,不重新加載,就需要使用 AutomaticKeepAliveClientMixin,代碼如下:(在旅拍頁面就有使用到它,爲了讓tabBar 和 tabBarView在切換時不重新加載)

class TravelTabPage extends StatefulWidget {
  ...
  //需要重寫 wantKeepAlive 且 設置成 true
  @override
  bool get wantKeepAlive => true;
}

推薦閱讀:2017-2020歷年字節跳動Android面試真題解析(累計下載1082萬次,持續更新中)
字節跳動面試題 —— 水壺問題

重磅來襲!阿里P7“青春修煉手冊”(全網獨家首發!)

暫時只能想到這些常用的知識點,以後如有新的會慢慢補充。

作者:子木_lsy
鏈接:https://www.jianshu.com/p/6ddeb5ee6619

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章