health_connector_hc_android 1.4.0 copy "health_connector_hc_android: ^1.4.0" to clipboard
health_connector_hc_android: ^1.4.0 copied to clipboard

PlatformAndroid

Android implementation of health_connector using Health Connect

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:health_connector_core/health_connector_core.dart';
import 'package:health_connector_hc_android/health_connector_hc_android.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Health Connector HC Android Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ExampleAppHomePage(),
    );
  }
}

/// Example app demonstrating all public API methods of HealthConnectorHCClient.
///
/// This page provides buttons to test each method with clear explanations
/// and error handling.
class ExampleAppHomePage extends StatefulWidget {
  const ExampleAppHomePage({super.key});

  @override
  State<ExampleAppHomePage> createState() => _ExampleAppHomePageState();
}

class _ExampleAppHomePageState extends State<ExampleAppHomePage> {
  // Instance of HealthConnectorHCClient to use for all API calls
  final _client = const HealthConnectorHCClient();

  // Loading state to show overlay during async operations
  bool _isPageLoading = false;
  bool _isLoading = false;

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

  /// Shows a snack bar with the given message and color.
  void _showSnackBar(String message, Color backgroundColor) {
    if (!mounted) {
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: backgroundColor,
        duration: const Duration(seconds: 3),
      ),
    );
  }

  /// Shows a success message.
  void _showSuccess(String message) {
    _showSnackBar(message, Colors.green);
  }

  /// Shows an error message.
  void _showError(String message) {
    _showSnackBar(message, Colors.red);
  }

  /// Shows an info message.
  void _showInfo(String message) {
    _showSnackBar(message, Colors.blue);
  }

  /// Demonstrates [HealthConnectorHCClient.getHealthPlatformStatus] method.
  ///
  /// Checks if Health Connect is available, unavailable, or requires
  /// installation/update on the device.
  Future<void> _getHealthPlatformStatus() async {
    setState(() {
      _isPageLoading = true;
    });

    try {
      final status = await _client.getHealthPlatformStatus();

      final statusMessage = switch (status) {
        HealthPlatformStatus.available => 'Health Connect is available',
        HealthPlatformStatus.unavailable =>
          'Health Connect is unavailable on this device',
        HealthPlatformStatus.installationOrUpdateRequired =>
          'Health Connect installation or update is required',
      };

      if (!mounted) {
        return;
      }
      _showSuccess(statusMessage);
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to get platform status: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isPageLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.requestPermissions] method.
  ///
  /// Requests both health data permissions (read/write for steps) and
  /// feature permissions (background reading, history access).
  Future<void> _requestPermissions() async {
    setState(() {
      _isLoading = true;
    });

    try {
      // Build a list of permissions to request
      final permissions = <Permission>[
        // Request health data read and write permissions
        HealthDataType.steps.readPermission,
        HealthDataType.steps.writePermission,
        HealthDataType.weight.readPermission,
        HealthDataType.weight.writePermission,
        // ...

        // Request feature permissions
        HealthPlatformFeature.readHealthDataInBackground.permission,
        HealthPlatformFeature.readHealthDataHistory.permission,
        // ...
      ];

      final results = await _client.requestPermissions(permissions);

      // Count permissions by status
      final grantedCount = results
          .where((r) => r.status == PermissionStatus.granted)
          .length;
      final deniedCount = results
          .where((r) => r.status == PermissionStatus.denied)
          .length;
      final unknownCount = results
          .where((r) => r.status == PermissionStatus.unknown)
          .length;

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Permission request completed: $grantedCount granted, '
        '$deniedCount denied, $unknownCount unknown',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to request permissions: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.getGrantedPermissions] method.
  ///
  /// Retrieves all permissions that are currently granted to the app.
  /// This is useful for checking which data types the app has access to
  /// without prompting the user.
  Future<void> _getGrantedPermissions() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final grantedPermissions = await _client.getGrantedPermissions();

      // Count permissions by type
      final dataTypePermissions = grantedPermissions
          .whereType<HealthDataPermission>()
          .length;
      final featurePermissions = grantedPermissions
          .whereType<HealthPlatformFeaturePermission>()
          .length;

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Found ${grantedPermissions.length} granted permissions: '
        '$dataTypePermissions data type(s), $featurePermissions feature(s)',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to get granted permissions: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.revokeAllPermissions] method.
  ///
  /// Revokes all health data permissions that were previously granted.
  /// After calling this, the app will need to request permissions again.
  Future<void> _revokeAllPermissions() async {
    setState(() {
      _isLoading = true;
    });

    try {
      await _client.revokeAllPermissions();

      if (!mounted) {
        return;
      }
      _showSuccess('All permissions have been revoked');
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to revoke permissions: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.getFeatureStatus] method.
  ///
  /// Checks the status of all available platform features to see which
  /// ones are available, unavailable, or require an update.
  Future<void> _getFeatureStatus() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final results = <String>[];

      // Check status for each available feature
      for (final feature in HealthPlatformFeature.values) {
        final status = await _client.getFeatureStatus(feature);
        results.add('$feature: ${status.name}');
      }

      if (!mounted) {
        return;
      }
      _showInfo('Feature Status:\n${results.join('\n')}');
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to get feature status: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.readRecord] method.
  ///
  /// Reads a single health record by its ID. First, we read some records
  /// to get an actual record ID, then read that specific record.
  Future<void> _readRecord() async {
    setState(() {
      _isLoading = true;
    });

    try {
      // First, read some records to get an actual record ID
      final now = DateTime.now();
      final readRecordsRequest = HealthDataType.steps.readRecords(
        startTime: now.subtract(const Duration(days: 7)),
        endTime: now,
        pageSize: 1,
      );

      final recordsResponse = await _client.readRecords(readRecordsRequest);

      if (recordsResponse.records.isEmpty) {
        if (!mounted) {
          return;
        }
        _showInfo(
          'No records found. Please write some records first using '
          'the "Write Record" button.',
        );
        return;
      }

      // Read the first record by ID
      final recordId = recordsResponse.records.first.id;
      final readRecordRequest = HealthDataType.steps.readRecord(recordId);
      final record = await _client.readRecord(readRecordRequest);

      if (!mounted) {
        return;
      }
      if (record != null) {
        _showSuccess(
          'Read record: ${record.count.value} steps from '
          '${record.startTime} to ${record.endTime}',
        );
      } else {
        _showInfo('Record not found');
      }
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to read record: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.readRecords] method.
  ///
  /// Reads multiple health records within a time range with pagination support.
  /// This is useful for querying historical data.
  Future<void> _readRecords() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final now = DateTime.now();
      final request = HealthDataType.steps.readRecords(
        startTime: now.subtract(const Duration(days: 7)),
        endTime: now,
        pageSize: 10,
      );

      final response = await _client.readRecords(request);

      final message = response.records.isEmpty
          ? 'No records found in the last 7 days'
          : 'Found ${response.records.length} record(s)'
                '${response.hasMorePages ? ' (more available)' : ''}';

      if (!mounted) {
        return;
      }
      _showSuccess(message);
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to read records: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.writeRecord] method.
  ///
  /// Writes a single health record to Health Connect. The platform will
  /// assign a unique ID to the record.
  Future<void> _writeRecord() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final now = DateTime.now();

      // Create a step record with sample data
      final record = StepRecord(
        startTime: now.subtract(const Duration(hours: 1)),
        endTime: now,
        count: const Numeric(1000),
        metadata: Metadata.automaticallyRecorded(
          dataOrigin: const DataOrigin('com.example.health_connector'),
          device: const Device.fromType(DeviceType.phone),
        ),
      );

      final recordId = await _client.writeRecord(record);

      if (!mounted) {
        return;
      }
      _showSuccess('Successfully wrote record with ID: ${recordId.value}');
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to write record: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.writeRecords] method.
  ///
  /// Writes multiple health records in a single operation. This is more
  /// efficient than writing records one by one.
  Future<void> _writeRecords() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final now = DateTime.now();

      // Create multiple step records with sample data
      final records = [
        StepRecord(
          startTime: now.subtract(const Duration(hours: 3)),
          endTime: now.subtract(const Duration(hours: 2)),
          count: const Numeric(1500),
          metadata: Metadata.automaticallyRecorded(
            dataOrigin: const DataOrigin('com.example.health_connector'),
            device: const Device.fromType(DeviceType.watch),
          ),
        ),
        StepRecord(
          startTime: now.subtract(const Duration(hours: 2)),
          endTime: now.subtract(const Duration(hours: 1)),
          count: const Numeric(2000),
          metadata: Metadata.automaticallyRecorded(
            dataOrigin: const DataOrigin('com.example.health_connector'),
            device: const Device.fromType(DeviceType.watch),
          ),
        ),
        StepRecord(
          startTime: now.subtract(const Duration(hours: 1)),
          endTime: now,
          count: const Numeric(1800),
          metadata: Metadata.automaticallyRecorded(
            dataOrigin: const DataOrigin('com.example.health_connector'),
            device: const Device.fromType(DeviceType.phone),
          ),
        ),
      ];

      final recordIds = await _client.writeRecords(records);

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Successfully wrote ${recordIds.length} record(s)',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to write records: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.updateRecord] method.
  ///
  /// Updates an existing health record. First, we read a record, then
  /// update it with new values.
  Future<void> _updateRecord() async {
    setState(() {
      _isLoading = true;
    });

    try {
      // First, read some records to get an actual record ID
      final now = DateTime.now();
      final readRecordsRequest = HealthDataType.steps.readRecords(
        startTime: now.subtract(const Duration(days: 7)),
        endTime: now,
        pageSize: 1,
      );

      final recordsResponse = await _client.readRecords(readRecordsRequest);

      if (recordsResponse.records.isEmpty) {
        if (!mounted) {
          return;
        }
        _showInfo(
          'No records found to update. Please write some records first '
          'using the "Write Record" button.',
        );
        return;
      }

      // Get the first record and update it
      final existingRecord = recordsResponse.records.first;
      final updatedRecord = StepRecord(
        id: existingRecord.id,
        startTime: existingRecord.startTime,
        endTime: existingRecord.endTime,
        count: Numeric(existingRecord.count.value + 100),
        // Add 100 steps
        metadata: existingRecord.metadata,
      );

      final updatedRecordId = await _client.updateRecord(updatedRecord);

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Successfully updated record with ID: ${updatedRecordId.value}',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to update record: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.aggregate] method.
  ///
  /// Aggregates health data over a time range. For steps, we can calculate
  /// the sum of all steps in the specified time period.
  Future<void> _aggregate() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final now = DateTime.now();

      // Create an aggregate request to sum all steps in the last 7 days
      final request = HealthDataType.steps.aggregateSum(
        startTime: now.subtract(const Duration(days: 7)),
        endTime: now,
      );

      final response = await _client.aggregate(request);

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Total steps in last 7 days: ${response.value.value}',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to aggregate data: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.deleteRecords] method.
  ///
  /// Deletes all health records of a specific type within a time range.
  /// Use with caution as this operation cannot be undone.
  Future<void> _deleteRecords() async {
    setState(() {
      _isLoading = true;
    });

    try {
      final now = DateTime.now();

      await _client.deleteRecords(
        dataType: HealthDataType.steps,
        startTime: now.subtract(const Duration(hours: 1)),
        endTime: now,
      );

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Successfully deleted step records from the last hour',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to delete records: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  /// Demonstrates [HealthConnectorHCClient.deleteRecordsByIds] method.
  ///
  /// Deletes specific health records by their IDs. First, we read some
  /// records to get their IDs, then delete those specific records.
  Future<void> _deleteRecordsByIds() async {
    setState(() {
      _isLoading = true;
    });

    try {
      // First, read some records to get IDs
      final now = DateTime.now();
      final readRequest = HealthDataType.steps.readRecords(
        startTime: now.subtract(const Duration(days: 7)),
        endTime: now,
        pageSize: 5,
      );

      final response = await _client.readRecords(readRequest);

      if (response.records.isEmpty) {
        if (!mounted) {
          return;
        }
        _showInfo('No records found to delete');
        return;
      }

      // Get IDs of the records to delete
      final recordIds = response.records.map((r) => r.id).toList();

      await _client.deleteRecordsByIds(
        dataType: HealthDataType.steps,
        recordIds: recordIds,
      );

      if (!mounted) {
        return;
      }
      _showSuccess(
        'Successfully deleted ${recordIds.length} record(s)',
      );
    } on HealthConnectorException catch (e) {
      if (!mounted) {
        return;
      }
      _showError('Failed to delete records: ${e.message}');
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Health Connector HC Android Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _LoadingOverlay(
        isLoading: _isPageLoading,
        message: 'Loading...',
        child: Stack(
          children: [
            // Main content with scrollable list of buttons
            SingleChildScrollView(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Permissions
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _requestPermissions,
                    icon: const Icon(Icons.lock_open),
                    label: const Text('Request Permissions'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _getGrantedPermissions,
                    icon: const Icon(Icons.check_circle),
                    label: const Text('Get Granted Permissions'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _revokeAllPermissions,
                    icon: const Icon(Icons.lock),
                    label: const Text('Revoke All Permissions'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),

                  // Features
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _getFeatureStatus,
                    icon: const Icon(Icons.featured_play_list),
                    label: const Text('Get Feature Status'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),

                  // Reading Records
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _readRecord,
                    icon: const Icon(Icons.read_more),
                    label: const Text('Read Record'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _readRecords,
                    icon: const Icon(Icons.list),
                    label: const Text('Read Records'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),

                  // Writing Records
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _writeRecord,
                    icon: const Icon(Icons.edit),
                    label: const Text('Write Record'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _writeRecords,
                    icon: const Icon(Icons.add),
                    label: const Text('Write Records'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _updateRecord,
                    icon: const Icon(Icons.update),
                    label: const Text('Update Record'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),

                  // Aggregation
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _aggregate,
                    icon: const Icon(Icons.calculate),
                    label: const Text('Aggregate Data'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),

                  // Deleting Records
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _deleteRecords,
                    icon: const Icon(Icons.delete),
                    label: const Text('Delete Records (Time Range)'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _isLoading ? null : _deleteRecordsByIds,
                    icon: const Icon(Icons.delete_sweep),
                    label: const Text('Delete Records (By IDs)'),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.all(16.0),
                    ),
                  ),
                ],
              ),
            ),

            // Loading overlay
            if (_isLoading) const _LoadingIndicator(),
          ],
        ),
      ),
    );
  }
}

/// A widget that displays a loading indicator in the center of a page.
/// Typically used to indicate that a page is initializing or loading data.
class _LoadingIndicator extends StatelessWidget {
  const _LoadingIndicator({this.message});

  /// Optional message to display below the loading indicator.
  final String? message;

  @override
  Widget build(BuildContext context) {
    final color =
        Theme.of(context).progressIndicatorTheme.color ??
        Theme.of(context).colorScheme.primary;

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(color: color),
          if (message != null) ...[
            const SizedBox(height: 16),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 32.0),
              child: Text(
                message!,
                style: Theme.of(
                  context,
                ).textTheme.bodyMedium?.copyWith(color: color),
                textAlign: TextAlign.center,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

/// A widget that displays a gray transparent overlay with a loading indicator
/// in the center. Typically used during button operations like form submission
/// to prevent user interaction while an operation is in progress.
class _LoadingOverlay extends StatelessWidget {
  const _LoadingOverlay({
    required this.isLoading,
    required this.child,
    this.message,
  });

  /// Whether the overlay should be displayed.
  final bool isLoading;

  /// The child widget to display behind the overlay.
  final Widget child;

  /// Optional message to display below the loading indicator.
  final String? message;

  @override
  Widget build(BuildContext context) {
    return PopScope(
      child: Stack(
        children: [
          child,
          if (isLoading)
            Positioned.fill(
              child: AbsorbPointer(
                child: ColoredBox(
                  color: Colors.grey.withValues(alpha: 0.5),
                  child: _LoadingIndicator(message: message),
                ),
              ),
            ),
        ],
      ),
    );
  }
}
4
likes
160
points
528
downloads

Publisher

unverified uploader

Weekly Downloads

Android implementation of health_connector using Health Connect

Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

flutter, health_connector_core, health_connector_logger, meta

More

Packages that depend on health_connector_hc_android

Packages that implement health_connector_hc_android