Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/flutter_build_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 17
java-version: 21

- name: Install Flutter
uses: subosito/flutter-action@v2
Expand All @@ -39,3 +39,8 @@ jobs:
run: |
cd example
flutter build appbundle --release

- name: Run native Android tests
run: |
cd example/android
./gradlew :android_id:testDebugUnitTest --console=plain
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.6.1

* Clarify Android ID lifetime, platform behavior and AGP 9 migration steps.
* Handle missing plugin registration in the example.

## 0.6.0

* **Breaking:** Require Flutter 3.47 and Android API 24 or newer.
Expand Down
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@ A Flutter plugin for retrieving the Android ID.

## Getting started

* Add plugin to pubspec.yaml
* Use it in your code (see examples of all methods below)
Add the matching dependency to your `pubspec.yaml`:

**Flutter 3.47+**

```yaml
dependencies:
android_id: ^0.6.1
```

**Flutter 3.10–<3.47**

```yaml
dependencies:
android_id: ^0.5.2+1
```

## Usage

```dart
import 'package:android_id/android_id.dart';

const _androidIdPlugin = AndroidId();

final String? androidId = await _androidIdPlugin.getId();
Expand All @@ -24,6 +39,9 @@ final String? androidId = await _androidIdPlugin.getId();
Treat registration/runtime failures as `null` by wrapping the call in a `try`/`catch`:

```dart
import 'package:android_id/android_id.dart';
import 'package:flutter/services.dart';

const _androidIdPlugin = AndroidId();

String? androidId;
Expand Down Expand Up @@ -79,7 +97,12 @@ The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin
support since AGP 9.0.
```

Most Flutter apps should not need app-code changes.
Use Java 17 or newer to build the Android app. Existing apps must use AGP 9
with `android.builtInKotlin=true` (the AGP 9 default); the temporary
`android.builtInKotlin=false` opt-out is not supported by this release. Updating
Flutter alone does not update an existing app's Gradle configuration. See the
[example Android configuration](example/android) and Flutter's
[AGP 9 migration guide](https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers).

## Troubleshooting

Expand All @@ -92,7 +115,7 @@ If you're experiencing `MissingPluginException`, try these steps in order:
flutter run
```

2. **Check Flutter Version**: Ensure you're using Flutter 3.47.0 or higher
2. **Check Flutter Version**: Ensure your Flutter version matches the dependency version selected above
```bash
flutter --version
```
Expand Down
23 changes: 17 additions & 6 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ android {
minSdk = 24
}

dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.20.0")
}

