B站视频
https://www.bilibili.com/vide...
本节目标
- 全局数据、响应数据、持久化
- http get 缓存
- http proxy 代理
- fiddle 抓包工具
- iconfont 字体库
- 主界面搭建
- BottomNavigationBar 导航控件
- 编写 api 接口代码
客户端数据管理
数据类型
- 全局数据
存储在内存
用户数据、语言包
- 响应数据
存储在内存
用户登录状态、多语言、皮肤样式
Redux、Bloc、provider
- 持久化
APP 保持磁盘上
浏览器 cookie localStorage
编写全局管理
- lib/global.dart
/// 全局配置
class Global {
/// 用户配置
static UserLoginResponseEntity profile = UserLoginResponseEntity(
accessToken: null,
);
/// 是否 release
static bool get isRelease => bool.fromEnvironment("dart.vm.product");
/// init
static Future init() async {
// 运行初始
WidgetsFlutterBinding.ensureInitialized();
// 工具初始
await StorageUtil.init();
HttpUtil();
// 读取离线用户信息
var _profileJSON = StorageUtil().getJSON(STORAGE_USER_PROFILE_KEY);
if (_profileJSON != null) {
profile = UserLoginResponseEntity.fromJson(_profileJSON);
}
// http 缓存
// android 状态栏为透明的沉浸
if (Platform.isAndroid) {
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
// 持久化 用户信息
static Future saveProfile(UserLoginResponseEntity userResponse) {
profile = userResponse;
return StorageUtil()
.setJSON(STORAGE_USER_PROFILE_KEY, userResponse.toJson());
}
}
调用运行
- lib/main.dart
void main() => Global.init().then((e) => runApp(MyApp()));
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
Http 内存缓存
缓存策略
代码
- 缓存工具类 lib/common/utils/net_cache.dart
import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:flutter_ducafecat_news/common/values/values.dart';
class CacheObject {
CacheObject(this.response)
: timeStamp = DateTime.now().millisecondsSinceEpoch;
Response response;
int timeStamp;
@override
bool operator ==(other) {
return response.hashCode == other.hashCode;
}
@override
int get hashCode => response.realUri.hashCode;
}
class NetCache extends Interceptor {
// 为确保迭代器顺序和对象插入时间一致顺序一致,我们使用LinkedHashMap
var cache = LinkedHashMap();
@override
onRequest(RequestOptions options) async {
if (!CACHE_ENABLE) return options;
// refresh标记是否是"下拉刷新"
bool refresh = options.extra["refresh"] == true;
// 如果是下拉刷新,先删除相关缓存
if (refresh) {
if (options.extra["list"] == true) {
//若是列表,则只要url中包含当前path的缓存全部删除(简单实现,并不精准)
cache.removeWhere((key, v) => key.contains(options.path));
} else {
// 如果不是列表,则只删除uri相同的缓存
delete(options.uri.toString());
}
return options;
}
// get 请求,开启缓存
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == 'get') {
String key = options.extra["cacheKey"] ?? options.uri.toString();
var ob = cache[key];
if (ob != null) {
//若缓存未过期,则返回缓存内容
if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
CACHE_MAXAGE) {
return cache[key].response;
} else {
//若已过期则删除缓存,继续向服务器请求
cache.remove(key);
}
}
}
}
@override
onError(DioError err) async {
// 错误状态不缓存
}
@override
onResponse(Response response) async {
// 如果启用缓存,将返回结果保存到缓存
if (CACHE_ENABLE) {
_saveCache(response);
}
}
_saveCache(Response object) {
RequestOptions options = object.request;
// 只缓存 get 的请求
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == "get") {
// 如果缓存数量超过最大数量限制,则先移除最早的一条记录
if (cache.length == CACHE_MAXCOUNT) {
cache.remove(cache[cache.keys.first]);
}
String key = options.extra["cacheKey"] ?? options.uri.toString();
cache[key] = CacheObject(object);
}
}
void delete(String key) {
cache.remove(key);
}
}
- dio 封装 lib/common/utils/http.dart
// 加内存缓存
HttpUtil._internal() {
...
dio.interceptors.add(NetCache());
...
}
// 修改 get 请求
/// restful get 操作
/// refresh 是否下拉刷新 默认 false
/// noCache 是否不缓存 默认 true
/// list 是否列表 默认 false
/// cacheKey 缓存key
Future get(
String path, {
dynamic params,
Options options,
bool refresh = false,
bool noCache = !CACHE_ENABLE,
bool list = false,
String cacheKey,
}) async {
try {
Options requestOptions = options ?? Options();
requestOptions = requestOptions.merge(extra: {
"refresh": refresh,
"noCache": noCache,
"list": list,
"cacheKey": cacheKey,
});
Map _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.merge(headers: _authorization);
}
var response = await dio.get(path,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken);
return response.data;
} on DioError catch (e) {
throw createErrorEntity(e);
}
}
Http Proxy 代理 + Fiddle 抓包
安装 Fiddle
https://www.telerik.com/downl...
dio 加入 proxy
- lib/common/utils/http.dart
if (!Global.isRelease && PROXY_ENABLE) {
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = (uri) {
return "PROXY $PROXY_IP:$PROXY_PORT";
};
//代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
};
}
Iconfont 字体库
引入流程
- 登录
- 创建字体项目
- 字体文件放入
assets/fonts/iconfont.ttf
- pubspec.yaml
fonts:
...
- family: Iconfont
fonts:
- asset: assets/fonts/iconfont.ttf
- lib/common/utils/iconfont.dart
import 'package:flutter/material.dart';
class Iconfont {
// iconName: share
static const share = IconData(
0xe60d,
fontFamily: 'Iconfont',
matchTextDirection: true,
);
...
}
自动生成字体库代码
https://github.com/ymzuiku/ic...
- 拉取项目、编译
# 拉取项目
> git clone https://github.com/ymzuiku/iconfont_builder
# 更新包
> pub get
# 安装工具
> pub global activate iconfont_builder
# 检查环境配置
export PATH=${PATH}:~/.pub-cache/bin
- 参考我的配置
# flutter sdk
export PATH=${PATH}:~/Documents/sdk/flutter/bin
# dart sdk
export PATH=${PATH}:~/Documents/sdk/flutter/bin/cache/dart-sdk/bin
export PATH=${PATH}:~/.pub-cache/bin
# flutter-io 国内镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# android
export ANDROID_HOME=~/Library/Android/sdk
export PATH=${PATH}:${ANDROID_HOME}/platform-tools
export PATH=${PATH}:${ANDROID_HOME}/tools
- 生成字体类
cd 你的项目根目录
iconfont_builder --from ./assets/fonts --to ./lib/common/utils/iconfont.dart
编写 api 业务代码
- yapi 配置
导入 doc/api.json
- 代码
搭建主界面框架
- 框架页面 lib/pages/application/application.dart
...
class _ApplicationPageState extends State
with SingleTickerProviderStateMixin {
// 当前 tab 页码
int _page = 0;
// tab 页标题
final List _tabTitles = [
'Welcome',
'Cagegory',
'Bookmarks',
'Account'
];
// 页控制器
PageController _pageController;
// 底部导航项目
final List _bottomTabs = [...];
// tab栏动画
void _handleNavBarTap(int index) {
...
}
// tab栏页码切换
void _handlePageChanged(int page) {
...
}
// 顶部导航
Widget _buildAppBar() {
return Container();
}
// 内容页
Widget _buildPageView() {
return Container();
}
// 底部导航
Widget _buildBottomNavigationBar() {
return Container();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: _buildPageView(),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
}
编写首页代码
- 首页代码 lib/pages/main/main.dart
...
class _MainPageState extends State {
NewsPageListResponseEntity _newsPageList; // 新闻翻页
NewsRecommendResponseEntity _newsRecommend; // 新闻推荐
List _categories; // 分类
List _channels; // 频道
String _selCategoryCode; // 选中的分类Code
@override
void initState() {
super.initState();
_loadAllData();
}
// 读取所有数据
_loadAllData() async {
...
}
// 分类菜单
Widget _buildCategories() {
return Container();
}
// 抽取前先实现业务
// 推荐阅读
Widget _buildRecommend() {
return Container();
}
// 频道
Widget _buildChannels() {
return Container();
}
// 新闻列表
Widget _buildNewsList() {
return Container();
}
// ad 广告条
// 邮件订阅
Widget _buildEmailSubscribe() {
return Container();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
_buildCategories(),
_buildRecommend(),
_buildChannels(),
_buildNewsList(),
_buildEmailSubscribe(),
],
),
);
}
}
- 抽取新闻分类 lib/pages/main/categories_widget.dart
Widget newsCategoriesWidget(
{List categories,
String selCategoryCode,
Function(CategoryResponseEntity) onTap}) {
return categories == null
? Container()
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: categories.map((item) {
return Container(
alignment: Alignment.center,
height: duSetHeight(52),
padding: EdgeInsets.symmetric(horizontal: 8),
child: GestureDetector(
child: Text(
item.title,
style: TextStyle(
color: selCategoryCode == item.code
? AppColors.secondaryElementText
: AppColors.primaryText,
fontSize: duSetFontSize(18),
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
),
),
onTap: () => onTap(item),
),
);
}).toList(),
),
);
}
蓝湖设计稿
https://lanhuapp.com/url/lYuz1
密码: gSKl
蓝湖现在收费了,所以查看标记还请自己上传 xd 设计稿
商业设计稿文件不好直接分享, 可以加微信联系 ducafecat
YAPI 接口管理
git 代码
https://github.com/ducafecat/...