DEV Community

Cover image for Adapt your Flutter app to Android 13
David Serrano
David Serrano

Posted on • Originally published at davidserrano.io

Adapt your Flutter app to Android 13

Android 13 is the latest major Android version released this year and it comes with some behavioral changes that will force us to make some adaptations to our app to ensure it works properly.

We already have at our disposal this informative document about these changes and links of interest to understand in more detail what they entail. However, in this article I want to take a more pragmatic approach through a simple example.

What I am going to present to you is an application running on Android 12. We are going to see what it does and what happens when we run it on Android 13. Once we have seen what happens, we will make the appropriate adjustments so that it works correctly on the latest version of Android.

📽 Video version available on YouTube and Odysee

Note: In this article I am going to focus basically on the changes related to reading files on disk and the new notifications permission, since they are functionalities that the vast majority of applications have. You can see the rest of the changes in the official documentation.

The example app

Our app will consist of the following:

  • A floating button that when pressed will ask us for permission to read files
  • Once the permission is granted, we can choose a photo and it will be painted on the screen
  • Additionally, a notification will be displayed indicating that the photo has been uploaded correctly

App preview

We will use file_picker to be able to select files and permission_handler to request the system permissions. To show notifications we will use flutter_local_notifications. The pubspec.yaml file looks like this:

name: adapt_android_13
description: Adapt Android 13
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=2.18.2 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  file_picker: ^5.2.0+1
  permission_handler: ^10.2.0
  flutter_local_notifications: ^12.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
Enter fullscreen mode Exit fullscreen mode

Let's quickly see the code that compose this application:

FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
    AndroidInitializationSettings('ic_notification');

const InitializationSettings initializationSettings = InitializationSettings(
  android: initializationSettingsAndroid,
);

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await flutterLocalNotificationsPlugin.initialize(initializationSettings,
      onDidReceiveNotificationResponse: (NotificationResponse response) {});

  runApp(const App());
}

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Adapt Android 13',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MainScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We first initialize the notifications using the flutter_local_notifications plugin. We then launch our app encapsulated in the App widget.

class _MainScreenState extends State<MainScreen> {
  String? _filepath;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Adapt Android 13'),
      ),
      body: _filepath != null
          ? Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  SizedBox(
                    width: 250.0,
                    height: 250.0,
                    child: Image.file(File(_filepath!)),
                  ),
                ],
              ),
            )
          : Container(),
      floatingActionButton: FloatingActionButton(
        onPressed: _pickFile,
        tooltip: 'Pick image',
        child: const Icon(Icons.add),
      ),
    );
  }

  Future<void> _pickFile() async {
    final result = await Permission.storage.request();

    if (result == PermissionStatus.granted) {
      final FilePickerResult? result =
          await FilePicker.platform.pickFiles(type: FileType.image);

      final path = result?.files.single.path;

      if (path != null) {
        _postNotification();
        setState(() {
          _filepath = path;
        });
      }
    }
  }

  Future<void> _postNotification() async {
    const AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
      'default_notification_channel_id',
      'Default',
      importance: Importance.max,
      priority: Priority.max,
    );
    const NotificationDetails notificationDetails =
        NotificationDetails(android: androidNotificationDetails);
    await flutterLocalNotificationsPlugin.show(
        0, 'Image successfully loaded', '', notificationDetails);
  }
}
Enter fullscreen mode Exit fullscreen mode

In our main screen we have a state variable _filepath, which will store the path to the chosen file. When the user clicks the button, the _pickFile() method will be called, which will take care of requesting permission to read files and will allow us to choose a file of type image.

Once selected, the status is updated and the picture is displayed on the screen, and the _postNotification() method is called to publish the informative notification.

This code snippet works correctly in versions before Android 13, let's see what happens if we run it in the new version of Android:

Android 13 changes

If we run this app on a device with Android 13 we will see that it works more or less the same as before, except that now, once the image is selected, it asks us for permission to show notifications.

Notification permission

Although the app continues to work, we have the problem that it is the system that is choosing the moment to ask for permission to show notifications, when ideally we should be the ones who decide when it is requested and if any explanatory text must be shown before.

The problem is aggravated if we target Android 13:

defaultConfig {
    applicationId "com.example.adaptandroid13"
    minSdkVersion 22
    targetSdkVersion 33 //Target Android 13
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
    multiDexEnabled true
}
Enter fullscreen mode Exit fullscreen mode

Now when pressing the floating button we are not even asked for permission to read files. Why is this happening?

Android 13 incorporates a granular permission system when it comes to reading files. While before it was enough to have the READ_EXTERNAL_STORAGE permission granted to read any file from external storage, now we will have to specifically define what type of files we intend to read.

In this app we want to read images, so we will ask for the READ_MEDIA_IMAGES permission. If we wanted to read videos we would ask for READ_MEDIA_VIDEO and if we wanted audio files we would have READ_MEDIA_AUDIO.

Let's make the following adjustments to the AndroidManifest file to resolve this issue:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
Enter fullscreen mode Exit fullscreen mode

Through this change we are indicating to the system that for Android 12 or lower we continue to depend on READ_EXTERNAL_STORAGE, while from Android 13 we will use the new granular permissions system.

Other change that Android 13 brings is the need to have explicit permission from the user to display notifications, as has been the case in iOS for a long time.

Let's declare that permission in AndroidManifest:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
Enter fullscreen mode Exit fullscreen mode

Now we are going to modify _pickFile() so that in versions before Android 13 it asks for the storage permission and in later versions the new notification and image permissions, to do this, first install device_info_plus to be able to know what version of Android is running on the device:

flutter pub add device_info_plus

Now modify _pickFile() as follows:

Future<void> _pickFile() async {
  final androidInfo = await DeviceInfoPlugin().androidInfo;
  late final Map<Permission, PermissionStatus> statusess;

  if (androidInfo.version.sdkInt <= 32) {
    statusess = await [
      Permission.storage,
    ].request();
  } else {
    statusess = await [Permission.photos, Permission.notification].request();
  }

  var allAccepted = true;
  statusess.forEach((permission, status) {
    if (status != PermissionStatus.granted) {
      allAccepted = false;
    }
  });

  if (allAccepted) {
    final FilePickerResult? result =
        await FilePicker.platform.pickFiles(type: FileType.image);

    final path = result?.files.single.path;

    if (path != null) {
      _postNotification();
      setState(() {
        _filepath = path;
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And with this last change we can now choose an image, it is displayed on the screen and the informative notification is also shown.

Conclusion

In this article I have made a simple review of two of the behavioral changes that Android 13 incorporates: the new granular permission system for reading files and the new permission to be able to show notifications.

In addition to these two, I recommend that you read the official documentation to see these changes in more detail as well as to see the rest of the changes that this version of Android incorporates since they may affect the operation of your application.

You can check the source code of the projecte used in this article here.

Thanks for reading this far, happy coding!

Top comments (3)

Collapse
 
ayevbeosa profile image
Ayevbeosa Iyamu

I am curious, does this code still run well on iOS devices. The reason for this is;

final androidInfo = await DeviceInfoPlugin().androidInfo;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
svprdga profile image
David Serrano

No!! You should avoid calling that method in a non-android target. I have left it this way for simplicity's sake, but in a real scenario you should first check the underlying platform.

Collapse
 
ayevbeosa profile image
Ayevbeosa Iyamu

Okay that makes sense. Thank you