在上一篇 【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 官方微信公众号