這是我參與8月更文挑戰的第16天,活動詳情檢視:8月更文挑戰
前言
好久沒有講介面的內容了,本篇借仿掘金個人主頁的頭部區域,一方面是講一下 Flutter 的 Stack 層疊元件的用法,另一方面是 Provider 的 FutureProvider 的使用。宣告一下,仿的掘金手機端個人主頁頭部區域如下圖,實際還有底部的滑動後懸停部分沒有完成,後續有時間的時候再完善整個介面,來個“假冒”掘金個人主頁。
介面分析
拿到介面,我們首先分析一下介面的結構,頭部這部分從下到上由如下內容組成:
- 底圖和頭像
- 底部 Banner 圖:在最底部;
- 頭像:包括頭像圖片下的背景圓(或邊框),疊加在底部 Banner 上;
- 使用者名稱稱、級別、工作資訊(頭銜及公司)及個人介紹等個人資訊:在底圖和頭像下方。
- 關注、關注者和掘力值等統計資料:在使用者資訊下方。
- 返回按鈕:返回按鈕在整個頁面的頂層,以便可以隨時點選返回。
得到我們頁面的佈局結構如下圖所示。其中頭像和 Banner
因為重疊了,需要使用一個 Stack
元件包裹,即藍色的區域。
元件結構
確定好了佈局,我們再來確定使用什麼樣的元件,各個佈局區域對應的元件如下:
- 返回按鈕:
IconButton
,使用一個返回圖示按鈕,點選後返回上一頁。 - Banner:使用
CachedImageNetWork
,以便可以載入網路圖片。 - 頭像:使用一個
Stack
,底部是實現邊框的圓形Container
,上層是圓形頭像,使用Container
+CachedImageNetwork
實現。 同時整個頭像區域使用Positioned
絕對位置佈局,保持左側和返回按鈕對齊,然後由有一半區域疊加在Banner
上。 - 個人資訊區域:使用列布局
Column
排布各項資訊,然後暱稱和等級使用行佈局Row
。 - 統計資訊區域:使用行佈局
Row
排布各個統計資料,統計資料本身使用Column
佈局數字和資料項名稱。
整個頁面使用的是 CustomScrollView
,然後再用 Stack
包裹整個頁面和返回按鈕,並將返回按鈕使用 Positioned
絕對定位保持在左上角。最後的元件樹如下(省略了Container
元件)。
重點介紹一下 Stack
和 Postioned
元件。Stack
元件分為Stack
和 IndexedStack
。其中IndexedStack
我們在Flutter入門與實戰(三):構建一個常用的頁面框架有介紹過,其實就是一個有序的 Stack
,可以通過控制當前序號顯示第幾層的介面。而 Stack
的 children 是一組元件,使用的是堆疊排序,次序在後面的層級越高,顯示層級也更靠前(後進先出原則)。通過這種方式可以達到多個介面層疊顯示的目的。
Positioned
元件是專門用於 Stack
的子元件,用於控制 Stack
的子元件相對於 Stack
的位置,可以通過 left
,top
,right
,bottom
和 height
屬性來控制子元件位置。
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
單位的位置。其他如left
,right
和bottom
的機制類似。 - 如果
top
和bottom
都不為空,那麼該元件就會按照約束條件限制在 Stack 中佈局的高度(固定距離上下邊界的位置)。left
和right
不為空的時候就會限制寬度約束。 - 如果
top
和bottom
只有一個不為空,那麼就可以指定高度。如果left
和right
只有一個不為空,那麼就可以指定寬度。 left
、right
和width
三個屬性至少有一個不為空;top
,bottom
和height
也一樣。
例如我們要定位頭像元件的位置,我們可以設定左邊距離為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
後,可以將請求放到 FutureProvider
。FutureProvider
會發起該非同步操作,並且在 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(
// ...省略介面程式碼
);
}
// ...省略介面程式碼
}
複製程式碼
執行結果
執行效果如下圖所示,是不是感覺和掘金的個人主頁很像?
總結
本篇仿了掘金的個人主頁頂部部分介面,通過介面我們分析了佈局、元件層級,重點介紹了 Stack
元件和 Positioned
元件的使用,以及使用FutureProvider
自動完成非同步操作後通知介面重新整理,從而簡化我們的頁面程式碼,比如無需使用 initState
和編寫狀態管理類。每個人實現介面的方式不同,但思路都是一致的:
- 分析UI設計稿佈局;
- 劃分程式碼層級大的佈局塊。
- 細化佈局塊,構建 UI 元件樹。
- 抽取介面中可能共用的部分,提高複用性,例如本篇中的統計資料,這塊就具有一定的通用性,是可以單獨抽出來元件的。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!