testOptions {
unitTests.includeAndroidResources = true
unitTests.all {
jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED",
"--add-opens=java.base/java.io=ALL-UNNAMED",
"--add-opens=java.base/java.net=ALL-UNNAMED",
"--add-opens=java.base/java.security=ALL-UNNAMED",
"--add-opens=java.base/java.text=ALL-UNNAMED",
"--add-opens=java.base/jdk.internal.access=ALL-UNNAMED"
}
unitTests.all {
useJUnitPlatform()
// Robolectric uses JUnit 4.

testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
Expand All @@ -46,3 +51,9 @@ kotlin {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}

dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.17")
testImplementation("org.mockito:mockito-core:5.20.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dev.fluttercommunity.android_id

import android.content.ContentResolver
import android.provider.Settings
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [24, 26, 36])
class AndroidIdPluginTest {
private lateinit var plugin: AndroidIdPlugin
private lateinit var binding: FlutterPlugin.FlutterPluginBinding
private lateinit var messenger: BinaryMessenger
private lateinit var result: MethodChannel.Result

@Before
fun setUp() {
plugin = AndroidIdPlugin()
binding = mock(FlutterPlugin.FlutterPluginBinding::class.java)
messenger = mock(BinaryMessenger::class.java)
result = mock(MethodChannel.Result::class.java)
`when`(binding.applicationContext).thenReturn(RuntimeEnvironment.getApplication())
`when`(binding.binaryMessenger).thenReturn(messenger)
plugin.onAttachedToEngine(binding)
}

@Test
fun returnsTheSecureAndroidId() {
Settings.Secure.putString(binding.applicationContext.contentResolver, Settings.Secure.ANDROID_ID, "abc123")
plugin.onMethodCall(MethodCall("getId", null), result)
verify(result).success("abc123")
verifyNoMoreInteractions(result)
}

@Test
fun returnsNullWhenTheSettingIsUnavailable() {
Settings.Secure.putString(binding.applicationContext.contentResolver, Settings.Secure.ANDROID_ID, null)
plugin.onMethodCall(MethodCall("getId", null), result)
verify(result).success(null)
verifyNoMoreInteractions(result)
}

@Test
@Config(shadows = [DeniedSecureSettings::class])
fun returnsPlatformErrorWhenTheSettingCannotBeRead() {
plugin.onMethodCall(MethodCall("getId", null), result)
verify(result).error("ERROR_GETTING_ID", "Failed to get Android ID", "Access denied")
verifyNoMoreInteractions(result)
}

@Test
fun unknownMethodsAreNotImplemented() {
plugin.onMethodCall(MethodCall("unknown", null), result)
verify(result).notImplemented()
verifyNoMoreInteractions(result)
}

@Test
fun detachingUnregistersTheChannel() {
clearInvocations(messenger)
plugin.onDetachedFromEngine(binding)
verify(messenger).setMessageHandler("android_id", null)
}
}

@Implements(Settings.Secure::class)
class DeniedSecureSettings {
companion object {
@JvmStatic
@Implementation
fun getString(resolver: ContentResolver, name: String): String? {
if (name == Settings.Secure.ANDROID_ID) throw SecurityException("Access denied")
return null
}
}
}
18 changes: 8 additions & 10 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
# android_id_example

Demonstrates how to use the android_id plugin.
Shows the Android ID and handles missing registration or lookup failures. Other platforms display an unavailable ID.

## Getting Started
Flutter 3.47 or newer. Run on Android API 24 or newer to retrieve an ID.

This project is a starting point for a Flutter application.
For Android tooling and platform requirements, see the [package README](../README.md).

A few resources to get you started if this is your first Flutter project:
From this directory, select a compatible Flutter SDK with FVM, then run:

- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
```sh
fvm flutter pub get
fvm flutter run -d <device-id>
```
3 changes: 3 additions & 0 deletions example/android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks

# Kotlin compiler session state
.kotlin/
7 changes: 3 additions & 4 deletions example/integration_test/android_id_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ void main() {
expect(id1, equals(id2), reason: 'Android ID should be consistent across multiple calls');
});

testWidgets('getId should work after hot restart', (WidgetTester tester) async {
// This test verifies that the plugin remains registered and functional
// after pumping frames. Note: Hot restart cannot be simulated in integration tests.
testWidgets('getId remains consistent after frames settle', (WidgetTester tester) async {
// Pumping frames verifies repeated calls; this does not simulate hot restart.

final id = await androidIdPlugin.getId();
expect(id, isNotNull);

// Pump frames to simulate app lifecycle
// Allow scheduled frames to settle
await tester.pumpAndSettle();

// Should still work
Expand Down
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class _MyAppState extends State<MyApp> {
// We also handle the message potentially returning null.
try {
androidId = await _androidIdPlugin.getId() ?? 'Unknown ID';
} on MissingPluginException {
androidId = 'Android ID plugin is not registered.';
} on PlatformException {
androidId = 'Failed to get Android ID.';
}
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.6.0"
version: "0.6.1"
async:
dependency: transitive
description:
Expand Down
9 changes: 9 additions & 0 deletions example/test/android_id_example_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@ void main() {
findsOneWidget,
);
});

testWidgets('Shows a registration error when the plugin is missing', (tester) async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (_) async {
throw MissingPluginException();
});
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
expect(find.text('Android ID: Android ID plugin is not registered.'), findsOneWidget);
});
}
9 changes: 8 additions & 1 deletion lib/android_id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ class AndroidId {
/// The method channel used to interact with the native platform.
static const _methodChannel = MethodChannel('android_id');

/// Calls the native method to retrieve the Android ID.
/// Returns Android's `Settings.Secure.ANDROID_ID`, or `null` when unavailable.
///
/// Returns `null` on non-Android platforms, including web. On Android 8.0 and
/// newer, the ID is scoped to the app-signing key, user, and device; it can
/// change after a factory reset or signing-key change.
///
/// Throws [MissingPluginException] when the Android plugin is not registered
/// and [PlatformException] when the native lookup fails.
Future<String?> getId() async {
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
if (!isAndroid) return null;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ topics:
- android
- native

version: 0.6.0
version: 0.6.1

environment:
sdk: '>=3.0.0 <4.0.0'
Expand Down
Loading
Loading