flutter开发小结
Provider 官方库,基于InheritedWidget的轻量级状态管理,通过数据共享和依赖注入实现状态管理,单项数据流:数据从父widget流向子widget,核心组件ChangeNotifier:通过notifyListeners()通知ui更新
ChangeNotifilerProvider:将ChangeNotifiler绑定到widget树,Consumer:选择性监听数据的变化,减少不必要的创建
适合小型项目,官方支持库,缺点依赖BuildContext,需要传递context,缺乏分层,复杂业务逻辑容易堆积在ChangeNotifiler中
Bloc的核心思想事件驱动,通过Events触发States变化,严格分离UI与业务逻辑,单项数据流:Event-bloc-state-ui
Bloc:处理事件并生成新状态
BlocProvider:将Bloc注入widget树
BlocBuilder/BlocListener:监听状态变化
优点:逻辑分层清晰,UI,事件,状态完全解耦
缺点:需要定义大量事件和状态类
适合中大型项目
RiverPod :provider的增强版,解决Provider的痛点(如依赖BuildContext、全局状态管理)
provider基础数据提供者。Stateprovider管理可变状态 stateNotifierProvider结合StateNotifier实现复杂逻辑
FutureProvider/StreamProvider处理异步数据
无BuildContext,通过ref对象直接访问provider
Const widgets在编译时创建并缓存,避免了在每次构建时重复创建相同的widget,由于不需要在每次更新时重新构造,可以减少内存的使用和cpu负担
使用setstate最小化重建
将状态管理限制在需要更新的子组件中,避免整个父组件重建
将大组件拆分为多个小组件,使得只重建需要的部分
对不需要更新的组件使用const,避免不必要的重建
将频繁变化的区域使用RepaintBoundary中,限制重绘,通过devtools的flutter inspector开启highlight repaints 模式,红色闪烁区域表示频繁重绘的组件
避免savelayer 使用withOpacity代替Opacity
状态管理精细化,选择性监听数据的变化,减少不必要的创建
启动优化:着色器预热,在运行时会动态编译着色器,首次编译耗时,未预热的着色器每次启动都要重新编译
铺获着色器 flutter run --profile --cache-sksl
打包时预加载缓存
将计算密集型任务移至isolate线程,避免ui线程阻塞
通过Flutter devtools的memory profiler 生成泄露热力图
使用obfuscate开启代码混淆
flutter analyze命令可以根据analysis_options的定义的规则进行代码检查
dart在单线程中是以消息循环机制来运行的,一个是微任务队列,一个是事件队列,首先会按照先进先出的顺序逐个执行微任务队列中的任务,当微任务队列的任务都执行完毕便开始执行事件队列的任务,事件队列的执行完毕,再去执行微任务,循环反复
Flutter 的热重载功能可帮助您在无需重新启动应用程序的情况下快速添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便您可以快速查看更改的效果
widget:控件的配置信息,不涉及渲染,更新代价低,element:widget和renderobject之间的粘合剂,负责将widget树的变化以更低的代价映射到redenrobject,renderonject真正的渲染树,负责最终的测量,布局和绘制
JIT:Just In Time . 动态解释,一边翻译一边执行,也称为即时编译,Flutter中的热重载正是基于此特性
动态编译,代码在运行时逐行编译为机器码
AOT:
在构建阶段将dart代码全部编译为原生机器码,用户安装后直接运行机器码,无需运行时编译,
优点:执行速度快,启动时间短,体积小,移除调试信息
缺点:失去热重载,构建耗时
使用provider包括widget后长按不回调onLongPressEnd
当 notifyListeners() 触发组件重建时,可能导致手势识别器重置,将GestureDetector放到Consumer外面
此时widget是包括在Consumer里面
借助 WidgetsBinding.instance.addPostFrameCallback 确保 _animationController
.reset() 在当前帧构建完成之
.. 是 级联操作符(Cascade Notation),它允许你在同一个对象上连续调用多个方法或修改多个属性,而无需重复引用该对象。
_dio.interceptors.add(RetryInterceptor(dio: _dio, maxRetries: 2));
class RetryInterceptor extends Interceptor {
final Dio dio;
final int maxRetries;
RetryInterceptor({required this.dio, required this.maxRetries});
@override
Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err)) {
final options = err.requestOptions;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
// 重试请求
final response = await dio.request(
options.path,
data: options.data,
queryParameters: options.queryParameters,
options: Options(
method: options.method,
headers: options.headers,
),
);
// 请求成功,将响应传递给下一个拦截器或返回给调用者
handler.resolve(response);
return;
} catch (e) {
retryCount++;
debugPrint('Retry attempt $retryCount failed: $e');
}
}
}
// 所有重试都失败,将错误传递给下一个拦截器或返回给调用者
handler.next(err);
}
bool _shouldRetry(DioError err) {
return err.type == DioErrorType.connectTimeout ||
err.type == DioErrorType.receiveTimeout || err.type == DioErrorType.sendTimeout;
}
}
intl: 0.18.1
基于Flutter开发的应用,发布于iOS和Android两端。
实现国际化:在lib/l10n/*.arb文件里加入每个字段的英汉内容,保存一下,会在lib/generated/l10n.dart自动生成get方法,如:helloWorld
使用时直接调用S.current.helloWorld即可,
final localeProvider = Provider.of<LocaleProvider>(context);设置完语言后,全局更新国际化
final MyRouterObserver myObserver = MyRouterObserver<PageRoute>();
class LocaleProvider extends ChangeNotifier {
late Locale _locale;
Locale get locale => _locale;
//const Locale('en', 'US'),
//const Locale('zh', 'CN'),
LocaleProvider() {
// 获取系统语言环境
_locale = WidgetsBinding.instance.platformDispatcher.locale;
// 检查支持的语言,如果不支持则设置为默认语言
if (!['en', 'zh'].contains(_locale.languageCode)) {
_locale = const Locale('zh', 'CN');
}
}
void setLocale(Locale locale) {
if (!['en', 'zh'].contains(locale.languageCode)) return;
_locale = locale;
notifyListeners();
}
void resetLocale() {
_locale = WidgetsBinding.instance.platformDispatcher.locale;
// 检查支持的语言,如果不支持则设置为默认语言
if (!['en', 'zh'].contains(_locale.languageCode)) {
_locale = const Locale('zh', 'CN');
}
notifyListeners();
}
}
runApp(MultiProvider(
providers: [
Provider<AppThemeConfig>(create: (_) => AppThemeConfig()),
ChangeNotifierProvider<SoundState>(create: (_) => SoundState()),
ChangeNotifierProvider<Chat>(create: (_) => Chat()),
ChangeNotifierProvider<RTCStateNotifier>(create: (_) => RTCStateNotifier()),
ChangeNotifierProvider<UserState>(create: (_) => UserState()..setAvatarUrl(SpUtil.getString(AppConstant.avatarKey))),
ChangeNotifierProvider<RecordingProvider>(create: (_) => RecordingProvider()),
ChangeNotifierProvider<SectionProvider>(create: (_) => SectionProvider()),
ChangeNotifierProvider<LocaleProvider>(create: (_) => LocaleProvider()),
ChangeNotifierProvider<Person>(create: (_) => Person()),
],
child: MyApp(
savedThemeMode: savedThemeMode,
)));
class RouterManage {
static String? currentRouterName;
/// 路由声明 - CupertinoPageRoute
static final Map<String, WidgetBuilder> routes = {
RouteConstant.LAUNCH: (ctx) => const LaunchPage(),
RouteConstant.HOME: (ctx) => HomePage(
params: _buildRouteParams(ctx),
),
RouteConstant.BROWSER_WEB: (ctx) => BrowserPage(
params: _buildRouteParams(ctx),
),
RouteConstant.LOGIN_PAGE: (ctx) => const LoginPage(),
RouteConstant.LOGIN_CODE_PAGE: (ctx) => LoginCodePage(
params: _buildRouteParams(ctx),
),
RouteConstant.LOGIN_ONE_PAGE: (ctx) => const LoginOnePage(),
RouteConstant.UPDATE_HEADER_PAGE: (ctx) => const UpdateHeaderPage(),
RouteConstant.UPDATE_AI_HEADER_PAGE: (ctx) => const UpdateAiHeaderPage(),
RouteConstant.CHAT_PAGE: (ctx) => ChatPage(params: _buildRouteParams(ctx),),
RouteConstant.CHAT_LIST_PAGE: (ctx) => const ChatListPage(),
RouteConstant.MAP_PAGE: (ctx) => const ShowMapPage(),
RouteConstant.GUIDE_MAP_PAGE: (ctx) => GuideMapPage(
params: _buildRouteParams(ctx),
),
RouteConstant.FRIEND_PAGE: (ctx) => const FriendPage(),
RouteConstant.TICKET_PAGE: (ctx) => const TicketPage(),
RouteConstant.SET_PAGE: (ctx) => const SetPage(),
RouteConstant.SET_THEME_PAGE: (ctx) => const SetThemePage(),
RouteConstant.PERSON_PAGE: (ctx) => const PersonPage(),
RouteConstant.ROUTE_PAGE: (ctx) => const RouteMapPage(),
RouteConstant.ABOUT_PAGE: (ctx) => const AboutPage(),
RouteConstant.COUPON_PAGE: (ctx) => const CouponPage(),
RouteConstant.MY_COLLECT_PAGE: (ctx) => const MyCollectPage(),
};
/// 根路由
static const String initialRoute = RouteConstant.LAUNCH;
/// 路由钩子
static final RouteFactory generateRoute = (settings) {
// if (settings.name == HMRouteConstant.HOME) {
// return MaterialPageRoute(builder: (ctx) {
// return const HomePage();
// });
// }
debugPrint('============${settings.name}');
return null;
};
/// 未知路由页面
static final RouteFactory unknownRoute = (settings) {
return MaterialPageRoute(builder: (ctx) {
return const UnknownPage();
});
};
static Map<String, dynamic> _buildRouteParams(BuildContext ctx) {
Map<String, dynamic> map = {};
try {
var arguments = ModalRoute.of(ctx)?.settings.arguments;
if (arguments != null) {
map = arguments as Map<String, dynamic>;
}
} catch (e) {
debugPrint('_buildRouteParams-error:$e');
}
return map;
}
static Future<T?> pushPage<T extends Object?>(
BuildContext context, String router,
{Map<String, dynamic>? arguments}) {
return Navigator.of(context).pushNamed(router, arguments: arguments);
}
///返回到目标路由页面,打开新页面,targetRoute为空时会关闭所有页面,打开新页面
static Future<T?> pushNamedAndRemoveUntil<T extends Object?>(
BuildContext context, String router,
{String? targetRoute, Map<String, dynamic>? arguments}) {
return Navigator.of(context).pushNamedAndRemoveUntil<T>(router, (route) {
if (ObjUtil.isEmpty(targetRoute)) {
return false;
} else {
return route.settings.name == targetRoute;
}
}, arguments: arguments);
}
static pop(BuildContext context, {Map<String, dynamic>? result}) {
Map<String, dynamic> popResult = result ?? {};
Navigator.of(context).pop(popResult);
}
static maybePop(BuildContext context, {Map<String, dynamic>? result}) {
Map<String, dynamic> popResult = result ?? {};
Navigator.of(context).maybePop(popResult);
}
static bool canPop(BuildContext context) {
return Navigator.of(context).canPop();
}
static Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
BuildContext context,
String routeName, {
TO? result,
Map<String, dynamic>? arguments,
}) {
return Navigator.of(context).pushReplacementNamed<T, TO>(routeName,
arguments: arguments, result: result);
}
static Future<T?> pushAndRemoveUntil<T extends Object?>(
BuildContext context, Route<T> newRoute,
{String? targetRoute}) {
return Navigator.of(context).pushAndRemoveUntil(
newRoute, (route) => route.settings.name == targetRoute);
}
/// pop到目标路由,targetName为空时 返回到roottabbar
static popTargetRoute(BuildContext context, {String? targetName}) {
Navigator.of(context).popUntil((route) {
if (route.settings.name == null ||
route.settings.name == (targetName ??= RouteConstant.HOME)) {
return true;
}
return false;
});
}
abstract class BaseState<T extends StatefulWidget> extends State
with RouteAware, WidgetsBindingObserver {
bool _pageShow = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
T get widget => super.widget as T;
@override
void didChangeDependencies() {
try {
myObserver.subscribe(this, ModalRoute.of(context)!);
} catch (e) {
}
// print("=================${ModalRoute.of(context)?.settings.name}");
/// 执行createElement后触发
super.didChangeDependencies();
}
@override
void dispose() {
// print('BaseState-dispose:${widget.toString()}');
myObserver.unsubscribe(this);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didUpdateWidget(covariant StatefulWidget oldWidget) {
// print('BaseState-didUpdateWidget:${widget.toString()}');
super.didUpdateWidget(oldWidget);
}
@override
void didPop() {
// print('BaseState-didPop:${widget.toString()}');
onPageDismiss();
super.didPop();
}
@override
Widget build(BuildContext context) {
AppThemeConfig themeConfig = Provider.of<AppThemeConfig>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
// android 更改状态栏颜色
value: themeConfig.getSystemUiOverlayStyle(context),
child: buildBody(context,themeConfig));
}
@override
void didPush() {
//页面首次创建
onPageShow();
super.didPush();
}
@override
void didPopNext() {
//页面可见时
onPageShow();
super.didPopNext();
}
@override
void didPushNext() {
onPageDismiss();
super.didPushNext();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// print(
// 'BaseState-didChangeAppLifecycleState:${HMRouter.currentRouterName}$state$pageRouterName');
if (state == AppLifecycleState.resumed) {
if (RouterManage.currentRouterName == pageRouterName) {
onPageShow();
}
} else if (state == AppLifecycleState.paused) {
if (RouterManage.currentRouterName == pageRouterName) {
onPageDismiss();
}
}
super.didChangeAppLifecycleState(state);
}
/// 需要走app级的onPageShow 重写这个方法返回当前页面的路由
String get pageRouterName => '';
/// 视图已显示
void onPageShow() {
_pageShow = true;
}
/// 视图已消失
void onPageDismiss() {
_pageShow = false;
}
bool get pageShow => _pageShow;
Widget buildBody(BuildContext context,AppThemeConfig themeConfig);
dio
Networking._init() {
_dio = Dio();
_dio.options.baseUrl = ApiHost.host;
_dio.options.connectTimeout = 20000; // 10s
_dio.options.receiveTimeout = 20000;
_dio.options.sendTimeout = 20000;
_dio.interceptors.add(RetryInterceptor(dio: _dio, maxRetries: 2));
configProxy();
}
void configProxy() {
/// 用1个开关设置是否开启代理
if (ObjUtil.isNotEmpty(ApiHost.proxy)) {
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = (uri) {
return 'PROXY ${ApiHost.proxy!}';
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return null;
};
}
}
static Future<T> httpJson<T>(
BuildContext? context,
String method,
String uri, {
dynamic header,
dynamic data,
bool loading = true,
String? contentType,
CancelToken? cancelToken,
bool isShowToast = true,
bool isShowAlert = false,
bool isUploadFile = false,
bool isShowSuccessToast = false,
int? timeout,
}) async {
if (_instance == null) Networking._getInstance();
data ??= <String, dynamic>{};
if (method == 'get') {
contentType = CONTENT_TYPE_FORM;
} else {
contentType = contentType ?? CONTENT_TYPE_JSON;
}
if (uri.contains('http')) {
_dio.options.baseUrl = '';
} else {
_dio.options.baseUrl = ApiHost.host;
}
if (timeout != null) {
_dio.options.connectTimeout = timeout;
_dio.options.receiveTimeout = timeout;
_dio.options.sendTimeout = timeout;
}
Options op = Options();
op.contentType = contentType;
op.method = method;
if(ObjUtil.isEmpty(op.headers)){
op.headers = header ?? <String, dynamic>{};
}else {
op.headers!.addAll(header);
}
op.headers!['version'] = AppInfo.appVersionCode ?? '';
op.headers!['versionName'] = AppInfo.appVersionName ?? '';
if (ObjUtil.isEmpty(GObj.token)) {
GObj.synchronizeToken();
}
if(!uri.contains(ApiHost.cozeHost)){
if (ObjUtil.isNotEmpty(GObj.token)) {
op.headers!['authorization'] = GObj.token;
}
}
String params = {'body': data.toString(), 'header': op.headers.toString()}.toString();
Logger.enableLog = !kReleaseMode;
try {
if (loading == true) EasyLoading.show();
Response response;
if (isUploadFile) {
FormData formData = FormData.fromMap(
{'file': await MultipartFile.fromFile(data!.path)});
response = await _dio.post(uri, data: formData, options: op);
} else {
response = await _dio.request(uri,
data: data,
queryParameters: method == 'get' ? data : null,
options: op,
cancelToken: cancelToken);
}
if (loading == true) EasyLoading.dismiss();
if (uri.contains('http')) {
Logger.d('请求地址:$uri', tag: 'Response');
} else {
Logger.d('请求地址:${ApiHost.host}$uri', tag: 'Response');
}
Logger.d('请求方式:$method==$contentType', tag: 'Response');
Logger.d('请求参数:$params', tag: 'Response');
Logger.d(response, tag: 'Response', isJson: true);
return logicalTransform(
context, response, isShowToast, isShowSuccessToast);
} catch (error) {
if (uri.contains('http')) {
Logger.d('请求地址:$uri', tag: 'Response');
} else {
Logger.d('请求地址:${ApiHost.host}$uri', tag: 'Response');
}
Logger.d('请求方式:$method==$contentType', tag: 'Response');
Logger.d('请求参数:$params', tag: 'Response');
Logger.d(error, tag: 'Response');
if (loading == true) EasyLoading.dismiss();
return logicalErrorTransform(
context, error, isShowToast,isShowAlert : isShowAlert);
}
}
fit:用于在图片的显示空间和图片本身大小不同时指定图片的适应模式,在BoxFit中定义
fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
SSE 全称是 Server-Sent Event,网页自动获取来自服务器的更新,SSE 是单向消息传递。
赋值时机
final在运行时赋值,可以延迟到对象构造时,如 final String name;
const在编译时就确定值,如const int maxCount = 100;