feat: add device_info_plus, TagTemplates & UserInfo button
Flutter CI / analyze-and-test (push) Waiting to run
Details
Flutter CI / analyze-and-test (push) Waiting to run
Details
- Integrate device_info_plus for readable OS version and device model - Add TagTemplates utility class with industry-standard custom tags: - businessContext, userSegmentation, forScreen, technicalContext - campaign (UTM), ecommerce (GA4), session, content - Use snake_case naming convention for data warehouse compatibility - Add Set UserInfo button to example app - Update DeviceInfo to use Platform.operatingSystemVersion
This commit is contained in:
parent
3277efd47c
commit
ee9e6739b5
|
|
@ -228,10 +228,27 @@ class _DemoPageState extends State<DemoPage> {
|
|||
url: 'https://example.com/demo',
|
||||
buttonId: 'demo_btn_01',
|
||||
),
|
||||
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||||
customTags: TagTemplates.merge([
|
||||
TagTemplates.businessContext(
|
||||
tenantId: 't1',
|
||||
appVersion: '1.0.0',
|
||||
channel: 'internal_test',
|
||||
),
|
||||
TagTemplates.forScreen(screenName: 'DemoPage', featureModule: 'demo'),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setUserInfo() async {
|
||||
const userInfo = UserInfo(
|
||||
userId: 10086,
|
||||
userName: 'Test User',
|
||||
account: 'test_account',
|
||||
);
|
||||
await Analytics.setUser(userInfo);
|
||||
_addLog('Set user: ${userInfo.toJson()}');
|
||||
}
|
||||
|
||||
Future<void> _flushNow() async {
|
||||
await Analytics.flush(force: true);
|
||||
}
|
||||
|
|
@ -510,6 +527,16 @@ class _DemoPageState extends State<DemoPage> {
|
|||
),
|
||||
child: const Text('Track Demo Event'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isRunning('set_user')
|
||||
? null
|
||||
: () => _runAction(
|
||||
'set_user',
|
||||
'Set User Info',
|
||||
_setUserInfo,
|
||||
),
|
||||
child: const Text('Set User Info'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isRunning('flush')
|
||||
? null
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import device_info_plus
|
||||
import path_provider_foundation
|
||||
import sqflite_darwin
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "12.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -81,6 +97,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -99,6 +123,11 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -376,6 +405,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class DeviceInfo {
|
|||
required this.screenResolution,
|
||||
});
|
||||
|
||||
/// 操作系统名称或版本。
|
||||
/// 操作系统名称或版本(例如 "Android 10" 或 "Version 14.4")。
|
||||
final String os;
|
||||
|
||||
/// 设备型号。
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:yx_tracking_flutter/src/model/device_info.dart';
|
||||
|
|
@ -17,10 +19,53 @@ class DeviceUtil {
|
|||
static Future<DeviceInfo> collectDeviceInfo() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final os = Platform.operatingSystem;
|
||||
final model = Platform.operatingSystemVersion;
|
||||
var os = 'unknown';
|
||||
var model = 'unknown';
|
||||
final screenResolution = _screenResolution();
|
||||
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
if (kIsWeb) {
|
||||
final webInfo = await deviceInfo.webBrowserInfo;
|
||||
os = '${webInfo.browserName.name} ${webInfo.appVersion}';
|
||||
model = webInfo.userAgent ?? 'unknown';
|
||||
} else {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
// 例如: Android 12
|
||||
os = 'Android ${androidInfo.version.release}';
|
||||
// 例如: Google Pixel 6
|
||||
model = '${androidInfo.brand} ${androidInfo.model}';
|
||||
} else if (Platform.isIOS) {
|
||||
final iosInfo = await deviceInfo.iosInfo;
|
||||
// 例如: iOS 15.4
|
||||
os = '${iosInfo.systemName} ${iosInfo.systemVersion}';
|
||||
// 例如: iPhone14,2
|
||||
model = iosInfo.utsname.machine;
|
||||
} else if (Platform.isMacOS) {
|
||||
final macInfo = await deviceInfo.macOsInfo;
|
||||
os = 'macOS ${macInfo.majorVersion}.${macInfo.minorVersion}';
|
||||
model = macInfo.model;
|
||||
} else if (Platform.isWindows) {
|
||||
final windowsInfo = await deviceInfo.windowsInfo;
|
||||
os =
|
||||
'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
|
||||
model = 'PC';
|
||||
} else if (Platform.isLinux) {
|
||||
final linuxInfo = await deviceInfo.linuxInfo;
|
||||
os = 'Linux ${linuxInfo.versionId}';
|
||||
model = linuxInfo.name;
|
||||
} else {
|
||||
os = Platform.operatingSystemVersion;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback if plugin fails
|
||||
if (!kIsWeb) {
|
||||
os = Platform.operatingSystemVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return DeviceInfo(
|
||||
os: os,
|
||||
model: model,
|
||||
|
|
@ -29,6 +74,7 @@ class DeviceUtil {
|
|||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
/// 测试钩子:通过注入 view 列表计算分辨率。
|
||||
static String screenResolutionForTesting({
|
||||
Iterable<FlutterView> Function()? viewsProvider,
|
||||
|
|
@ -37,6 +83,7 @@ class DeviceUtil {
|
|||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
/// 测试钩子:通过 size 计算分辨率字符串。
|
||||
static String resolutionFromSizeForTesting(Size size) {
|
||||
return _resolutionFromSize(size);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,277 @@
|
|||
/// 自定义标签模板工具类。
|
||||
///
|
||||
/// 提供符合行业标准(Firebase GA4、Amplitude、Mixpanel)的自定义标签模板,
|
||||
/// 使用 snake_case 命名规范,便于与数据仓库(BigQuery、Snowflake)集成。
|
||||
///
|
||||
/// ## 设计原则 / Design Principle
|
||||
///
|
||||
/// `TagTemplates` 是 `Event` 的**补充**,不重复 `Event` 已有的字段:
|
||||
///
|
||||
/// | Event 已有字段 | 说明 |
|
||||
/// |---|---|
|
||||
/// | `systemCode` | 系统编码(配置提供) |
|
||||
/// | `eventType` | 事件类型(调用时传入) |
|
||||
/// | `userInfo` | 用户信息(`Analytics.setUser` 设置) |
|
||||
/// | `clientType` | 客户端类型(自动检测:iOS/Android/Web) |
|
||||
/// | `deviceInfo` | 设备信息(自动采集:OS/Model/屏幕) |
|
||||
/// | `timestamp` | 时间戳(自动生成) |
|
||||
///
|
||||
/// `TagTemplates` 提供 `Event` **未覆盖**的业务维度标签。
|
||||
class TagTemplates {
|
||||
/// 私有构造,避免外部实例化。
|
||||
TagTemplates._();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 业务上下文标签 / Business Context Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建业务上下文标签。
|
||||
///
|
||||
/// > **注意**: 不包含 `platform`,因为 `Event.clientType` 已自动检测平台。
|
||||
///
|
||||
/// - [tenantId]: 租户 ID(多租户系统必填)。
|
||||
/// - [appVersion]: 应用版本号,例如 "1.2.3"。
|
||||
/// - [channel]: 分发渠道,例如 "appstore", "huawei", "xiaomi"。
|
||||
/// - [abTestGroup]: A/B 测试组,例如 "control", "variant_a"。
|
||||
/// - [environment]: 环境标识,例如 "dev", "staging", "prod"。
|
||||
static Map<String, dynamic> businessContext({
|
||||
required String tenantId,
|
||||
String? appVersion,
|
||||
String? channel,
|
||||
String? abTestGroup,
|
||||
String? environment,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
'tenant_id': tenantId,
|
||||
if (appVersion != null) 'app_version': appVersion,
|
||||
if (channel != null) 'channel': channel,
|
||||
if (abTestGroup != null) 'ab_test_group': abTestGroup,
|
||||
if (environment != null) 'environment': environment,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 用户分群标签 / User Segmentation Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建用户分群标签。
|
||||
///
|
||||
/// - [userType]: 用户类型,例如 "free", "premium", "enterprise"。
|
||||
/// - [subscriptionTier]: 订阅级别,例如 "basic", "pro", "enterprise"。
|
||||
/// - [isFirstTimeUser]: 是否为首次使用用户。
|
||||
/// - [daysFromSignup]: 注册天数,用于生命周期分析。
|
||||
/// - [cohort]: 用户群组标识,例如 "2024-01", "beta_testers"。
|
||||
/// - [acquisitionSource]: 获客来源,例如 "organic", "paid", "referral"。
|
||||
/// - [lifetimeValueTier]: 用户价值层级,例如 "high", "medium", "low"。
|
||||
static Map<String, dynamic> userSegmentation({
|
||||
String? userType,
|
||||
String? subscriptionTier,
|
||||
bool? isFirstTimeUser,
|
||||
int? daysFromSignup,
|
||||
String? cohort,
|
||||
String? acquisitionSource,
|
||||
String? lifetimeValueTier,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (userType != null) 'user_type': userType,
|
||||
if (subscriptionTier != null) 'subscription_tier': subscriptionTier,
|
||||
if (isFirstTimeUser != null) 'is_first_time_user': isFirstTimeUser,
|
||||
if (daysFromSignup != null) 'days_from_signup': daysFromSignup,
|
||||
if (cohort != null) 'cohort': cohort,
|
||||
if (acquisitionSource != null) 'acquisition_source': acquisitionSource,
|
||||
if (lifetimeValueTier != null) 'lifetime_value_tier': lifetimeValueTier,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 功能追踪标签 / Feature Tracking Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建页面/功能追踪标签。
|
||||
///
|
||||
/// - [screenName]: 当前页面名称。
|
||||
/// - [featureModule]: 功能模块,例如 "profile", "checkout", "search"。
|
||||
/// - [entrySource]: 入口来源,例如 "deeplink", "push", "organic"。
|
||||
/// - [previousScreen]: 上一个页面名称(用于漏斗分析)。
|
||||
/// - [sessionDepth]: 当前会话中的页面深度。
|
||||
static Map<String, dynamic> forScreen({
|
||||
required String screenName,
|
||||
String? featureModule,
|
||||
String? entrySource,
|
||||
String? previousScreen,
|
||||
int? sessionDepth,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
'screen_name': screenName,
|
||||
if (featureModule != null) 'feature_module': featureModule,
|
||||
if (entrySource != null) 'entry_source': entrySource,
|
||||
if (previousScreen != null) 'previous_screen': previousScreen,
|
||||
if (sessionDepth != null) 'session_depth': sessionDepth,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 技术上下文标签 / Technical Context Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建技术上下文标签。
|
||||
///
|
||||
/// - [networkType]: 网络类型,例如 "wifi", "4g", "5g"。
|
||||
/// - [isOffline]: 是否离线模式。
|
||||
/// - [buildMode]: 构建模式,例如 "debug", "release", "profile"。
|
||||
/// - [appInstallSource]: 安装来源,例如 "play_store", "app_store"。
|
||||
/// - [locale]: 语言区域,例如 "zh_CN", "en_US"。
|
||||
/// - [timezone]: 时区,例如 "Asia/Shanghai", "America/New_York"。
|
||||
static Map<String, dynamic> technicalContext({
|
||||
String? networkType,
|
||||
bool? isOffline,
|
||||
String? buildMode,
|
||||
String? appInstallSource,
|
||||
String? locale,
|
||||
String? timezone,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (networkType != null) 'network_type': networkType,
|
||||
if (isOffline != null) 'is_offline': isOffline,
|
||||
if (buildMode != null) 'build_mode': buildMode,
|
||||
if (appInstallSource != null) 'app_install_source': appInstallSource,
|
||||
if (locale != null) 'locale': locale,
|
||||
if (timezone != null) 'timezone': timezone,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 营销归因标签 / Campaign Attribution Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建营销归因标签(UTM 参数)。
|
||||
///
|
||||
/// - [campaignId]: 活动 ID。
|
||||
/// - [campaignName]: 活动名称。
|
||||
/// - [campaignSource]: 流量来源,例如 "google", "facebook", "tiktok"。
|
||||
/// - [campaignMedium]: 媒介类型,例如 "cpc", "email", "social"。
|
||||
/// - [campaignTerm]: 搜索关键词(付费搜索)。
|
||||
/// - [campaignContent]: 广告内容/创意标识。
|
||||
static Map<String, dynamic> campaign({
|
||||
String? campaignId,
|
||||
String? campaignName,
|
||||
String? campaignSource,
|
||||
String? campaignMedium,
|
||||
String? campaignTerm,
|
||||
String? campaignContent,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (campaignId != null) 'campaign_id': campaignId,
|
||||
if (campaignName != null) 'campaign_name': campaignName,
|
||||
if (campaignSource != null) 'campaign_source': campaignSource,
|
||||
if (campaignMedium != null) 'campaign_medium': campaignMedium,
|
||||
if (campaignTerm != null) 'campaign_term': campaignTerm,
|
||||
if (campaignContent != null) 'campaign_content': campaignContent,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 电商标签 / E-commerce Tags (Firebase GA4 Compatible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建电商标签(兼容 Firebase GA4 标准)。
|
||||
///
|
||||
/// - [currency]: 货币代码,例如 "CNY", "USD"。
|
||||
/// - [value]: 交易金额。
|
||||
/// - [transactionId]: 交易 ID。
|
||||
/// - [paymentMethod]: 支付方式,例如 "alipay", "wechat_pay", "credit_card"。
|
||||
/// - [coupon]: 优惠券代码。
|
||||
/// - [shipping]: 运费。
|
||||
/// - [tax]: 税费。
|
||||
static Map<String, dynamic> ecommerce({
|
||||
String? currency,
|
||||
double? value,
|
||||
String? transactionId,
|
||||
String? paymentMethod,
|
||||
String? coupon,
|
||||
double? shipping,
|
||||
double? tax,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (currency != null) 'currency': currency,
|
||||
if (value != null) 'value': value,
|
||||
if (transactionId != null) 'transaction_id': transactionId,
|
||||
if (paymentMethod != null) 'payment_method': paymentMethod,
|
||||
if (coupon != null) 'coupon': coupon,
|
||||
if (shipping != null) 'shipping': shipping,
|
||||
if (tax != null) 'tax': tax,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 会话上下文标签 / Session Context Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建会话上下文标签。
|
||||
///
|
||||
/// - [sessionId]: 会话唯一标识。
|
||||
/// - [sessionNumber]: 第几次会话(用户历史会话计数)。
|
||||
/// - [eventIndex]: 当前事件在会话中的序号。
|
||||
/// - [sessionDurationSeconds]: 会话持续时长(秒)。
|
||||
static Map<String, dynamic> session({
|
||||
String? sessionId,
|
||||
int? sessionNumber,
|
||||
int? eventIndex,
|
||||
int? sessionDurationSeconds,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (sessionId != null) 'session_id': sessionId,
|
||||
if (sessionNumber != null) 'session_number': sessionNumber,
|
||||
if (eventIndex != null) 'event_index': eventIndex,
|
||||
if (sessionDurationSeconds != null)
|
||||
'session_duration_seconds': sessionDurationSeconds,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 内容/媒体标签 / Content/Media Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 创建内容/媒体标签。
|
||||
///
|
||||
/// - [contentType]: 内容类型,例如 "video", "article", "audio", "image"。
|
||||
/// - [contentId]: 内容唯一标识。
|
||||
/// - [contentName]: 内容名称/标题。
|
||||
/// - [contentCategory]: 内容分类。
|
||||
/// - [contentDurationSeconds]: 内容时长(秒,适用于视频/音频)。
|
||||
/// - [contentAuthor]: 内容作者/创作者。
|
||||
static Map<String, dynamic> content({
|
||||
String? contentType,
|
||||
String? contentId,
|
||||
String? contentName,
|
||||
String? contentCategory,
|
||||
int? contentDurationSeconds,
|
||||
String? contentAuthor,
|
||||
}) =>
|
||||
<String, dynamic>{
|
||||
if (contentType != null) 'content_type': contentType,
|
||||
if (contentId != null) 'content_id': contentId,
|
||||
if (contentName != null) 'content_name': contentName,
|
||||
if (contentCategory != null) 'content_category': contentCategory,
|
||||
if (contentDurationSeconds != null)
|
||||
'content_duration_seconds': contentDurationSeconds,
|
||||
if (contentAuthor != null) 'content_author': contentAuthor,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 合并工具 / Merge Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 合并多个标签模板为一个 Map。
|
||||
///
|
||||
/// 后面的模板会覆盖前面的同名键。
|
||||
///
|
||||
/// 示例:
|
||||
/// ```dart
|
||||
/// final tags = TagTemplates.merge([
|
||||
/// TagTemplates.businessContext(tenantId: 't1', appVersion: '1.0.0'),
|
||||
/// TagTemplates.forScreen(screenName: 'HomePage'),
|
||||
/// TagTemplates.campaign(campaignSource: 'google', campaignMedium: 'cpc'),
|
||||
/// ]);
|
||||
/// ```
|
||||
static Map<String, dynamic> merge(List<Map<String, dynamic>> templates) {
|
||||
final result = <String, dynamic>{};
|
||||
for (final template in templates) {
|
||||
result.addAll(template);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ export 'src/model/device_info.dart';
|
|||
export 'src/model/event.dart';
|
||||
export 'src/model/recent_event_summary.dart';
|
||||
export 'src/model/user_info.dart';
|
||||
export 'src/util/tag_templates.dart';
|
||||
|
||||
/// 对外唯一入口(Facade)。
|
||||
class Analytics {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ environment:
|
|||
flutter: ">=3.22.0"
|
||||
|
||||
dependencies:
|
||||
device_info_plus: ^12.3.0
|
||||
dio: ^5.4.3+1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
|
|
|||
Loading…
Reference in New Issue