x_app_utils 0.4.0 copy "x_app_utils: ^0.4.0" to clipboard
x_app_utils: ^0.4.0 copied to clipboard

Unified native app, device, locale, timezone, and identifier information for Flutter apps on iOS and Android.

example/lib/main.dart

import 'dart:convert';

import 'package:x_app_utils/x_app_utils.dart';
import 'package:x_app_utils/event_bus_manager_flutter.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await XAppUtils.instance.init();
  runApp(const ExampleApp());
}

final class CounterChangedEvent {
  const CounterChangedEvent({required this.source, required this.value});

  final String source;
  final int value;
}

final class ExampleMessageEvent {
  const ExampleMessageEvent(this.message);

  final String message;
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'EventManager Example',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const EventManagerHomePage(),
    );
  }
}

class EventManagerHomePage extends StatefulWidget {
  const EventManagerHomePage({super.key});

  @override
  State<EventManagerHomePage> createState() => _EventManagerHomePageState();
}

class _EventManagerHomePageState extends State<EventManagerHomePage>
    with EventManagerStateMixin<EventManagerHomePage> {
  final List<String> _eventLog = <String>[];

  @override
  void initState() {
    super.initState();

    // `dynamic` receives all event types. The State Mixin cancels this
    // subscription automatically when this page is disposed.
    EventBusManager.owner(this).listen<dynamic>((event) {
      if (!mounted) {
        return;
      }
      setState(() {
        _eventLog.insert(0, '${event.runtimeType}: ${_eventText(event)}');
        if (_eventLog.length > 8) {
          _eventLog.removeLast();
        }
      });
    });
  }

  String _eventText(dynamic event) => switch (event) {
        CounterChangedEvent() => '${event.source} = ${event.value}',
        ExampleMessageEvent() => event.message,
        _ => event.toString(),
      };

  @override
  Widget build(BuildContext context) {
    final info = XAppUtils.instance;
    final platformSummary = <String>[
      'Platform: ${info.platform.isEmpty ? '-' : info.platform}',
      'App: ${info.appName.isEmpty ? '-' : info.appName}',
      'Device: ${info.deviceModel.isEmpty ? '-' : info.deviceModel}',
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('EventManager 示例')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          Text('当前平台信息', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 8),
          ...platformSummary.map(Text.new),
          const SizedBox(height: 24),
          Text('事件场景', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 8),
          FilledButton(
            onPressed: () => Navigator.of(context).push<void>(
              MaterialPageRoute<void>(
                builder: (_) => const StateEventManagerPage(),
              ),
            ),
            child: const Text('Flutter State 自动销毁'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).push<void>(
              MaterialPageRoute<void>(
                builder: (_) => const ServiceEventManagerPage(),
              ),
            ),
            child: const Text('普通服务对象关闭'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).push<void>(
              MaterialPageRoute<void>(
                builder: (_) => const XAppUtilsConsolePage(),
              ),
            ),
            child: const Text('XAppUtils 全功能面板'),
          ),
          const SizedBox(height: 24),
          Text('全局事件日志', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 8),
          if (_eventLog.isEmpty)
            const Text('尚未发送事件')
          else
            ..._eventLog.map(
              (entry) => ListTile(
                dense: true,
                contentPadding: EdgeInsets.zero,
                title: Text(entry),
              ),
            ),
        ],
      ),
    );
  }
}

class StateEventManagerPage extends StatefulWidget {
  const StateEventManagerPage({super.key});

  @override
  State<StateEventManagerPage> createState() => _StateEventManagerPageState();
}

