在上一篇 【flutter_web 初體驗】中,簡單的介紹了下 flutter_web 建立一個專案和一些踩坑的解決方案,本篇將進一步講解搭建 flutter_web 專案的基本過程。
本文將講解以下要點:
- 專案結構介紹
- 佈局
- 響應式佈局
- 頁面路由
- 網路請求
- markdown 渲染
專案目錄結構
/
├── README.md
├── analysis_options.yaml
├── build # webdev build 編譯生成的目錄,用於部署
├── lib # 工作區
│ ├── components # 元件(Widgets)
│ ├── kit # 工具、父類
│ ├── main.dart # 入口
│ ├── models # 資料模型
│ ├── network # 網路
│ ├── pages # 頁面
│ │ ├── detail # 詳情頁
│ │ │ ├── bloc
│ │ │ ├── detail.dart
│ │ │ ├── model
│ │ │ └── page
│ │ ├── index # 首頁
│ │ ├── pages.dart
│ │ └── user # 使用者
│ └── router # 頁面路由
├── pubspec.lock
├── pubspec.yaml # 依賴
└── web
├── assets # 資源區
│ ├── FontManifest.json # 字型
│ └── images # 圖片
│ └── swift_logo.png
├── index.html
└── main.dart
複製程式碼
主要劃分了 6 大部分:
- network: 網路
- models: 模型
- router: 路由
- pages: 頁面
- components:元件
- kit: 工具、常量、基類等
佈局
接下來,將要實現 2 個頁面, 效果分別如下:
列表和詳情,頁面還是比較簡單的。
整體佈局就是頭部、內容、尾部。比如尾部在首頁和詳情頁的底部都是一樣的,把它領出來作為一個公共元件。個人的開發習慣就是,相同的東西往上冒泡,讓檔案目錄層次上浮。
首頁佈局
因為頁面不存在懸浮情況,所以首頁佈局還是比較簡單的:
// pages/index/index_pages.dart
// 新增頭部
List<Widget> lists = [_buildHeader(context)];
// 新增列表內容
lists.addAll(rows.map((item) {
return _buildCell(item);
}).toList());
// 新增尾部
lists.add(Container(
margin: EdgeInsets.only(top: 100),
child: FooterView(),
));
/// 整體內容用 SingleChildScrollView 進行包裝
SingleChildScrollView(
child: Container(
color: Colors.white,
child: Column(
children: lists,
),
))
複製程式碼
詳情頁佈局
詳情頁頭部是懸浮的,且文章採用 markdown,也是一個 ListView。 然後底部是通用的底部欄。
那麼懸浮的話就採用了 AppBar :
Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
child: HeaderView(), preferredSize: Size.fromHeight(50)),
body: body)
複製程式碼
上面的 body
是通過 SingleChildScrollView 包裝:
return SingleChildScrollView(
child: Column(
children: <Widget>[mdView, FooterView()],
),
);
複製程式碼
響應式佈局
在有使用過站點的初版的時候,當你改變瀏覽器的大小的時候,佈局會比較醜陋,且會傳送一些佈局警告。造成的原因是沒有適配各種螢幕的大小。如果做前端的,會有一些比較通用的解決辦法:
- 媒體查詢
- 百分比
- rem
- vw/vh
如果對此感興趣可深入閱讀 《響應式佈局的常用解決方案對比(媒體查詢、百分比、rem和vw/vh)》
那麼如果 flutter_web 要做響應是佈局,該怎麼辦?
- 尺寸大小和位置不使用硬編碼
- 使用
MediaQuery
獲取當前的視窗的大小 - 使用
Flexible
和Expanded
去佈局介面,使用百分比而不是硬編碼。 - 使用
LayoutBuilder
獲取父 widget 的ConstraintBox
- 使用
MediaQuery
或OrientationBuilder
獲取裝置的方向。 AspectRatio
和FractionallySizedBox
是常用的百分比相關的 Widget。
響應式佈局有兩篇文章推薦閱讀:
頁面路由
// main.dart
main() {
Static.storage = Storage();
runApp(SwiftClub());
}
class SwiftClub extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'swiftclub',
theme: ThemeData(fontFamily: "Montserrat"),
onGenerateRoute: (setting) => buildRouters(setting),
initialRoute: "/",
);
}
}
複製程式碼
// router/router.dart
Route<dynamic> buildRouters(RouteSettings settings) {
dynamic args = settings.arguments;
switch (settings.name) {
case "/login":
return SimpleRoute(
name: "/login", title: "login", builder: (context) => LoginPage());
case "/detail":
if (args == null) {
// 直接重新整理當前,引數可能不存在,返回首頁
return defaultRoute();
}
// 解析頁面傳遞的引數
final topicId = SafeValue.toInt(args['topicId']);
return SimpleRoute(
name: "detail",
title: "detaila",
builder: (context) => DetailPage(
topicId: topicId,
));
case "/":
return defaultRoute();
default:
return defaultRoute();
}
}
SimpleRoute defaultRoute() {
return SimpleRoute(
name: '/', title: 'swiftclub', builder: (context) => IndexPage());
}
複製程式碼
flutter_web 的路由,跟 flutter 的路由管理是一樣的,主要是注意兩點:
-
如果重新整理當前頁面,之前其他頁面傳遞過來的引數就沒有了,重新整理後,頁面獲取不到傳遞過來的引數,進行網路請求,報錯。所以這裡做了判斷,如果引數不存在了,則返回到預設的首頁。
-
路由引數取值,嘗試多次,發現
ModalRoute.of(context).settings.arguments; 複製程式碼
獲取不到 arguments
,但在判斷路由的時候可以獲取。
網路請求
由於 dart:io
在 flutter_web 中還不支援,所以 dio
是不能使用的,官方建議使用 package:http
為此做了個 http 請求的封裝,可參考使用:
import 'dart:convert';
import 'package:flutter_web/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:swiftclub/kit/macro/macro.dart';
class Network {
static getReq(String url, {Map params, Map headers}) async {
var fullUrl = Macro.URL_base + url;
return await _getReq(fullUrl, params: params, headers: headers);
}
static _getReq(String url, {Map params, Map headers}) async {
var reqUri = _uriWith(url, queryParameters: params);
http.Response response = await http.get(reqUri, headers: headers);
var responseBody = json.decode(response.body);
return responseBody;
}
static _postReq(String url, {Map headers, Map params}) async {
var fullUrl = Macro.URL_base + url;
if (headers != null && headers.isNotEmpty) {
http.Response response =
await http.post(Uri.parse(fullUrl), headers: headers, body: params);
var responseBody = json.decode(response.body);
return responseBody;
} else {
http.Response response =
await http.post(Uri.parse(fullUrl), body: params);
var responseBody = json.decode(response.body);
return responseBody;
}
}
static Uri _uriWith(String url, {Map queryParameters}) {
String _url = url;
String query = _urlEncodeMap(queryParameters);
if (query.isNotEmpty) {
_url += (_url.contains("?") ? "&" : "?") + query;
}
// Normalize the url.
return Uri.parse(_url).normalizePath();
}
static String _urlEncodeMap(data) {
StringBuffer urlData = StringBuffer("");
bool first = true;
void urlEncode(dynamic sub, String path) {
if (sub is List) {
for (int i = 0; i < sub.length; i++) {
urlEncode(sub[i],
"$path%5B${(sub[i] is Map || sub[i] is List) ? i : ''}%5D");
}
} else if (sub is Map) {
sub.forEach((k, v) {
if (path == "") {
urlEncode(v, "${Uri.encodeQueryComponent(k)}");
} else {
urlEncode(v, "$path%5B${Uri.encodeQueryComponent(k)}%5D");
}
});
} else {
if (!first) {
urlData.write("&");
}
first = false;
urlData.write("$path=${Uri.encodeQueryComponent(sub.toString())}");
}
}
urlEncode(data, "");
return urlData.toString();
}
}
複製程式碼
markdown 渲染
在 github 上巡遊一番,應該找不到針對 flutter_web 的 markdown 的支援庫。筆者在參考
封裝了在 flutter_web 上可用的 markdown 元件,具體實現可參考 swiftclub/site
語法高亮現在只支援 dart 語言的。
效果
更多閱讀,請關注 SwiftOldBird 官方微信公眾號