Deep dive intro Permissions in Flutter

Yevhenii Smirnov
Yevhenii Smirnov
Dec 9 2022
Posted in Engineering & Technology

Basic permissions requests in Android and iOS

Deep dive intro Permissions in Flutter

It's been a while since our blog post about permissions in Android, more specifically permissions for notifications, which you could adapt it to different scenarios. If you missed that, take a look here, it may be interesting because some important changes were introduced Android 13.

With your permission, let's get started!

I'm going to focus on the technical part and the recommended flow and best practices as I did mention in the blog post linked above. In the following examples, I'll be using the permission_handler flutter package, probably the best package to go with when we need to handle permissions in Flutter. For a basic permissions setup on both platforms (Android and iOS), you can visit the package documentation which contains helpful info.

Let's prepare our environment

Usually, permission is requested during some sort of onboarding, when the app needs that feature, or from a settings view. Let's take the settings view as an example, but you could easily adapt it to other situations.

Assuming you have a toggle button (Switch) for Notifications and Location, this is how your methods could look like.

For notifications:

  void _updateNotificationsStatus(bool checked) async {
    setState(() {
      _hasNotificationsEnabled = checked;
    });

    if (!checked) {
      try {
        await NotificarePush.disableRemoteNotifications();
      } catch (error) {
        // Handle the error
      }

      return;
    }

    if (await _ensureNotificationsPermission()) {
      try {
        await NotificarePush.enableRemoteNotifications();
      } catch (error) {
        // Handle the error
      }

      return;
    }

    setState(() {
      _hasNotificationsEnabled = false;
    });
  }

For location:

  void _updateLocationStatus(bool checked) async {
    setState(() {
      _hasLocationEnabled = checked;
    });

    if (!checked) {
      try {
        await NotificareGeo.disableLocationUpdates();
      } catch (error) {
        // Handle the error
      }

      return;
    }

    if (await _ensureForegroundLocationPermission()) {
      try {
        await NotificareGeo.enableLocationUpdates();
      } catch (error) {
        // Handle the error
      }

      if (await _ensureBackgroundLocationPermission()) {
        try {
          await NotificareGeo.enableLocationUpdates();
        } catch (error) {
          // Handle the error
        }
      }

      return;
    }

    setState(() {
      _hasLocationEnabled = false;
    });
  }

Although the code above doesn't need too much explanation, but let's ensure we see the same thing:

  • Both methods are invoked by the toggle button.
  • When we switch it OFF, we turn off notifications or location updates.
  • When we switch it ON, we call our permission request method and enable notifications or location updates.
  • If no permissions were granted, we make sure the switch is OFF.

It's time to get the Permissions

Let's dive in, and see some code examples, and then we will go through the details.

For notifications permission:

  Future<bool> _ensureNotificationsPermission() async {
    const permission = Permission.notification;
    if (await permission.isGranted) return true;

    if (await permission.isPermanentlyDenied) {
      await _handleOpenSettings(permission);

      return false;
    }

    if (await permission.shouldShowRequestRationale) {
      await showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("Your title here"),
            content: Text("Your rationale message here"),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text("Ok"),
              ),
            ],
          );
        },
      );

      return await permission.request().isGranted;
    }

    final granted = await permission.request().isGranted;

    return granted;
  }

Foreground location updates:

  Future<bool> _ensureForegroundLocationPermission() async {
    const permission = Permission.locationWhenInUse;
    if (await permission.isGranted) return true;

    if (await permission.isPermanentlyDenied) {
      await _handleOpenSettings(permission);

      return false;
    }

    if (await permission.shouldShowRequestRationale) {
      await showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("Your title here"),
            content: Text("Your rationale message here"),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text("Ok"),
              ),
            ],
          );
        },
      );

      return await permission.request().isGranted;
    }

    final granted = await permission.request().isGranted;

    return granted;
  }