class _StateEventManagerPageState extends State<StateEventManagerPage>
    with EventManagerStateMixin<StateEventManagerPage> {
  late final EventSubscription<CounterChangedEvent> _subscription;
  int _receivedValue = 0;
  bool _paused = false;

  @override
  void initState() {
    super.initState();
    _subscription = EventBusManager.owner(this).listen<CounterChangedEvent>(
      (event) {
        if (!mounted) {
          return;
        }
        setState(() => _receivedValue = event.value);
      },
    );
  }

  void _fire() {
    EventBusManager.fire(
      CounterChangedEvent(source: 'State page', value: _receivedValue + 1),
    );
  }

  void _togglePause() {
    setState(() {
      _paused = !_paused;
      if (_paused) {
        _subscription.pause();
      } else {
        _subscription.resume();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter State 自动销毁')),
      body: _EventPageBody(
        receivedLabel: 'State 接收值: $_receivedValue',
        paused: _paused,
        onFire: _fire,
        onTogglePause: _togglePause,
        description: '返回上一页时 State.dispose() 自动取消 owner(this) 的订阅。',
      ),
    );
  }
}

class ExampleEventService with EventManagerMixin {
  final ValueNotifier<String> latestMessage = ValueNotifier<String>('尚未接收消息');

  void start() {
    EventBusManager.owner(this).listen<ExampleMessageEvent>((event) {
      latestMessage.value = event.message;
    });
  }

  void close() {
    disposeEventManager();
    latestMessage.dispose();
  }
}

class ServiceEventManagerPage extends StatefulWidget {
  const ServiceEventManagerPage({super.key});

  @override
  State<ServiceEventManagerPage> createState() =>
      _ServiceEventManagerPageState();
}

class _ServiceEventManagerPageState extends State<ServiceEventManagerPage> {
  late final ExampleEventService _service;
  int _messageNumber = 0;

  @override
  void initState() {
    super.initState();
    _service = ExampleEventService()..start();
  }

  @override
  void dispose() {
    _service.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('普通服务对象关闭')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            const Text('服务对象没有框架生命周期,需要在 close() 中调用 disposeEventManager()。'),
            const SizedBox(height: 16),
            ValueListenableBuilder<String>(
              valueListenable: _service.latestMessage,
              builder: (context, message, _) => Text('最新消息: $message'),
            ),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: () {
                _messageNumber++;
                EventBusManager.fire(
                  ExampleMessageEvent('服务消息 #$_messageNumber'),
                );
              },
              child: const Text('发送服务消息'),
            ),
          ],
        ),
      ),
    );
  }
}

class XAppUtilsConsolePage extends StatefulWidget {
  const XAppUtilsConsolePage({super.key});

  @override
  State<XAppUtilsConsolePage> createState() => _XAppUtilsConsolePageState();
}

class _XAppUtilsConsolePageState extends State<XAppUtilsConsolePage> {
  static const JsonEncoder _jsonEncoder = JsonEncoder.withIndent('  ');

  final TextEditingController _fallbackNamespaceController =
      TextEditingController(text: 'x_app_utils_example');
  final TextEditingController _localIdController =
      TextEditingController(text: 'example-local-id');
  final Map<String, String> _outputs = <String, String>{};

  LocalIdSlot _slot = LocalIdSlot.primary;
  bool _running = false;

  XAppUtils get _utils => XAppUtils.instance;

  @override
  void dispose() {
    _fallbackNamespaceController.dispose();
    _localIdController.dispose();
    super.dispose();
  }

  void _append(String key, String value) {
    if (!mounted) {
      return;
    }
    setState(() {
      _outputs[key] = '[${DateTime.now().toIso8601String()}]\n$value';
    });
  }

  Future<void> _run(String label, Future<dynamic> Function() action) async {
    if (_running) {
      return;
    }

    setState(() => _running = true);
    try {
      final result = await action();
      _append(label, '$label\n$result');
    } catch (error, stackTrace) {
      _append(label, '$label 失败\n$error\n$stackTrace');
    } finally {
      if (mounted) {
        setState(() => _running = false);
      }
    }
  }

  void _printData(String label) {
    _append(label, '完整参数 JSON\n${_jsonEncoder.convert(_utils.data)}');
  }

