Let’s build an app with biometric lock. This app will require biometric authentication to open or resume the app. When locked the app screen will be blurred and we can also toggle the lock.
Let’s start by creating a minimal app with flutter create -e applock
and then create a UI for locked state like this.
import 'dart:ui';
import 'package:flutter/material.dart';
class AppLock extends StatefulWidget {
/// Hides [child] when locked. Use inside MaterialApp builder.
const AppLock({
super.key,
required this.requestUnlock,
this.enabled = true,
required this.child,
});
// Widget to lock.
final Widget child;
/// Unlocks if true is returned.
final Future<bool> Function() requestUnlock;
/// Shows locked screen when true.
final bool enabled;
@override
State<AppLock> createState() => _AppLockState();
}
class _AppLockState extends State<AppLock> {
late bool locked;
@override
void initState() {
super.initState();
// If app lock is enabled, initial state for locked should be true.
locked = widget.enabled;
}
Future<void> verifyAndUnlock() async {
// Request unlock.
final verified = await widget.requestUnlock();
if (verified) {
// If requestUnlock returns true then update state to unlocked.
setState(() {
locked = false;
});
}
}
@override
Widget build(BuildContext context) {
return widget.enabled
? Material(
child: Stack(
children: [
widget.child,
if (locked) ...[
Positioned.fill(
child: AbsorbPointer(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 20.0,
sigmaY: 20.0,
),
child: const Center(),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height / 5,
child: Center(
child: Column(
children: [
Text(
'App is locked. Please authenticate to continue.',
style: Theme.of(context).textTheme.bodyLarge,
),
IconButton(
onPressed: verifyAndUnlock,
icon: const Icon(
Icons.fingerprint,
size: 50,
),
),
],
),
),
),
],
],
),
)
: widget.child;
}
}
Now we need this UI to overlay entire app when app is in locked state. For that we can use builder
from MaterialApp
which will insert widget above navigator. For now requestUnlock
always returns true. We will update this later.
return MaterialApp(
builder: (context, child) {
return AppLock(
requestUnlock: () async {
return true;
},
child: child!,
);
},
home: const Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
For biometric authentication we will be using local_auth. Make sure you setup local_auth
plugin in your app because this article does not include setup for local_auth
. Use local_auth
to authenticate with biometric inside requestUnlock
.
requestUnlock: () async {
return LocalAuthentication().authenticate(
localizedReason: 'Please authenticate to unlock.');
},
Now the app asks for biometric authentication on startup but still app is not locked when minimizing the app. To achieve this we will use WidgetsBindingObserver. Lets go to app_lock.dart
again and use WidgetsBindingObserver
mixin.
We also need to add WidgetsBinding
observer for this to work.
@override
void initState() {
super.initState();
// If app lock is enabled, initial state for locked should be true.
locked = widget.enabled;
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Override didChangeAppLifecycleState
to lock and unlock when paused and resumed respectively.
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (widget.enabled && state == AppLifecycleState.paused) {
setState(() {
locked = true;
});
}
if (state == AppLifecycleState.resumed) {
if (locked) {
await verifyAndUnlock();
}
}
}
After this our app can lock when minimized and ask for biometric authentication when resumed. But there is one downside. When we minimize the app the app can’t do anything so app is not actually updating the UI to locked state when we minimize. When we try to resume the app then only didChangeAppLifecycleState
is called with AppLifecycleState.paused
and hence updates the UI to locked state. Between this time you can view the screen just before the app is minimized. You can avoid this by disabling screenshot in recent app menu in Android and iOS which this article does not cover.
We are almost at the end. Now create a toggle button to enable or disable the app lock within our app. To update the state of toggle button and AppLock we also need MainApp
to be StatefulWidget
.
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
bool lockEnabled = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
return AppLock(
enabled: lockEnabled,
requestUnlock: () async {
return LocalAuthentication().authenticate(
localizedReason: 'Please authenticate to unlock.');
},
child: child!,
);
},
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlutterLogo(
size: 100,
),
const SizedBox(height: 20),
Switch(
value: lockEnabled,
onChanged: (value) {
setState(() {
lockEnabled = value;
});
},
),
],
),
),
),
);
}
}
Here is the complete code. https://github.com/2shrestha22/flutter_examples/tree/main/examples/applock