https://www.bilibili.com/vide...android
存储在内存git
用户数据、语言包github
存储在内存json
用户登陆状态、多语言、皮肤样式api
Redux、Bloc、provider浏览器
APP 保持磁盘上缓存
浏览器 cookie localStorage服务器
/// 全局配置 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<bool> saveProfile(UserLoginResponseEntity userResponse) { profile = userResponse; return StorageUtil() .setJSON(STORAGE_USER_PROFILE_KEY, userResponse.toJson()); } }
void main() => Global.init().then((e) => runApp(MyApp())); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Container(); } }
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<String, CacheObject>(); @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); } }
// 加内存缓存 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<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); } }
https://www.telerik.com/downl...微信
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; }; }
https://www.iconfont.cnrestful
assets/fonts/iconfont.ttf
fonts: ... - family: Iconfont fonts: - asset: assets/fonts/iconfont.ttf
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
导入 doc/api.json
... class _ApplicationPageState extends State<ApplicationPage> with SingleTickerProviderStateMixin { // 当前 tab 页码 int _page = 0; // tab 页标题 final List<String> _tabTitles = [ 'Welcome', 'Cagegory', 'Bookmarks', 'Account' ]; // 页控制器 PageController _pageController; // 底部导航项目 final List<BottomNavigationBarItem> _bottomTabs = <BottomNavigationBarItem>[...]; // 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(), ); } }
... class _MainPageState extends State<MainPage> { NewsPageListResponseEntity _newsPageList; // 新闻翻页 NewsRecommendResponseEntity _newsRecommend; // 新闻推荐 List<CategoryResponseEntity> _categories; // 分类 List<ChannelResponseEntity> _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: <Widget>[ _buildCategories(), _buildRecommend(), _buildChannels(), _buildNewsList(), _buildEmailSubscribe(), ], ), ); } }
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
https://github.com/ducafecat/...