  void _printSummary(String label) {
    final summary = <String, dynamic>{
      'isInitialized': _utils.isInitialized,
      'appName': _utils.appName,
      'packageName': _utils.packageName,
      'version': _utils.version,
      'buildNumber': _utils.buildNumber,
      'platform': _utils.platform,
      'deviceModel': _utils.deviceModel,
      'osVersion': _utils.osVersion,
      'locale': _utils.locale,
      'timeZone': _utils.timeZone,
      'advertisingId': _utils.advertisingId,
      'deviceId': _utils.deviceId,
      'primaryLocalId': _utils.primaryLocalId,
      'secondaryLocalId': _utils.secondaryLocalId,
    };
    _append(label, '常用参数\n${_jsonEncoder.convert(summary)}');
  }

  Future<Map<String, dynamic>> _configureLocalIds() async {
    final namespace = _fallbackNamespaceController.text.trim();
    await _utils.configureLocalIds(
      LocalIdStorageOptions(
        fallbackNamespace: namespace.isEmpty ? null : namespace,
      ),
    );
    return _utils.data;
  }

  Widget _actionButton({
    required String label,
    required VoidCallback onPressed,
  }) {
    return _ActionButton(
      label: label,
      running: _running,
      onPressed: onPressed,
      output: _outputs[label],
    );
  }

