Flutter日志记录插件cr_logger的使用
Flutter日志记录插件cr_logger的使用
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/plugin_banner.png)
Flutter插件用于日志记录
- 简单的日志记录到logcat。
- 网络请求拦截。
- 日志导出(JSON格式)。
- 为“Charles”设置代理。
- 按级别记录日志。
支持的Dart http客户端插件:
- ✔️ Dio
- ✔️ Chopper
- ✔️ Http from http/http package
- ✔️ HttpClient from dart:io package
目录
截图
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_log_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/debug_log_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_db_log_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/quick_action_menu_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_request_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_response_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_error_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http_search_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/logs_search_screenshot.png)
![](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/screenshot-web.png)
开始使用
- 在项目中添加插件:
dependencies:
cr_logger: ^2.2.0
- 初始化日志记录器。在
main.dart
文件中:
void main() {
...
CRLoggerInitializer.instance.init(
theme: ThemeData.light(),
levelColors: {
Level.debug: Colors.grey.shade300,
Level.warning: Colors.orange,
},
hiddenFields: [
'token',
],
logFileName: 'my_logs',
printLogs: true,
useCrLoggerInReleaseBuild: false,
useDatabase: false,
);
}
printLogs
- 当printLogs
为true
时打印所有日志。useCrLoggerInReleaseBuild
- 当kReleaseMode
为true
时,所有日志将被打印并使用数据库。useDatabase
- 使用数据库保存日志历史记录。它只会在useCrLoggerInReleaseBuild
设置为true
时工作。theme
- 自定义日志记录器主题。levelColors
- 消息类型的颜色(debug, verbose, info, warning, error, wtf)。hiddenFields
- 需要替换为字符串’Hidden’的键列表。logFileName
- 导出日志时的文件名。maxLogsCount
- 每种类型的日志(http, debug, info, error)的最大数量,默认为50。maxDatabaseLogsCount
- 保存到数据库的每种类型日志的最大数量,默认为50。logger
- 自定义日志记录器。printLogsCompactly
- 如果值为false
,则除了HTTP日志外,所有日志都将有边框,并且会有一个链接到调用位置和创建时间的时间戳。否则,它只会写入日志消息。默认为true
。
- 可选:初始化检查器:
return MaterialApp(
home: const MainPage(),
builder: (context, child) => CrInspector(child: child!),
);
- 定义变量:
4.1 appInfo
- 可以提供自定义信息以显示在AppInfo页面:
CRLoggerInitializer.instance.appInfo = {
'Build type': buildType.toString(),
'Endpoint': 'https/cr_logger/example/',
};
4.2 logFileName
- 导出日志时的文件名。
4.3 hiddenFields
- 需要隐藏的网络日志头的键列表。
- 添加覆盖按钮:
CRLoggerInitializer.instance.showDebugButton(context);
button
- 自定义浮动按钮。left
- X轴起始位置。top
- Y轴起始位置。
- 支持从json导入日志:
await CRLoggerInitializer.instance.createLogsFromJson(json);
- 您可以获取当前的代理设置来初始化Charles:
final proxy = CRLoggerInitializer.instance.getProxySettings();
if (proxy != null) {
RestClient.instance.init(proxy);
}
使用方法
如果启用了日志记录器,屏幕上会出现一个浮动按钮;它还指示项目的构建号。点击浮动按钮即可显示日志记录器的主要屏幕。也可以通过双击按钮来调用快速操作。
快速操作
使用此弹出菜单,您可以快速访问所需的CRLogger选项。通过长按或双击调试按钮调用。
应用信息
允许查看包名、应用版本、构建版本。
清除日志
清除某些日志或所有日志。可以从数据库中清除日志。
显示检查器
如果启用了检查器,则屏幕右侧会出现一个面板,带有大小检查和颜色选择器的按钮。
设置Charles代理
需要设置代理设置以供Charles使用。
搜索
提供日志搜索。可以通过路径搜索HTTP日志。还可以搜索数据库中的日志。
分享日志
与团队分享日志。
创建带参数的日志
您可以创建带有参数的日志。为此,使用{{parameter}}
模式来突出显示需要作为参数显示的文本。
例如:
const parameter = 'PARAMETER';
log.d('Debug message with param: {{$parameter}}');
log.v('Verbose message with param: {{$parameter}}');
log.i('Info message with param: {{$parameter}}');
log.e('Error message with param: {{$parameter}}');
现在,只需单击详细信息中的参数值即可复制该值。
动作和值
打开包含动作按钮和值通知器的页面。
动作按钮允许您添加不同的回调进行测试。
- 添加动作:
CRLoggerInitializer.instance.addActionButton('Log Hi', () => log.i('Hi'));
CRLoggerInitializer.instance.addActionButton(
'Log By',
() => log.i('By'),
connectedWidgetId: 'some identifier',
);
- 通过指定ID移除动作:
CRLoggerInitializer.instance.removeActionsById('some identifier');
值通知器帮助跟踪ValueNotifier
类型的变量更改。
- 类型通知器:
/// 类型通知器
final boolNotifier = ValueNotifier<bool>(false);
final stringNotifier = ValueNotifier<String>('integer: ');
/// 小部件通知器
final boolWithWidgetNotifier = ValueNotifier<bool>(false);
final boolWidget = ValueListenableBuilder<bool>(
valueListenable: boolWithWidgetNotifier,
builder: (_, value, __) => SwitchListTile(
title: const Text('Bool'),
subtitle: Text(value.toString()),
value: value,
onChanged: (newValue) => boolWithWidgetNotifier.value = newValue,
),
);
final textNotifier = ValueNotifier<Text>(
const Text('Widget text'),
);
final textWidget = ValueListenableBuilder<Text>(
valueListenable: textNotifier,
builder: (_, value, __) => Row(
children: [
const Text('Icon'),
const Spacer(),
value,
const Spacer(),
],
),
);
- 添加通知器:
如果您只想查看通知器的值,最好使用
name
+notifier
。如果需要更改通知器的值,例如通过开关器,最好添加widget
。
CRLoggerInitializer.instance.addValueNotifier(
widget: boolWidget,
);
CRLoggerInitializer.instance.addValueNotifier(
name: 'Bool',
notifier: boolNotifier,
);
CRLoggerInitializer.instance.addValueNotifier(
widget: textWidget,
);
- 通过指定ID移除通知器:
CRLoggerInitializer.instance.removeNotifiersById('some identifier');
- 清除所有通知器:
CRLoggerInitializer.instance.notifierListClear();
- 可选:初始化以下回调:
ShareLogsFileCallback
- 当需要在应用程序端共享日志文件时:
CRLoggerInitializer.instance.onShareLogsFile = (path) async {
await Share.shareFiles([path]);
};
设置
在IntelliJ/Studio中,您可以折叠请求/响应体:
![Gif showing collapsible body](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/http-logs-example.gif)
通过转到Preferences -> Editor -> General -> Console
并在Fold console lines that contain
下添加这两条规则:║
,╟
,并在Exceptions
下添加一条规则:╔╣
。
![Settings](https://raw.githubusercontent.com/Cleveroad/cr_logger/master/screenshots/settings-screenshot.png)
示例代码
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:chopper/chopper.dart' as chopper;
import 'package:cr_logger/cr_logger.dart';
import 'package:cr_logger_example/generated/assets.dart';
import 'package:cr_logger_example/rest_client.dart';
import 'package:cr_logger_example/widgets/example_btn.dart';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dropzone/flutter_dropzone.dart';
import 'package:http/http.dart' as http;
import 'package:logger/logger.dart';
import 'package:share_plus/share_plus.dart';
Future<void> main() async {
// 如果主函数是异步的,首先调用此函数
WidgetsFlutterBinding.ensureInitialized();
// 首先!初始化日志记录器
await CRLoggerInitializer.instance.init(
useDatabase: true,
theme: ThemeData.light(),
levelColors: {
Level.debug: Colors.lightGreenAccent,
Level.warning: Colors.orange,
Level.trace: Colors.blueAccent,
Level.info: Colors.blueAccent,
Level.error: Colors.red,
Level.fatal: Colors.red.shade900,
Level.off: Colors.grey.shade300,
Level.all: Colors.grey.shade300,
},
hiddenFields: [
'Test',
'Test3',
'Test7',
'freeform',
'qwe',
],
hiddenHeaders: [
'content-type',
'Test3',
'Authorization',
],
logFileName: 'my_logs',
/// 为了在pub.dev上的web示例中显示日志
/// https://cleveroad.github.io/cr_logger/#/
useCrLoggerInReleaseBuild: !kReleaseMode,
);
// 第二!定义变量
CRLoggerInitializer.instance.appInfo = {
'Build type': 'release',
'Endpoint': 'https/cr_logger/example/',
};
final proxy = CRLoggerInitializer.instance.getProxySettings();
if (proxy != null) {
RestClient.instance.initDioProxyForCharles(proxy);
}
CRLoggerInitializer.instance.onShareLogsFile = (String path) async {
await Share.shareXFiles([XFile(path)]);
};
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: const MainPage(),
builder: (context, child) => CrInspector(child: child!),
theme: ThemeData(fontFamily: 'Epilogue'),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
[@override](/user/override)
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
static const platform = MethodChannel('com.cleveroad.cr_logger_example/logs');
final _debouncer = Debouncer(100);
late DropzoneViewController _dropCtrl;
bool _dragging = false;
[@override](/user/override)
void initState() {
super.initState();
// 第三!初始化调试按钮
CRLoggerInitializer.instance.showDebugButton(
context,
left: 16,
);
/// 类型通知器
final doubleNotifier = ValueNotifier<double>(0);
final boolNotifier = ValueNotifier<bool>(false);
final stringNotifier = ValueNotifier<String>('integer: ');
/// 小部件通知器
final iconNotifier = ValueNotifier<IconData>(Icons.clear);
final iconWidget = ValueListenableBuilder<IconData>(
valueListenable: iconNotifier,
builder: (_, value, __) => Row(
children: [
const Text('Icon'),
const Spacer(),
Icon(value),
const Spacer(),
],
),
);
final textNotifier = ValueNotifier<Text>(
const Text('Widget text'),
);
final textWidget = ValueListenableBuilder<Text>(
valueListenable: textNotifier,
builder: (_, value, __) => Row(
children: [
const Text('Icon'),
const Spacer(),
value,
const Spacer(),
],
),
);
final integerNotifier = ValueNotifier<int>(0);
final intWidget = ValueListenableBuilder<int>(
valueListenable: integerNotifier,
builder: (_, value, __) => Text('Int: ${value.toString()}'),
);
final boolWithWidgetNotifier = ValueNotifier<bool>(false);
final boolWidget = ValueListenableBuilder<bool>(
valueListenable: boolWithWidgetNotifier,
builder: (_, value, __) => SwitchListTile(
title: const Text('Bool'),
subtitle: Text(value.toString()),
value: value,
onChanged: (newValue) => boolWithWidgetNotifier.value = newValue,
),
);
/// 类型通知器变化
Timer.periodic(const Duration(seconds: 1), (_) => integerNotifier.value++);
Timer.periodic(
const Duration(milliseconds: 1),
(_) => doubleNotifier
..value += 0.1
..value = double.parse(doubleNotifier.value.toStringAsFixed(3)),
);
Timer.periodic(
const Duration(seconds: 1),
(_) => boolNotifier.value = !boolNotifier.value,
);
Timer.periodic(
const Duration(seconds: 1),
(_) => stringNotifier.value = 'number: ${integerNotifier.value}',
);
/// 小部件通知器变化
Timer.periodic(
const Duration(seconds: 1),
(_) => Icon(boolNotifier.value
? Icons.airline_seat_flat
: Icons.airline_seat_flat_angled),
);
Timer.periodic(
const Duration(seconds: 1),
(_) => iconNotifier.value = boolNotifier.value
? Icons.airline_seat_flat
: Icons.airline_seat_flat_angled,
);
Timer.periodic(
const Duration(seconds: 1),
(_) => textNotifier.value = Text(
'Widget text',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: boolNotifier.value ? FontWeight.bold : FontWeight.normal,
fontFamily: 'Epilogue',
),
),
);
/// 如果未定义小部件,则使用名称和值。
CRLoggerInitializer.instance.addValueNotifier(
widget: intWidget,
);
CRLoggerInitializer.instance.addValueNotifier(
widget: boolWidget,
);
CRLoggerInitializer.instance.addValueNotifier(
name: 'Double',
notifier: doubleNotifier,
);
CRLoggerInitializer.instance.addValueNotifier(
name: 'Bool',
notifier: boolNotifier,
);
CRLoggerInitializer.instance.addValueNotifier(
name: 'String',
notifier: stringNotifier,
);
CRLoggerInitializer.instance.addValueNotifier(
widget: iconWidget,
);
CRLoggerInitializer.instance.addValueNotifier(
widget: textWidget,
);
/// 动作
CRLoggerInitializer.instance.addActionButton('Log Hi', () => log.i('Hi'));
CRLoggerInitializer.instance.addActionButton('Log By', () => log.i('By'));
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF3F5F6),
body: SafeArea(
child: Column(
children: <Widget>[
const SizedBox(height: 20),
const Text(
'CR Logger example app',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 28),
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 592),
child: Column(
children: [
if (kIsWeb) ...[
Container(
height: 200,
decoration: BoxDecoration(
color: _dragging
? Colors.white.withOpacity(0.4)
: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Stack(
children: [
const Center(
child: Text(
'drop logs here',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: Colors.black,
),
),
),
DropzoneView(
key: UniqueKey(),
operation: DragOperation.move,
onCreated: (ctrl) => _dropCtrl = ctrl,
onDrop: _onDrop,
onHover: _onHover,
onLeave: _onLeave,
),
],
),
),
const SizedBox(height: 12),
],
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Make HTTP request',
assetName: Assets.assetsIcHttp,
onTap: _makeHttpRequest,
),
),
const SizedBox(width: 12),
Expanded(
child: ExampleBtn(
text: 'Make JSON log',
assetName: Assets.assetsIcJson,
onTap: _makeLogJson,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Log debug',
assetName: Assets.assetsIcDebug,
onTap: _makeLogDebug,
),
),
const SizedBox(width: 12),
Expanded(
child: ExampleBtn(
text: 'Log debug native',
assetName: Assets.assetsIcDebugNative,
onTap: kIsWeb ? null : _makeLogDebugNative,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Log warning',
assetName: Assets.assetsIcWarning,
onTap: _makeLogWarning,
),
),
const SizedBox(width: 12),
Expanded(
child: ExampleBtn(
text: 'Log warning native',
assetName: _getWarningNativeAsset(),
onTap: kIsWeb ? null : _makeLogWarningNative,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Log error',
assetName: Assets.assetsIcError,
onTap: _makeLogError,
),
),
const SizedBox(width: 12),
Expanded(
child: ExampleBtn(
text: 'Log error native',
assetName: _getErrorNativeAsset(),
onTap: kIsWeb ? null : _makeLogErrorNative,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Log debug with params',
assetName: Assets.assetsIcDebug,
onTap: _makeLogDebugWithParam,
),
),
const SizedBox(width: 12),
Expanded(
child: ExampleBtn(
text: 'Make native JSON log',
assetName: _getErrorNativeAsset(),
onTap: kIsWeb ? null : _makeNativeLogJson,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ExampleBtn(
text: 'Log debug with toast',
assetName: Assets.assetsIcDebug,
onTap: _makeLogDebugWithToast,
),
),
const SizedBox(width: 12),
const Expanded(child: SizedBox()),
],
),
const SizedBox(height: 12),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey,
),
onPressed: () => _toggleDebugButton(context),
child: const Text(
'Toggle debug button',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: Colors.white,
),
),
),
],
),
),
),
),
),
],
),
),
);
}
void _onHover() {
_debouncer.dispose();
if (!_dragging) {
setState(() => _dragging = true);
}
}
void _onLeave() {
_debouncer.run(() {
setState(() => _dragging = false);
});
}
void _toggleDebugButton(BuildContext context) {
if (CRLoggerInitializer.instance.isDebugButtonDisplayed) {
CRLoggerInitializer.instance.dismissDebugButton();
} else {
CRLoggerInitializer.instance.showDebugButton(
context,
left: 16,
);
}
}
/// HTTP请求示例
Future<void> _makeHttpRequest() async {
/// Dio包的请求示例
await _makeDioHttpRequest();
/// Chopper包的请求示例
//await _makeChopperHttpRequest();
/// Http包的请求示例
//await _makeRegularHttpRequest();
/// dart:io库的请求示例
//await _makeHttpClientRequest();
}
Future<void> _makeDioHttpRequest() async {
final queryParameters = <String, dynamic>{
'freeform': 'test',
'testParameter': 'test',
};
await RestClient.instance.dio.post(
'https://httpbin.org/anything',
queryParameters: queryParameters,
data: {
'Test': '1',
'Test2': {
'Test': [
{'Test': 'qwe'},
{'Test': 'qwe'},
],
},
'Test5': {
'Test9': [
{'name': 'qwe'},
{'Test7': 'qwe'},
],
},
'Test3': {'qwe': 1},
'Test4': {'qwe': 1},
},
options: dio.Options(
headers: {
'Authorization': 'qwewrrq',
'Test3': 'qwewrrq',
},
),
);
}
//忽略:未使用的元素
Future<void> _makeRegularHttpRequest() async {
final url = Uri.parse('https://httpbin.org/anything');
const body = {'Test': '1', 'Test2': 'qwe'};
/// 如果没有网络连接,http请求将抛出SocketException并且日志不会记录任何内容。
/// 您可以在try catch块中包装请求并自行记录错误
try {
final response = await http.post(url, body: body);
CRLoggerInitializer.instance.onHttpResponse(response, body);
} on SocketException catch (error) {
log.e(error.message);
}
}
//忽略:未使用的元素
Future<void> _makeHttpClientRequest() async {
final url = Uri.parse('https://httpbin.org/anything');
const body = {'Test': '1', 'Test2': 'qwe'};
/// 如果没有网络连接,http请求将抛出SocketException并且日志不会记录任何内容。
/// 您可以在try catch块中包装请求并自行记录错误
final client = HttpClient();
try {
final request = await client.postUrl(url);
request.headers.set(
HttpHeaders.contentTypeHeader,
'application/json; charset=UTF-8',
);
request.write('{"title": "Foo","body": "Bar", "userId": 99}');
CRLoggerInitializer.instance.onHttpClientRequest(request, body);
final response = await request.close();
CRLoggerInitializer.instance.onHttpClientResponse(
response,
request,
body,
);
} on SocketException catch (error) {
log.e(error.message);
}
}
//忽略:未使用的元素
Future<void> _makeChopperHttpRequest() async {
/// 如果没有网络连接,Chopper将记录请求,然后会收到SocketException并且不会记录响应。
/// 这会导致请求在日志中保持“正在发送”状态。
await RestClient.instance.chopper.send(chopper.Request(
'POST',
Uri.parse('https://httpbin.org/anything'),
Uri.parse(''),
headers: {
'Authorization': 'qwewrrq',
'Test3': 'qwewrrq',
},
body: {'Test': '1', 'Test2': 'qwe'},
));
}
String _getWarningNativeAsset() {
if (kIsWeb) {
return Assets.assetsIcWarning;
} else if (Platform.isIOS) {
return Assets.assetsIcWarningIos;
} else {
return Assets.assetsIcWarningAndroid;
}
}
String _getErrorNativeAsset() {
if (kIsWeb) {
return Assets.assetsIcError;
} else if (Platform.isIOS) {
return Assets.assetsIcErrorIos;
} else {
return Assets.assetsIcErrorAndroid;
}
}
void _makeLogDebug() {
log
..d('Debug message at ${DateTime.now().toIso8601String()}')
..t('Trace message at ${DateTime.now().toIso8601String()}');
}
void _makeLogDebugWithToast() {
log.d(
'Debug message at ${DateTime.now().toIso8601String()}',
showToast: true,
);
}
void _makeLogDebugWithParam() {
const token =
'e9U_NJKzXUtNkom7BTlOSn:APA91bF4vFgc8nsFE2SKt7XLDTdpvPPf6xlGicRXR9sxyu6Wfd48Xm00oh-r3TqGaKyEUixlfE7HYfE62V83skQYwzcvxAi34Lp7a9IxCuVBB9Ovxj-xZm5T_RFbtn_7di7v_dTU0fLD';
log
..d('Debug message with param: {{$token}}')
..t('Trace message with param: {{$token}}');
}
void _makeLogJson() {
final data = {
'a': 1,
'b': 2,
'c': 3,
'str1': 'string1',
'str2': 'string2',
'tempData': {
'a': 1,
'b': 2,
'c': 3,
'str1': 'string1',
'str2': 'string2',
},
};
log
..d(data)
..i(data)
..e(data);
}
void _makeLogWarning() {
log
..w('Warning message at ${DateTime.now().toIso8601String()}')
..i('Info message at ${DateTime.now().toIso8601String()}');
}
void _makeLogError() {
log
..e(
'Error message at ${DateTime.now().toIso8601String()}',
error: const HttpException('message'),
)
..f('Fatal message at ${DateTime.now().toIso8601String()}');
}
void _makeLogDebugNative() {
platform.invokeMethod('debug');
}
void _makeLogWarningNative() {
platform.invokeMethod('info');
}
void _makeLogErrorNative() {
platform.invokeMethod('error');
}
void _makeNativeLogJson() {
platform.invokeMethod('logJson');
}
Future<void> _onDrop(dynamic value) async {
final bytes = await _dropCtrl.getFileData(value);
try {
final json = await compute(jsonDecode, utf8.decode(bytes));
await CRLoggerInitializer.instance.createLogsFromJson(json);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logs successfully imported'),
),
);
} catch (ex) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Unsupported file'),
),
);
}
setState(() {
_dragging = false;
});
}
}
class Debouncer {
Debouncer(this.milliseconds);
final int milliseconds;
Timer? _timer;
void run(VoidCallback action) {
dispose();
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
void dispose() {
_timer?.cancel();
}
}
更多关于Flutter日志记录插件cr_logger的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html