Flutter 入門與實戰(五十三):仿掘金個人主頁,學習 FutureProvider 狀態管理

島上碼農發表於2021-08-16

這是我參與8月更文挑戰的第16天,活動詳情檢視:8月更文挑戰

前言

好久沒有講介面的內容了,本篇借仿掘金個人主頁的頭部區域,一方面是講一下 Flutter 的 Stack 層疊元件的用法,另一方面是 Provider 的 FutureProvider 的使用。宣告一下,仿的掘金手機端個人主頁頭部區域如下圖,實際還有底部的滑動後懸停部分沒有完成,後續有時間的時候再完善整個介面,來個“假冒”掘金個人主頁。

image.png

介面分析

拿到介面,我們首先分析一下介面的結構,頭部這部分從下到上由如下內容組成:

  • 底圖和頭像
    • 底部 Banner 圖:在最底部;
    • 頭像:包括頭像圖片下的背景圓(或邊框),疊加在底部 Banner 上;
  • 使用者名稱稱、級別、工作資訊(頭銜及公司)及個人介紹等個人資訊:在底圖和頭像下方。
  • 關注、關注者和掘力值等統計資料:在使用者資訊下方。
  • 返回按鈕:返回按鈕在整個頁面的頂層,以便可以隨時點選返回。

得到我們頁面的佈局結構如下圖所示。其中頭像和 Banner 因為重疊了,需要使用一個 Stack 元件包裹,即藍色的區域。 個人主頁.png

元件結構

確定好了佈局,我們再來確定使用什麼樣的元件,各個佈局區域對應的元件如下:

  • 返回按鈕:IconButton,使用一個返回圖示按鈕,點選後返回上一頁。
  • Banner:使用 CachedImageNetWork,以便可以載入網路圖片。
  • 頭像:使用一個 Stack,底部是實現邊框的圓形 Container,上層是圓形頭像,使用 Container+CachedImageNetwork 實現。 同時整個頭像區域使用 Positioned 絕對位置佈局,保持左側和返回按鈕對齊,然後由有一半區域疊加在Banner上。
  • 個人資訊區域:使用列布局 Column排布各項資訊,然後暱稱和等級使用行佈局 Row
  • 統計資訊區域:使用行佈局 Row 排布各個統計資料,統計資料本身使用 Column 佈局數字和資料項名稱。

整個頁面使用的是 CustomScrollView,然後再用 Stack包裹整個頁面和返回按鈕,並將返回按鈕使用 Positioned 絕對定位保持在左上角。最後的元件樹如下(省略了Container元件)。

個人主頁元件樹.png

重點介紹一下 StackPostioned 元件。Stack 元件分為StackIndexedStack。其中IndexedStack 我們在Flutter入門與實戰(三):構建一個常用的頁面框架有介紹過,其實就是一個有序的 Stack,可以通過控制當前序號顯示第幾層的介面。而 Stack 的 children 是一組元件,使用的是堆疊排序,次序在後面的層級越高,顯示層級也更靠前(後進先出原則)。通過這種方式可以達到多個介面層疊顯示的目的。

掘金個人主頁-堆疊次序.png

Positioned 元件是專門用於 Stack 的子元件,用於控制 Stack 的子元件相對於 Stack 的位置,可以通過 lefttoprightbottomheight屬性來控制子元件位置。

const Positioned({
  Key? key,
  this.left,
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  required Widget child,
}) : assert(left == null || right == null || width == null),
     assert(top == null || bottom == null || height == null),
     super(key: key, child: child);
複製程式碼

具體規則如下:

  • 如果 top 屬性不為空,就會將元件的頂部定位到 Stack 元件頂部距離 top單位的位置。其他如 leftrightbottom 的機制類似。
  • 如果 topbottom 都不為空,那麼該元件就會按照約束條件限制在 Stack 中佈局的高度(固定距離上下邊界的位置)。leftright 不為空的時候就會限制寬度約束。
  • 如果 topbottom 只有一個不為空,那麼就可以指定高度。如果 leftright 只有一個不為空,那麼就可以指定寬度。
  • leftrightwidth 三個屬性至少有一個不為空;topbottomheight 也一樣。

例如我們要定位頭像元件的位置,我們可以設定左邊距離為20,然後垂直方向距離為 Banner 圖片的高度減去頭像元件自身高度的一半(等於頭像半徑加上邊框尺寸)使得頭像與 Banner 圖片重疊。

Positioned(
    left: 20,
    top: imageHeight - avatarRadius - avatarBorderSize,
    child: _getAvatar(
      personalProfile.avatar,
      avatarRadius * 2,
      avatarBorderSize,
    ),
  ),
複製程式碼

接下來就是其他程式碼實現了,這裡就只貼一下頂層元件的程式碼,具體實現細節可以去這裡下載程式碼(介面檔案為:personal_homepage.dart):狀態管理程式碼