  @override
  Widget build(BuildContext context) {
    final localIdLabel = _slot == LocalIdSlot.primary ? '主本地 ID' : '副本地 ID';

    return Scaffold(
      appBar: AppBar(title: const Text('XAppUtils 全功能面板')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          Row(
            children: <Widget>[
              const Expanded(child: Text('每个按钮的返回值、异常和参数会显示在按钮下方。')),
              TextButton(
                onPressed:
                    _outputs.isEmpty ? null : () => setState(_outputs.clear),
                child: const Text('清空输出'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          _ConsoleSection(
            title: '初始化与刷新',
            children: <Widget>[
              _actionButton(
                label: '初始化 init()',
                onPressed: () => _run('初始化 init()', _utils.init),
              ),
              _actionButton(
                label: '等待 ready',
                onPressed: () =>
                    _run('等待 ready', () async => (await _utils.ready).data),
              ),
              _actionButton(
                label: '刷新全部 refresh()',
                onPressed: () => _run('刷新全部 refresh()', _utils.refresh),
              ),
              _actionButton(
                label: '刷新广告 ID',
                onPressed: () => _run('刷新广告 ID', () async {
                  await _utils.refreshAdvertisingId();
                  return _utils.advertisingId;
                }),
              ),
              _actionButton(
                label: '刷新设备 ID',
                onPressed: () => _run('刷新设备 ID', () async {
                  await _utils.refreshDeviceId();
                  return _utils.deviceId;
                }),
              ),
              _actionButton(
                label: '请求用户跟踪授权',
                onPressed: () => _run('请求用户跟踪授权', () async {
                  final result = await _utils.requestAppTrackingTransparency();
                  return <String, dynamic>{
                    'isSuccess': result.isSuccess,
                    'idfa': result.idfa,
                    'failure': result.failure.name,
                  };
                }),
              ),
            ],
          ),
          _ConsoleSection(
            title: '参数打印',
            children: <Widget>[
              _actionButton(
                label: '打印常用参数',
                onPressed: () => _printSummary('打印常用参数'),
              ),
              _actionButton(
                label: '打印完整参数 JSON',
                onPressed: () => _printData('打印完整参数 JSON'),
              ),
            ],
          ),
          _ConsoleSection(
            title: '本地安全 ID',
            children: <Widget>[
              TextField(
                controller: _fallbackNamespaceController,
                decoration: const InputDecoration(
                  labelText: '兜底 namespace(可选)',
                ),
              ),
              const SizedBox(height: 8),
              _actionButton(
                label: '配置本地 ID 存储',
                onPressed: () => _run('配置本地 ID 存储', _configureLocalIds),
              ),
              DropdownButtonFormField<LocalIdSlot>(
                initialValue: _slot,
                decoration: const InputDecoration(labelText: 'ID 槽位'),
                items: LocalIdSlot.values
                    .map(
                      (slot) => DropdownMenuItem<LocalIdSlot>(
                        value: slot,
                        child:
                            Text(slot == LocalIdSlot.primary ? '主 ID' : '副 ID'),
                      ),
                    )
                    .toList(),
                onChanged: _running
                    ? null
                    : (slot) {
                        if (slot != null) {
                          setState(() => _slot = slot);
                        }
                      },
              ),
              TextField(
                controller: _localIdController,
                decoration: InputDecoration(labelText: '$localIdLabel 写入值'),
              ),
              const SizedBox(height: 8),
              _actionButton(
                label: '读取 $localIdLabel',
                onPressed: () => _run('读取 $localIdLabel', () {
                  return _utils.readLocalId(slot: _slot);
                }),
              ),
              _actionButton(
                label: '写入 $localIdLabel',
                onPressed: () => _run('写入 $localIdLabel', () async {
                  await _utils.writeLocalId(
                    _localIdController.text,
                    slot: _slot,
                  );
                  return _utils.data;
                }),
              ),
              _actionButton(
                label: '检查 $localIdLabel 是否存在',
                onPressed: () => _run('检查 $localIdLabel 是否存在', () {
                  return _utils.containsLocalId(slot: _slot);
                }),
              ),
              _actionButton(
                label: '重置 $localIdLabel',
                onPressed: () => _run('重置 $localIdLabel', () {
                  return _utils.resetLocalId(slot: _slot);
                }),
              ),
              _actionButton(
                label: '读取全部本地 ID',
                onPressed: () => _run('读取全部本地 ID', _utils.readAllLocalIds),
              ),
              _actionButton(
                label: '重置全部本地 ID',
                onPressed: () => _run('重置全部本地 ID', _utils.resetAllLocalIds),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _ConsoleSection extends StatelessWidget {
  const _ConsoleSection({required this.title, required this.children});

  final String title;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Text(title, style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            ...children,
          ],
        ),
      ),
    );
  }
}

class _ActionButton extends StatelessWidget {
  const _ActionButton({
    required this.label,
    required this.running,
    required this.onPressed,
    this.output,
  });

  final String label;
  final bool running;
  final VoidCallback onPressed;
  final String? output;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          OutlinedButton(
            onPressed: running ? null : onPressed,
            child: Text(label),
          ),
          if (output case final String value) ...<Widget>[
            const SizedBox(height: 6),
            DecoratedBox(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: SelectableText(value),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _EventPageBody extends StatelessWidget {
  const _EventPageBody({
    required this.receivedLabel,
    required this.paused,
    required this.onFire,
    required this.onTogglePause,
    required this.description,
  });

  final String receivedLabel;
  final bool paused;
  final VoidCallback onFire;
  final VoidCallback onTogglePause;
  final String description;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Text(description),
          const SizedBox(height: 16),
          Text(receivedLabel, style: Theme.of(context).textTheme.headlineSmall),
          const SizedBox(height: 16),
          FilledButton(
              onPressed: onFire, child: const Text('发送 CounterChangedEvent')),
          OutlinedButton(
            onPressed: onTogglePause,
            child: Text(paused ? '恢复监听' : '暂停监听并丢弃事件'),
          ),
        ],
      ),
    );
  }
}
0
likes
140
points
36
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Unified native app, device, locale, timezone, and identifier information for Flutter apps on iOS and Android.

Repository (GitHub)
View/report issues

Topics

#app-info #device-info #locale #advertising-id #identifiers

License

MIT (license)

Dependencies

flutter

More

Packages that depend on x_app_utils

Packages that implement x_app_utils