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/...