And background location updates:

  Future<bool> _ensureBackgroundLocationPermission() async {
    const permission = Permission.locationAlways;
    if (await permission.isGranted) return true;

    if (await permission.isPermanentlyDenied) {

      return false;
    }

    if (await permission.shouldShowRequestRationale) {
      await showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text("Your title here"),
            content: Text("Your rationale message here"),
            actions: [
              TextButton(
                style: TextButton.styleFrom(
                  foregroundColor: Colors.black,
                ),
                onPressed: () => Navigator.of(context).pop(),
                child: Text("Ok"),
              ),
            ],
          );
        },
      );

      return await permission.request().isGranted;
    }

    final granted = await permission.request().isGranted;

    return granted;
  }

A step by step description of the methods above:

  • Check if the permission is already granted, return True if it is.
  • Check if the permission is permanently denied, return False if it is.
  • Check shouldShowRationale (Android only), in case it's True, show rationale and request the permission. Return True or False, depending on the user's response.
  • If all 3 conditionals above are skipped, request permission, if possible.
  • Return the request result (True or False).

Seems quite complete, but a few details are missing. If you read the previous blog post, you know how tricky a permanently denied permission is in Android. The conditional above with permission.isPermanentlyDenied will work only on iOS, so we need to implement our way to handle permanently denied permission in Android too.

Let's add the code below to our permissions request, just above the final return, at the end of each method, for notifications and foreground location:

    if (Platform.isAndroid) {
      if (!granted && !await permission.shouldShowRequestRationale) {
        await _handleOpenSettings(permission);

        return false;
      }
    }

About the method above:

  • It should only run for Android users, so we make sure that's the case.
  • We are also checking different scenarios:
    • In the case granted is true, it is obvious we are not facing the permanently denied situation, and the conditional block is skipped.
    • If granted is false and shouldShowRationale is true, we are certain it's the first time permission is requested, so it is not permanently denied.
    • In case the granted is false and shouldRequestRationale is false too, then we can assume it is permanently denied because the request forwarded from shouldShowRationale is done above and will not reach this part of the code.

Handle permanently denied Permission

At this point, we can properly handle permissions requests and identify whether the permission is permanently denied. Now it is time to handle that permanently denied situation.

First, let's shortly explain to the user why we need those permissions instead of opening the device's settings straight away. This could be done like our approach shown below, where we use an explanation text, and offer the option to go to the device's settings or close the AlertDialog.

  Future<void> _handleOpenSettings(Permission permission) async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Your title"),
          content: permission == Permission.notification
              ? const Text("Your rationale message for notifications permission")
              : const Text("Your rationale message for location permission"),
          actions: <Widget>[
            TextButton(
              child: const Text("Settings"),
              onPressed: () {
                Navigator.of(context).pop();
                openAppSettings();

                // Declare _hasOpenedNotificationsSettings = false && _hasOpenedLocationSettings = false
                // at the top of your class

                if (permission == Permission.notification) {
                  _hasOpenedNotificationsSettings = true;
                }
                if (permission == Permission.locationWhenInUse) {
                  _hasOpenedLocationSettings = true;
                }
              },
            ),
            TextButton(
              child: const Text("Cancel"),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
  }

Hopefully, the user will choose to open the settings, and we will keep track of the permission that should be prompted. In the next example, I will use didChangeAppLifecycleState and check if the state is resumed, and if it is, we should expect to see some changes in permissions and act accordingly.

So, this would be our approach:

  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _checkSettingsChanges();
    }
  }

And we would implement the following method:

  void _checkSettingsChanges() async {
    if (_hasOpenedNotificationsSettings) {
      _hasOpenedLocationSettings = false;
      if (!await Permission.notification.isGranted) {
        return;
      }

      _updateNotificationsSettings(true);
    }

    if (_hasOpenedLocationSettings) {
      _hasOpenedLocationSettings = false;
      if (!await Permission.locationWhenInUse.isGranted) {
        return;
      }

      _updateLocationSettings(true);
    }
  }

In the example above, every time the settings view is resumed, we'll check if the user is coming back from device's settings. This is a simple approach, that will work in both Android and iOS, without having to go to the native part of each platform. If the user is coming back from the device's settings, we check if the permission was granted and act accordingly.

Conclusion

Now we are ready to handle permissions requests and all possible scenarios. You could also forward the user to device's settings from the location background permission, when it's denied. It all depends on your application and the approach you choose.

As always, we hope this post was helpful and feel free to share with us your ideas or feedback via our Support Channel.

Keep up-to-date with the latest news