Flutter / Inclusive interfaces and target evidence / Lesson 16
Platform channels and permission boundaries
Model native calls as fallible asynchronous contracts and keep platform capabilities behind explicit adapters.
On this page
Lesson goals
- A plugin crosses more than a Dart method boundary
- Define channel names and payloads as a protocol
- Translate a native result into an application contract
A plugin crosses more than a Dart method boundary
A platform feature can involve Dart code, a channel codec, native registration, platform APIs, entitlement or manifest configuration, user permission, and device capability. A successful analyzer run covers only part of that path. Before adopting a plugin, review its supported targets, maintained configuration instructions, and failure semantics for the project’s selected versions. A feature working on Android does not establish an iOS or web implementation. Keep native capability access behind a small adapter so the rest of the application can distinguish available, unavailable, denied, and failed outcomes without importing platform details everywhere.
Define channel names and payloads as a protocol
Method-channel names and method identifiers form a protocol shared with the native implementation. Names should be scoped to avoid collisions, and payload values must be supported by the selected codec. Document units, nullability, error categories, and compatibility policy just as for a network boundary. Dart static types do not validate a native implementation’s returned value. A nullable result may mean unsupported capability, missing value, or a bug depending on the protocol; choose one meaning explicitly rather than forcing it to zero. Keep platform calls away from build methods so rebuilding cannot repeat a permission prompt.
Translate a native result into an application contract
The first fragment calls a deliberately synthetic channel for a battery percentage. It checks the permitted numeric range and reports malformed native data distinctly. There is no native counterpart in this course, so executing it would not establish a valid capability implementation. MissingPluginException and PlatformException are possible integration outcomes that the adapter should translate according to the application contract. Do not present the fragment as a way to bypass a permission or query a real device from the authoring environment. The channel is an illustration of a boundary requiring target work.
import 'package:flutter/services.dart';
const batteryChannel = MethodChannel('example.learning/battery');
Future<int?> readBattery() async {
final value = await batteryChannel.invokeMethod<int>('readPercent');
if (value == null) return null;
if (value < 0 || value > 100) {
throw const FormatException('Invalid battery percentage');
}
return value;
}
A conforming native implementation returns null or an integer from 0 through 100.
No native channel handler is supplied or executed. The expected result describes a protocol contract, not observed device output.
Use a fake capability to test application decisions
The second fragment gives the application a narrow battery-reader dependency and a deterministic fake. A model can use this fake to test low-battery presentation without needing native registration or hardware. Add fakes for unavailable and failure states when those are meaningful. This is a test seam, not evidence that the production channel codec or native API is correct. Keep platform integration tests separate and execute them only in an authorized target environment. The distinction prevents a convenient unit test from becoming an unsupported claim about a device feature.
abstract interface class BatteryReader { Future<int?> readPercent(); }
class FakeBattery implements BatteryReader {
FakeBattery(this.value);
final int? value;
@override Future<int?> readPercent() async => value;
}
Future<String> batteryLabel(BatteryReader reader) async {
final value = await reader.readPercent();
return value == null ? 'Unavailable' : '$value%';
}
// await batteryLabel(FakeBattery(25)) -> '25%'
The fake value 25 produces 25%; a null fake produces Unavailable.
Only application mapping is represented. The fake does not validate native registration, permissions, or codec compatibility.
Ask for permissions at a comprehensible moment
Permission should be requested when the user can understand which feature needs it and what happens if it is denied. Platform states can include limited access, denial, restrictions, or settings changes; they are not always one boolean. Follow the current platform and plugin documentation for the selected target instead of repeatedly prompting whenever a screen rebuilds. Preserve a useful fallback where possible. When the app resumes, a permission may have changed outside the app, so refresh capability state at a deliberate lifecycle boundary without automatically repeating the user’s original command.
Diagnose the boundary that actually failed
A missing channel handler, malformed payload, denied permission, and absent device capability require different fixes. Capture the first relevant exception, target platform, build variant, plugin versions, and minimal synthetic call. Review registration and configuration before changing unrelated business logic. A hot reload may not apply native configuration changes, so use the documented rebuild workflow in the authorized development host. Do not claim that cleaning build files is a universal solution. Verify the repaired feature on each intended target, including denial and recovery, and retain the application-level fake tests for predictable policy coverage.
Common errors and fixes
A channel reports MissingPluginException
- Why it happens
- No matching native implementation is registered for the target and method.
- How to fix it
- Check target support, registration, exact protocol names, and required rebuild.
- Prevent it next time
- Keep a target capability matrix and a minimal integration fixture.
A permission dialog repeats unexpectedly
- Why it happens
- Permission requests are triggered by rendering or indiscriminate resume logic.
- How to fix it
- Request from an explicit feature action and refresh status separately.
- Prevent it next time
- Test rebuild and resume without user activation.
A fake capability test is mistaken for native validation
- Why it happens
- The adapter boundary was never exercised on the target.
- How to fix it
- Report fake policy tests separately from target integration results.
- Prevent it next time
- Record exactly which implementation was used.
Check your understanding
1. Should null battery data automatically become zero percent?
Show worked solution
No. Zero is a valid and consequential measurement, while null may mean unavailable under the chosen protocol. Preserve that distinction and display an unavailable state. If the native protocol permits other meanings, define and test them explicitly before mapping values.
2. What does a passing batteryLabel fake test establish?
Show worked solution
It establishes the application’s mapping from a supplied reader result to a label. It does not establish the native method name, codec type, plugin registration, permission state, or hardware behavior. Those require separate adapter and target integration evidence.
Key takeaways
- A plugin crosses more than a Dart method boundary
- Diagnose the boundary that actually failed