simple_native 0.0.16
simple_native: ^0.0.16 copied to clipboard
A comprehensive Flutter plugin for accessing native device features including Camera (Photos, QR & Barcode), Location, Device Information, and Biometric Authentication.
example/lib/main.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:simple_native/simple_native.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
String _locationInfo = 'Unknown';
String _deviceInfo = 'Unknown';
String _biometricInfo = 'Unknown';
final _simpleNativePlugin = SimpleNative();
@override
void initState() {
super.initState();
initPlatformState();
_getLocation();
}
Future<void> _getLocation() async {
String locationInfo;
try {
final location = await _simpleNativePlugin.getCurrentLocation();
locationInfo = location?.toString() ?? 'Location not available';
} on PlatformException {
locationInfo = 'Failed to get location.';
}
if (!mounted) return;
setState(() {
_locationInfo = locationInfo;
});
}
Future<void> _getDeviceInfo() async {
String deviceInfo;
try {
final info = await _simpleNativePlugin.getDeviceInfo();
deviceInfo = info?.toString() ?? 'Device info not available';
} on PlatformException {
deviceInfo = 'Failed to get device info.';
}
if (!mounted) return;
setState(() {
_deviceInfo = deviceInfo;
});
}
Future<void> _testBiometrics() async {
String info = '';
try {
final type = await _simpleNativePlugin.getAvailableBiometric();
info += 'Type: $type\n';
if (type != SimpleBiometricType.notSupported) {
final result = await _simpleNativePlugin.authenticate(
localizedReason: "Please authenticate to test",
);
info += 'Result: $result';
}
} on PlatformException catch (e) {
info = 'Failed to test biometrics: ${e.message}';
}
if (!mounted) return;
setState(() {
_biometricInfo = info;
});
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _simpleNativePlugin.getPlatformVersion() ??
'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Plugin example app')),
body: Builder(
builder: (context) {
return Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Running on: $_platformVersion\n'),
Text('Location: $_locationInfo\n'),
Text('Device info: $_deviceInfo\n'),
Text(
'Biometric Info:\n$_biometricInfo\n',
textAlign: TextAlign.center,
),
ElevatedButton(
onPressed: _getLocation,
child: const Text('Get Location'),
),
ElevatedButton(
onPressed: _getDeviceInfo,
child: const Text('Get Device Info'),
),
ElevatedButton(
onPressed: _testBiometrics,
child: const Text('Test Biometrics'),
),
ElevatedButton(
onPressed: () =>
_simpleNativePlugin.openBiometricSettings(),
child: const Text('Open Biometric Settings'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CameraPage(),
),
);
},
child: const Text('Open Camera'),
),
],
),
),
);
},
),
),
);
}
}
class CameraPage extends StatefulWidget {
const CameraPage({super.key});
@override
State<CameraPage> createState() => _CameraPageState();
}
class _CameraPageState extends State<CameraPage> {
SimpleCameraController? _controller;
final ValueNotifier<Uint8List?> _vnPreviewImage = ValueNotifier(null);
List<SimpleBarcode> _detectedBarcodes = [];
bool _freezeOnResult = false;
bool _isFrozen = false;
String? _frozenImagePath;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Camera View')),
body: Stack(
children: [
SimpleCameraView(
key: ValueKey('camera_qr_$_freezeOnResult'),
cameraType: SimpleCameraType.qr,
cameraPosition: SimpleCameraPosition.back,
freezeOnResult: _freezeOnResult,
onCameraCreated: (controller) {
_controller = controller;
},
onResult: (result) {
if (_isFrozen) return;
setState(() {
_detectedBarcodes = result.barcodes;
if (_freezeOnResult && result.barcodes.isNotEmpty) {
_isFrozen = true;
_frozenImagePath = result.imagePath;
}
});
},
),
if (_isFrozen && _frozenImagePath != null)
Positioned.fill(
child: Image.file(
File(_frozenImagePath!),
fit: BoxFit.cover,
),
),
// Bounding Box Overlays
for (final barcode in _detectedBarcodes)
if (barcode.boundingBox != null)
Positioned(
left: barcode.boundingBox!.x,
top: barcode.boundingBox!.y,
width: barcode.boundingBox!.width,
height: barcode.boundingBox!.height,
child: GestureDetector(
onTap: () async {
if (_isFrozen) {
await _controller?.resumeScanning();
if (_frozenImagePath != null) {
try {
final file = File(_frozenImagePath!);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
// ignore
}
}
setState(() {
_isFrozen = false;
_detectedBarcodes = [];
_frozenImagePath = null;
});
}
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.greenAccent, width: 3),
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(
color: Color(0x4D69F0AE),
blurRadius: 6,
spreadRadius: 2,
),
],
),
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: -24,
left: -3,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: BorderRadius.circular(4),
),
child: Text(
barcode.value ?? '',
style: const TextStyle(
color: Colors.black,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
Positioned(
bottom: 30 + MediaQuery.viewPaddingOf(context).bottom,
left: 0,
right: 0,
child: Center(
child: FloatingActionButton(
onPressed: () async {
final path = await _controller?.takePicture();
if (path != null && context.mounted) {
showDialog(
context: context,
builder: (context) =>
Dialog.fullscreen(child: Image.file(File(path))),
);
} else {
print('Không chụp được ảnh');
}
},
child: const Icon(Icons.camera),
),
),
),
Positioned(
top: 20,
right: 20,
child: Column(
children: [
FloatingActionButton(
heroTag: "switch_camera",
mini: true,
onPressed: () {
_controller?.switchCamera();
},
child: const Icon(Icons.switch_camera),
),
const SizedBox(height: 10),
ValueListenableBuilder(
valueListenable: _vnPreviewImage,
builder: (context, value, child) => value == null
? const SizedBox.shrink()
: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
),
child: Image.memory(value, width: 100),
),
),
],
),
),
// Top settings panel
Positioned(
top: 20,
left: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(153),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Dừng hình:',
style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
Switch(
value: _freezeOnResult,
activeThumbColor: Colors.greenAccent,
activeTrackColor: Colors.greenAccent.withAlpha(128),
onChanged: (val) {
setState(() {
_freezeOnResult = val;
if (!val) {
_isFrozen = false;
_controller?.resumeScanning();
if (_frozenImagePath != null) {
try {
final file = File(_frozenImagePath!);
if (file.existsSync()) {
file.deleteSync();
}
} catch (e) {
// ignore
}
}
_frozenImagePath = null;
}
});
},
),
],
),
),
),
// Status banner when frozen
if (_isFrozen)
Positioned(
bottom: 110 + MediaQuery.viewPaddingOf(context).bottom,
left: 20,
right: 20,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.black.withAlpha(204),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.greenAccent.withAlpha(128), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(102),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_outline, color: Colors.greenAccent, size: 20),
SizedBox(width: 8),
Text(
'Đã dừng hình. Chạm vào QR để tiếp tục.',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
],
),
);
}
}