flutter_device_apps 0.3.0 copy "flutter_device_apps: ^0.3.0" to clipboard
flutter_device_apps: ^0.3.0 copied to clipboard

PlatformAndroid

App-facing API for listing/inspecting installed apps (federated).

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_device_apps/flutter_device_apps.dart';

void main() {
  runApp(const MainApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Device Apps Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AppManagerScreen(),
    );
  }
}

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

  @override
  State<AppManagerScreen> createState() => _AppManagerScreenState();
}

class _AppManagerScreenState extends State<AppManagerScreen> {
  List<AppInfo> _apps = [];
  bool _loading = false;
  String _statusMessage = '';
  AppInfo? _selectedApp;
  StreamSubscription<AppChangeEvent>? _appChangeSubscription;
  bool _isMonitoring = false;
  final List<String> _changeEvents = [];

  // Filtering options
  bool _includeSystem = false;
  bool _onlyLaunchable = true;
  bool _includeIcons = false;

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

  @override
  void dispose() {
    _appChangeSubscription?.cancel();
    if (_isMonitoring) {
      FlutterDeviceApps.stopAppChangeStream();
    }
    super.dispose();
  }

  Future<void> _loadApps() async {
    setState(() {
      _loading = true;
      _statusMessage = 'Loading apps...';
    });

    try {
      final apps = await FlutterDeviceApps.listApps(
        includeSystem: _includeSystem,
        onlyLaunchable: _onlyLaunchable,
        includeIcons: _includeIcons,
      );

      setState(() {
        _apps = apps;
        _statusMessage = 'Found ${apps.length} apps';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error loading apps: $e';
      });
    } finally {
      setState(() {
        _loading = false;
      });
    }
  }