Stack(
  children: [
    CustomScrollView(
      slivers: [
        _getBannerWithAvatar(context, personalProfile),
        _getPersonalProfile(personalProfile),
        _getPersonalStatistic(personalProfile),
      ],
    ),
    Positioned(
      top: 40,
      left: 10,
      child: IconButton(
        onPressed: () {
          Navigator.of(context).pop();
        },
        icon: Icon(
          Icons.arrow_back,
          color: Colors.white,
        ),
      ),
    ),
  ],
);
複製程式碼

介面資料獲取

從瀏覽器開發者工具抓出掘金的個人主頁介面為:https://api.juejin.cn/user_api/v1/user/get?user_id={user_id},介面返回的資料項很多,摘抄我們需要的資料格式如下:

{
    "err_no": 0,
    "err_msg": "success",
    "data": {
        "user_id": "70787819648695",
        "user_name": "島上碼農",
        "company": "島上碼農",
        "job_title": "公眾號",
        "avatar_large": "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/097899bbe5d10bceb750d5c69415518a~300x300.image",
        "level": 3,
        "power": 2193,
        "description": "從南飄到北,從北游到南的業餘碼農",
        "github_verified": 1,
        "followee_count": 148,
        "follower_count": 440,
    }
}
複製程式碼

然後就是基於這個資料構建實體類了,即原始碼裡的personal_entity.dart 檔案。對應的介面請求服務如下:

class JuejinService {
  static Future<PersonalEntity?> getPersonalProfile(String userId) async {
    var response = await HttpUtil.getDioInstance()
        .get('https://api.juejin.cn/user_api/v1/user/get?user_id=$userId');
    if (response.statusCode == 200) {
      if (response.data['err_no'] == 0) {
        return PersonalEntity.fromJson(response.data['data']);
      }
    }

    return null;
  }
}
複製程式碼

Future 狀態管理

Provider 的狀態管理為非同步操作 Future 物件提供了一種更為快捷簡便的方式,那就是 FutureProvider。還記得我們的動態詳情頁面,因為需要先請求資料才能重新整理介面,我們將詳情頁面改成了 StatefulWidget,以便在 initState 中請求資料。

@override
void initState() {
  super.initState();
  context.read<DynamicModel>().getDynamic(widget.id).then((success) {
    if (success) {
      context.read<DynamicModel>().updateViewCount(widget.id);
    }
  });
}
複製程式碼

而使用了 FutureProvider 後,可以將請求放到 FutureProviderFutureProvider 會發起該非同步操作,並且在 Future非同步操作完成後會自動通知下級元件,可以不需要使用 StatefulWidget也能完成網路請求。FutureProvider 的使用方法和 ChangeNotiferProvider 類似,如下所示,其中 initialValue 是初始資料,可以是 null

// create 方式
FutureProvider<T?>(
  initialValue: null,
  create: (context) => Future,
  child: MyApp(),
)

// value 方式
FutureProvider<T?>.value(
  value: Future, 
	initialData: null,
	child: MyApp(),
}
複製程式碼

在這裡我們就可以使用 FutureProvider 來完成個人資訊請求後自動重新整理介面,相關程式碼如下所示:

class PersonalHomePageWrapper extends StatelessWidget {
  const PersonalHomePageWrapper({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureProvider<PersonalEntity?>(
      create: (context) => JuejinService.getPersonalProfile('70787819648695'),
      initialData: null,
      child: _PersonalHomePage(),
    );
  }
}

class _PersonalHomePage extends StatelessWidget {
  const _PersonalHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    PersonalEntity? personalProfile = context.watch<PersonalEntity?>();
    if (personalProfile == null) {
      return Center(
        child: Text('載入中...'),
      );
    }
    return Stack(
  		// ...省略介面程式碼
  	);
	}
	// ...省略介面程式碼
}
複製程式碼

執行結果

執行效果如下圖所示,是不是感覺和掘金的個人主頁很像?

螢幕錄製2021-08-15 下午2.32.12.gif

總結

本篇仿了掘金的個人主頁頂部部分介面,通過介面我們分析了佈局、元件層級,重點介紹了 Stack 元件和 Positioned 元件的使用,以及使用FutureProvider 自動完成非同步操作後通知介面重新整理,從而簡化我們的頁面程式碼,比如無需使用 initState 和編寫狀態管理類。每個人實現介面的方式不同,但思路都是一致的:

  • 分析UI設計稿佈局;
  • 劃分程式碼層級大的佈局塊。
  • 細化佈局塊,構建 UI 元件樹。
  • 抽取介面中可能共用的部分,提高複用性,例如本篇中的統計資料,這塊就具有一定的通用性,是可以單獨抽出來元件的。

我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章