本节目标
- 全局数据、响应数据、持久化
- http get 缓存
- http proxy 代理
- fiddle 抓包工具
- iconfont 字体库
- 主界面搭建
- BottomNavigationBar 导航控件
- 编写 api 接口代码
视频
代码
https://github.com/ducafecat/flutter_learn_news/releases/tag/v1.0.5
客户端数据管理
数据类型
存储在内存
用户数据、语言包
存储在内存
用户登录状态、多语言、皮肤样式
Redux、Bloc、provider
APP 保持磁盘上
浏览器 cookie localStorage
编写全局管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class Global { static UserLoginResponseEntity profile = UserLoginResponseEntity( accessToken: null, );
static bool get isRelease => bool.fromEnvironment("dart.vm.product");
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); }
if (Platform.isAndroid) { SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent); SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); } }
static Future<bool> saveProfile(UserLoginResponseEntity userResponse) { profile = userResponse; return StorageUtil() .setJSON(STORAGE_USER_PROFILE_KEY, userResponse.toJson()); } }
|
调用运行
1 2 3 4 5 6 7 8
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| 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 { var cache = LinkedHashMap<String, CacheObject>();
@override onRequest(RequestOptions options) async { if (!CACHE_ENABLE) return options;
bool refresh = options.extra["refresh"] == true;
if (refresh) { if (options.extra["list"] == true) { cache.removeWhere((key, v) => key.contains(options.path)); } else { delete(options.uri.toString()); } return options; }
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;
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| HttpUtil._internal() { ... dio.interceptors.add(NetCache()); ... }
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<String, dynamic> _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/download/fiddler-everywhere
dio 加入 proxy
- lib/common/utils/http.dart
1 2 3 4 5 6 7 8 9 10 11
| 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 字体库
引入流程
https://www.iconfont.cn
assets/fonts/iconfont.ttf
1 2 3 4 5
| fonts: ... - family: Iconfont fonts: - asset: assets/fonts/iconfont.ttf
|
- lib/common/utils/iconfont.dart
1 2 3 4 5 6 7 8 9 10 11 12
| import 'package:flutter/material.dart';
class Iconfont { static const share = IconData( 0xe60d, fontFamily: 'Iconfont', matchTextDirection: true, );
... }
|
自动生成字体库代码
https://github.com/ymzuiku/iconfont_builder
1 2 3 4 5 6 7 8 9 10 11
| > git clone https://github.com/ymzuiku/iconfont_builder
> pub get
> pub global activate iconfont_builder
export PATH=${PATH}:~/.pub-cache/bin
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export PATH=${PATH}:~/Documents/sdk/flutter/bin
export PATH=${PATH}:~/Documents/sdk/flutter/bin/cache/dart-sdk/bin export PATH=${PATH}:~/.pub-cache/bin
export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export ANDROID_HOME=~/Library/Android/sdk export PATH=${PATH}:${ANDROID_HOME}/platform-tools export PATH=${PATH}:${ANDROID_HOME}/tools
|
1 2
| cd 你的项目根目录 iconfont_builder --from ./assets/fonts --to ./lib/common/utils/iconfont.dart
|
编写 api 业务代码
导入 doc/api.json
搭建主界面框架
- 框架页面 lib/pages/application/application.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| ... class _ApplicationPageState extends State<ApplicationPage> with SingleTickerProviderStateMixin { int _page = 0; final List<String> _tabTitles = [ 'Welcome', 'Cagegory', 'Bookmarks', 'Account' ]; PageController _pageController;
final List<BottomNavigationBarItem> _bottomTabs = <BottomNavigationBarItem>[...];
void _handleNavBarTap(int index) { ... }
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| ...
class _MainPageState extends State<MainPage> { NewsPageListResponseEntity _newsPageList; NewsRecommendResponseEntity _newsRecommend; List<CategoryResponseEntity> _categories; List<ChannelResponseEntity> _channels;
String _selCategoryCode;
@override void initState() { super.initState(); _loadAllData(); }
_loadAllData() async { ... }
Widget _buildCategories() { return Container(); }
Widget _buildRecommend() { return Container(); }
Widget _buildChannels() { return Container(); }
Widget _buildNewsList() { return Container(); }
Widget _buildEmailSubscribe() { return Container(); }
@override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: <Widget>[ _buildCategories(), _buildRecommend(), _buildChannels(), _buildNewsList(), _buildEmailSubscribe(), ], ), ); } }
|
- 抽取新闻分类 lib/pages/main/categories_widget.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| Widget newsCategoriesWidget( {List<CategoryResponseEntity> categories, String selCategoryCode, Function(CategoryResponseEntity) onTap}) { return categories == null ? Container() : SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: categories.map<Widget>((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 接口管理
http://yapi.demo.qunar.com/
工具
VSCode 插件
视频
© 猫哥
https://ducafecat.tech
邮箱 ducafecat@gmail.com / 微信 ducafecat / 留言板 disqus