  Future<void> _getAppDetails(String packageName) async {
    setState(() {
      _loading = true;
      _statusMessage = 'Loading app details...';
    });

    try {
      final app = await FlutterDeviceApps.getApp(packageName, includeIcon: true);
      if (app != null) {
        setState(() {
          _selectedApp = app;
          _statusMessage = 'App details loaded';
        });
      } else {
        setState(() {
          _statusMessage = 'App not found';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error loading app details: $e';
      });
    } finally {
      setState(() {
        _loading = false;
      });
    }
  }

  Future<void> _openApp(String packageName) async {
    try {
      final success = await FlutterDeviceApps.openApp(packageName);
      setState(() {
        _statusMessage = success
            ? 'App opened successfully'
            : 'Failed to open app (not launchable?)';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening app: $e';
      });
    }
  }

  Future<void> _openAppSettings(String packageName) async {
    try {
      final success = await FlutterDeviceApps.openAppSettings(packageName);
      setState(() {
        _statusMessage = success ? 'App settings opened' : 'Failed to open app settings';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening app settings: $e';
      });
    }
  }

  Future<void> _uninstallApp(String packageName) async {
    try {
      final success = await FlutterDeviceApps.uninstallApp(packageName);
      setState(() {
        _statusMessage = success ? 'Uninstall dialog opened' : 'Failed to open uninstall dialog';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening uninstall dialog: $e';
      });
    }
  }

  Future<void> _getInstallerStore(String packageName) async {
    try {
      final store = await FlutterDeviceApps.getInstallerStore(packageName);
      setState(() {
        _statusMessage = store != null
            ? 'Installer: ${_getStoreDisplayName(store)}'
            : 'Unknown installer (sideloaded?)';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error getting installer info: $e';
      });
    }
  }

  Future<void> _toggleAppMonitoring() async {
    try {
      if (_isMonitoring) {
        await _appChangeSubscription?.cancel();
        await FlutterDeviceApps.stopAppChangeStream();
        setState(() {
          _isMonitoring = false;
          _statusMessage = 'Stopped monitoring app changes';
        });
      } else {
        await FlutterDeviceApps.startAppChangeStream();
        _appChangeSubscription = FlutterDeviceApps.appChanges.listen((event) {
          final eventText = '${event.type?.name.toUpperCase()} → ${event.packageName}';
          setState(() {
            _changeEvents.insert(0, eventText);
            if (_changeEvents.length > 10) {
              _changeEvents.removeLast();
            }
            _statusMessage = 'App change detected: $eventText';
          });
        });
        setState(() {
          _isMonitoring = true;
          _statusMessage = 'Started monitoring app changes';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error toggling monitoring: $e';
      });
    }
  }

  String _formatDateTime(DateTime? dateTime) {
    if (dateTime == null) return 'N/A';
    return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
  }

  String _getStoreDisplayName(String? store) {
    if (store == null) return 'Unknown/Sideloaded';

    final storeNames = {
      'com.android.vending': 'Google Play Store',
      'com.amazon.venezia': 'Amazon Appstore',
      'com.sec.android.app.samsungapps': 'Samsung Galaxy Store',
      'com.huawei.appmarket': 'Huawei AppGallery',
    };

    return storeNames[store] ?? store;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Flutter Device Apps Example'),
        actions: [
          IconButton(
            onPressed: _toggleAppMonitoring,
            icon: Icon(_isMonitoring ? Icons.stop : Icons.play_arrow),
            tooltip: _isMonitoring ? 'Stop Monitoring' : 'Start Monitoring',
          ),
        ],
      ),
      body: Column(
        children: [
          // Status bar
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(8.0),
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            child: Text(
              _statusMessage.isEmpty ? 'Ready' : _statusMessage,
              style: Theme.of(context).textTheme.bodyMedium,
              textAlign: TextAlign.center,
            ),
          ),
          // Filter section
          _buildFilterSection(),
          // Recent changes (if monitoring)
          if (_isMonitoring && _changeEvents.isNotEmpty) _buildChangeEventsSection(),
          // Main content
          Expanded(child: _buildMainContent()),
        ],
      ),
    );
  }

  Widget _buildFilterSection() {
    return Card(
      margin: const EdgeInsets.all(8.0),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Filter Options', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 12),

            // Checkbox options in a responsive wrap
            Wrap(
              spacing: 24.0,
              runSpacing: 8.0,
              children: [
                _buildCompactCheckbox(
                  'System Apps',
                  _includeSystem,
                  (value) => setState(() => _includeSystem = value ?? false),
                ),
                _buildCompactCheckbox(
                  'Launchable Only',
                  _onlyLaunchable,
                  (value) => setState(() => _onlyLaunchable = value ?? true),
                ),
                _buildCompactCheckbox(
                  'Include Icons',
                  _includeIcons,
                  (value) => setState(() => _includeIcons = value ?? false),
                ),
              ],
            ),

            const SizedBox(height: 16),

            // Refresh button
            Center(
              child: ElevatedButton.icon(
                onPressed: _loading ? null : _loadApps,
                icon: _loading
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.refresh, size: 18),
                label: const Text('Refresh Apps'),
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildChangeEventsSection() {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Recent Changes', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            ...(_changeEvents
                .take(3)
                .map((event) => Text('• $event', style: Theme.of(context).textTheme.bodySmall))),
          ],
        ),
      ),
    );
  }

  Widget _buildMainContent() {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // App list
        Expanded(flex: 2, child: _buildAppList()),
        // App details
        Expanded(flex: 2, child: _buildAppDetails()),
      ],
    );
  }

  Widget _buildAppList() {
    return Card(
      margin: const EdgeInsets.only(left: 8.0, right: 4.0, bottom: 8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'Installed Apps (${_apps.length})',
              style: Theme.of(context).textTheme.titleMedium,
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _apps.length,
              itemBuilder: (context, index) {
                final app = _apps[index];
                return ListTile(
                  leading: app.iconBytes != null
                      ? Image.memory(
                          app.iconBytes!,
                          width: 32,
                          height: 32,
                          errorBuilder: (context, error, stackTrace) =>
                              const Icon(Icons.android, size: 32),
                        )
                      : const Icon(Icons.android, size: 32),
                  title: Text(
                    app.appName ?? 'Unknown',
                    style: const TextStyle(fontSize: 14),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  subtitle: Text(
                    app.packageName ?? '',
                    style: const TextStyle(fontSize: 12),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  trailing: app.isSystem == true ? const Icon(Icons.settings, size: 16) : null,
                  onTap: () => _getAppDetails(app.packageName ?? ''),
                  dense: true,
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildAppDetails() {
    return Card(
      margin: const EdgeInsets.only(left: 4.0, right: 8.0, bottom: 8.0),
      child: _selectedApp == null
          ? const Center(
              child: Text(
                'Select an app to view details',
                style: TextStyle(fontSize: 16, color: Colors.grey),
              ),
            )
          : SingleChildScrollView(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // App icon and name
                  Row(
                    children: [
                      if (_selectedApp!.iconBytes != null)
                        Image.memory(
                          _selectedApp!.iconBytes!,
                          width: 64,
                          height: 64,
                          errorBuilder: (context, error, stackTrace) =>
                              const Icon(Icons.android, size: 64),
                        )
                      else
                        const Icon(Icons.android, size: 64),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              _selectedApp!.appName ?? 'Unknown',
                              style: Theme.of(context).textTheme.titleLarge,
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                            Text(
                              _selectedApp!.packageName ?? '',
                              style: Theme.of(context).textTheme.bodyMedium,
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),

                  const SizedBox(height: 16),

                  // App details
                  _buildDetailRow(
                    'Version',
                    '${_selectedApp!.versionName ?? 'N/A'} (${_selectedApp!.versionCode ?? 'N/A'})',
                  ),
                  _buildDetailRow('First Install', _formatDateTime(_selectedApp!.firstInstallTime)),
                  _buildDetailRow('Last Update', _formatDateTime(_selectedApp!.lastUpdateTime)),
                  _buildDetailRow('System App', _selectedApp!.isSystem == true ? 'Yes' : 'No'),

                  const SizedBox(height: 16),

                  // Action buttons
                  Text('Actions', style: Theme.of(context).textTheme.titleMedium),
                  const SizedBox(height: 8),

                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: [
                      ElevatedButton.icon(
                        onPressed: () => _openApp(_selectedApp!.packageName!),
                        icon: const Icon(Icons.launch, size: 16),
                        label: const Text('Open'),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _openAppSettings(_selectedApp!.packageName!),
                        icon: const Icon(Icons.settings, size: 16),
                        label: const Text('Settings'),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _uninstallApp(_selectedApp!.packageName!),
                        icon: const Icon(Icons.delete, size: 16),
                        label: const Text('Uninstall'),
                        style: ElevatedButton.styleFrom(foregroundColor: Colors.red),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _getInstallerStore(_selectedApp!.packageName!),
                        icon: const Icon(Icons.store, size: 16),
                        label: const Text('Installer'),
                      ),
                    ],
                  ),
                ],
              ),
            ),
    );
  }

  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 100,
            child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.bold)),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }

  Widget _buildCompactCheckbox(String label, bool value, ValueChanged<bool?> onChanged) {
    return InkWell(
      onTap: () => onChanged(!value),
      borderRadius: BorderRadius.circular(4),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              height: 24,
              width: 24,
              child: Checkbox(
                value: value,
                onChanged: onChanged,
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
              ),
            ),
            const SizedBox(width: 8),
            Text(label, style: Theme.of(context).textTheme.bodyMedium),
          ],
        ),
      ),
    );
  }
}