App Lock with Flutter

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

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.