Many Android apps require what Android deems a dangerous permission. For example using the camera, adding an event to the user calendar, or reading the user contacts. Previously, Android used to ask for those permissions at install time, but since Marshmallow, it does it at runtime. So, how do you implement the Android runtime permission flow in Flutter?
A search of Flutter plugins currently shows no results, so I had to figure it out myself for my app Preset SMSs.
In this code tutorial, we will create an app that shows a list of all contacts with a mobile phone number. The app displays a loading screen while it obtains the contacts and an error Snackbar when the app has no permission. The Android code itself is based on the official Requesting Permissions at Run Time guide. For communicating between Flutter and Android, we will use Method Channels.
Setting up the app
The app has one screen, which has a list, as well as a CircularProgressIndicator. It handles errors with snackbars. We will use a basic MVP structure for this screen.
To follow the code tutorial, create a new app as follows.
1 |
flutter create runtimepermissionexample |
If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.
Firstly, we create a Material app in main.dart, which will launch the HomePage widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import 'package:flutter/material.dart'; import 'home_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Runtime Permission Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new HomePage(), ); } } |
Secondly, we create home_page.dart. This displays a list or an in progress indicator, depending on its state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import 'package:flutter/material.dart'; import 'contact.dart'; class HomePage extends StatefulWidget { HomePage({Key key}) : super(key: key); @override _HomePageState createState() => new _HomePageState(); } class _HomePageState extends State<HomePage> { List<Contact> _contacts; bool _loadingInProgress; @override void initState() { super.initState(); _loadingInProgress = true; } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Runtime Permission example"), ), body: _buildBody(), ); } Widget _buildBody() { if (_loadingInProgress) { return new Center( child: new CircularProgressIndicator(), ); } else { return new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _contacts.map((Contact contact) { return _buildListRow(contact); }).toList(), ); } } Widget _buildListRow(Contact contact) { return new ListTile( title: new Text(contact.displayName), subtitle: new Text(contact.mobileNumber), ); } } |
Thirdly, we define a simple Contact, with a display name and a mobile phone number, in contact.dart.
1 2 3 4 5 6 7 |
class Contact { final String displayName; final String mobileNumber; const Contact(this.displayName, this.mobileNumber); } |
Setting up the MVP structure
I like to use “contracts” when using MVP. Contracts are interfaces (in Java) or abstract classes (in Dart) that define the methods for each component of the feature, ie View, Model, and Presenter.
Note: You do not need to set up contracts for applying MVP, but I find it very helpful to have them. I define them all in one file, and this makes it easy to understand what a feature does. When I worked as an Android DPE at Google, I worked on the first release of the Android Architecture Blueprints project and we decided to use contracts. I have used contracts on all my professional projects since, and I haven’t looked back: this really helps make a complex app maintainable!
So let’s set up our contract for this feature, by creating home_contract.dart.
1 2 3 4 5 6 7 8 9 10 11 |
abstract class View { } abstract class Model { } abstract class Presenter { } |
The only user action per se is the user starting the app to show the screen, ie view displayed. This is async, because it will update the view based on async data responses from the Model.
1 2 3 4 5 6 7 8 9 |
import 'dart:async'; [...] abstract class Presenter { Future viewDisplayed(); } |
Getting the contacts is 2 separate data actions: check if we can get contacts, and get contacts. The former takes care of requesting permission if the app hasn’t got it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[...] import 'contact.dart'; [...] abstract class Model { Future<PermissionState> canGetContacts(); Future<List<Contact>> getContactsWithMobilePhoneNumber(); } enum PermissionState { GRANTED, DENIED, SHOW_RATIONALE // Refer https://developer.android.com/training/permissions/requesting.html#explain } |
Finally, the view has 4 possible states: loading in progress, contacts list, display error message when permission is denied, and display permission rationale.
1 2 3 4 5 6 7 8 9 10 11 |
abstract class View { void showErrorMessage(); void showContactsWithPhoneNumber(List<Contact> contacts); void showLoadingContactsInProgress(); void showPermissionRationale(); } |
Now, we create Model and Presenter classes that implement the abstract classes. The View abstract class is implemented in HomePage.
Firstly, let’s start with home_presenter.dart. The Presenter has a reference to the View and the Model, as it acts as a “coordinator” between View and Model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import 'home_contract.dart'; import 'dart:async'; import 'contact.dart'; class HomePresenter implements Presenter { Model _model; View _view; HomePresenter(this._model, this._view); @override Future viewDisplayed() async { _view.showLoadingContactsInProgress(); PermissionState permissionState = await _model.canGetContacts(); switch (permissionState) { case PermissionState.GRANTED: List<Contact> contacts = await _model.getContactsWithMobilePhoneNumber(); _view.showContactsWithPhoneNumber(contacts); break; case PermissionState.DENIED: await new Future.delayed(new Duration(seconds : 1)); _view.showErrorMessage(); break; case PermissionState.SHOW_RATIONALE: _view.showPermissionRationale(); break; } return null; } } |
Note: in case of permission denied, we add a 1 second wait. This is because the Android SDK somehow still holds the UI (due to the permission dialog). If we don’t add this delay, the error snackbar isn’t shown. You may want to play with the duration value, but I found that 1 second worked on all the devices I tested it on. UPDATE: There is apparently a way around this, refer to comment by Christian. I’ll update the tutorial accordingly soon.
Secondly, let’s set up home_model.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import 'dart:async'; import 'home_contract.dart'; import 'contact.dart'; class HomeModel implements Model { @override Future<PermissionState> canGetContacts() async { // TODO return new Future.value(PermissionState.DENIED); } @override Future<List<Contact>> getContactsWithMobilePhoneNumber() async { // TODO return new Future.value(null); } } |
And, lastly, HomePage itself will implement the View, and will create the Presenter when initialised.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
[...] import 'home_contract.dart'; import 'home_model.dart'; import 'home_presenter.dart'; import 'dart:async'; [...] class _HomePageState extends State<HomePage> implements View { List<Contact> _contacts; bool _loadingInProgress; HomePresenter _presenter; BuildContext _scaffoldContext; @override void initState() { super.initState(); _loadingInProgress = true; _presenter = new HomePresenter(new HomeModel(), this); _presenter.viewDisplayed(); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Runtime Permission example"), ), body: new Builder( builder: (BuildContext context) { _scaffoldContext = context; return _buildBody(); }, ), ); } [...] @override void showErrorMessage() { setState(() { _loadingInProgress = false; _contacts = new List<Contact>(); }); Scaffold.of(_scaffoldContext).showSnackBar(new SnackBar( content: new Text('No permission'), duration: new Duration(seconds: 5), )); } @override void showLoadingContactsInProgress() { setState(() { _loadingInProgress = true; }); } @override void showContactsWithPhoneNumber(List<Contact> contacts) { setState(() { _loadingInProgress = false; _contacts = contacts; }); } @override Future showPermissionRationale() { return showDialog<Null>( context: context, barrierDismissible: false, // user must tap button! child: new AlertDialog( title: new Text('Contacts Permission'), content: new SingleChildScrollView( child: new ListBody( children: <Widget>[ new Text('We need this permission because ...'), ], ), ), actions: <Widget>[ new FlatButton( child: new Text('OK'), onPressed: () { Navigator.of(context).pop(); _presenter.viewDisplayed(); }, ), ], ), ); } } |
Checking and requesting permission
Firstly, we add the permission to the Android app manifest.
1 2 3 4 |
[...] <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_CONTACTS"/> [...] |
Secondly, to check and request a permission, we need to use the Android SDK. Therefore, we use a MethodChannel to communicate between Flutter and the Android SDK.
On the Flutter side, it is pretty simple. We set up the channel in home_model.dart, as per below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[...] import 'package:flutter/services.dart'; class HomeModel implements Model { static const _methodChannel = const MethodChannel('runtimepermissiontutorial/contacts'); @override Future<PermissionState> canGetContacts() async { try { final int result = await _methodChannel.invokeMethod('hasPermission'); return new Future.value(PermissionState.values.elementAt(result)); } on PlatformException catch (e) { print('Exception ' + e.toString()); } return new Future.value(PermissionState.DENIED); } [...] } |
Note: for the permission status, the channel returns an int, which corresponds to the index of the PermissionState value.
On the Android side, we need to set it up in MainActivity.java as below. When requesting a permission, the process is async, relying on a system callback in the Activity. So we define our own Callback, to be able to send the result back to the Channel. The permission code itself is copied from the official Android documentation – check it out if you need more explanations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
[...] import android.Manifest; import android.content.pm.PackageManager; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; [...] public class MainActivity extends FlutterActivity { private static final String CHANNEL_CONTACTS = "runtimepermissiontutorial/contacts"; private static final int GET_CONTACTS_PERMISSION_REQUEST_ID = 2345; private PermissionCallback getContactsPermissionCallback; private boolean rationaleJustShown = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); new MethodChannel(getFlutterView(), CHANNEL_CONTACTS).setMethodCallHandler( new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall call, final MethodChannel.Result result) { getContactsPermissionCallback = new PermissionCallback() { @Override public void granted() { rationaleJustShown = false; result.success(0); } @Override public void denied() { rationaleJustShown = false; result.success(1); } @Override public void showRationale() { rationaleJustShown = true; result.success(2); } }; if (call.method.equals("hasPermission")) { hasPermission(); } } }); } private void hasPermission() { if (rationaleJustShown) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, GET_CONTACTS_PERMISSION_REQUEST_ID); } else { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) { getContactsPermissionCallback.showRationale(); } else { // No explanation needed, we can request the permission. ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, GET_CONTACTS_PERMISSION_REQUEST_ID); } } else { getContactsPermissionCallback.granted(); } } } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case GET_CONTACTS_PERMISSION_REQUEST_ID: // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { getContactsPermissionCallback.granted(); } else { getContactsPermissionCallback.denied(); } return; } } public interface PermissionCallback { void granted(); void denied(); void showRationale(); } } |
Note: the Android SDK has no method to indicate that the rationale has been shown. When the user has just seen the rationale, the app is expected to request the permission directly, without checking if we should show the rationale (as this will return true and we will be stuck in a loop). To handle this, we are using a boolean rationaleJustShown. We handle this in the Android code and not the Flutter code because it is an Android SDK detail, and the implementation on iOS may well be different.
Lastly, don’t forget to add the support library to build.gradle.
1 2 3 4 5 6 7 |
dependencies { compile 'com.android.support:support-v4:25.0.0' androidTestCompile 'com.android.support:support-annotations:25.4.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' } |
Getting contacts with a mobile phone number
The process to get the contacts is similar. We need to use the Android SDK to query the contacts, so we set up a Method Channel.
In Flutter, we do this in home_model.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@override Future<List<Contact>> getContactsWithMobilePhoneNumber() async { List<Contact> contacts = new List<Contact>(); try { final List<Map<String,Object>> result = await _methodChannel.invokeMethod('getContacts'); if (result != null) { for (var contact in result) { contacts.add(new Contact(contact['NAME'], contact['MOBILE'])); } } } on PlatformException catch (e) { print('Exception ' + e.toString()); } return new Future.value(contacts); } |
In Android, we set it up in MainActivity.java. To avoid blocking the UI thread, we use an AsyncTask to get the contacts; to send the results back to the channel, we use a callback setup similar to permission. If you didn’t use an AsyncTask to query the contacts, the circular progress bar would freeze.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
[...] import java.util.HashMap; import java.util.ArrayList; import android.annotation.TargetApi; import android.os.Build; import java.util.List; import android.os.AsyncTask; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import android.util.Log; [...] public class MainActivity extends FlutterActivity { [...] private ContactsCallback contactsCallback; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); new MethodChannel(getFlutterView(), CHANNEL_CONTACTS).setMethodCallHandler( new MethodChannel.MethodCallHandler() { @TargetApi(Build.VERSION_CODES.CUPCAKE) @Override public void onMethodCall(MethodCall call, final MethodChannel.Result result) { [...] contactsCallback = new ContactsCallback() { @Override public void onSuccess(List<HashMap<String, String>> contacts) { result.success(contacts); } @Override public void onError() { result.success(null); } }; if (call.method.equals("hasPermission")) { hasPermission(); } else if (call.method.equals("getContacts")) { new GetContactsTask().execute(); } } }); } [...] @TargetApi(Build.VERSION_CODES.CUPCAKE) private class GetContactsTask extends AsyncTask<Void, Void, ArrayList<HashMap<String,String>>> { @TargetApi(Build.VERSION_CODES.ECLAIR) protected ArrayList<HashMap<String,String>> doInBackground(Void... urls) { try { ContentResolver cr = MainActivity.this.getContentResolver(); Uri uri = ContactsContract.Contacts.CONTENT_URI; String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}; String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = '1'"; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; ArrayList<HashMap<String,String>> contacts = new ArrayList<HashMap<String,String>>(); Cursor users = cr.query(uri, projection, selection, null, sortOrder); while (users != null && users.moveToNext()) { int contactId = users.getInt(users.getColumnIndex(ContactsContract.Contacts._ID)); String displayName = users.getString(users.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); String mobileNumber = null; Cursor contactNumbers = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null, null); while (contactNumbers != null && contactNumbers.moveToNext()) { String number = contactNumbers.getString(contactNumbers.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); int type = contactNumbers.getInt(contactNumbers.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)); switch (type) { case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: mobileNumber = number; break; default: // Ignore that number break; } } if (contactNumbers != null) { contactNumbers.close(); } HashMap<String,String> contact = new HashMap<String, String>(); contact.put("NAME", displayName); contact.put("MOBILE", mobileNumber); contacts.add(contact); } if (users != null) { users.close(); } return contacts; } catch (Exception e) { Log.e("DEBUG","exception " + e); } return null; } protected void onPostExecute(ArrayList<HashMap<String,String>> result) { if (result == null) { contactsCallback.onError(); } else { contactsCallback.onSuccess(result); } } } [...] public interface ContactsCallback { void onSuccess(List<HashMap<String,String>> contacts); void onError(); } } |
What next?
This code tutorial includes an introduction to MVP. App architecture is probably the most important factor determining app longevity. With good architecture, it is easy to add or change features, and therefore, the app is a pleasure to maintain. So it’s well worth keeping an eye on Flutter Architecture Samples, a project inspired, in part, by Android Architecture Blueprints.

Mobile app developer with 12 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).
Please extend this tutorial to iOS too!
I plan to do it, but it will take me a few weeks: I need to sort out access to a Mac & iPhone, which I currently don’t have (I’m an Android/Linux user).
Hi Natalie,
Thank you very much for your write up.
I’ve had a similar need to ask for permissions, and used the (defunct) permit plugin (https://github.com/goposse/permit) to get me started.
I spent more than an hour trying to figure out why my snackbar isn’t showing, until I read about your one second delay. It was even worse for me, no snackbars would show anymore at all until I restarted the app.
I’ve looked into the snackbar issue: the activity should return the result from onPostResume() instead. Then you don’t need the second delay anymore. See https://github.com/flutter/flutter/issues/15777
Great, thanks very much for looking into it, that’s very much appreciated! I’ll update the code-tutorial.
Great tutorial. Thanks for spending time on educating us. Is the full example available in GitHub?
Not yet on github (it will be at some point, I haven’t got around to it yet), but all the code is in the tutorial, there is no extra code required.
Thanks a lot. The code worked beautifully. I am novice Android programmer and I learnt a lot by following your tutorial.
I need to change the line in the function getContactsWithMobilePhoneNumber(home_model.dart) from
final List<Map> result = await _methodChannel.invokeMethod(‘getContacts’);
to
var result = await _methodChannel.invokeMethod(‘getContacts’);
After the latest upgrade, it was producing a run time error
type ‘List’ is not a subtype of type ‘List<Map>’ where
Hope this is useful to someone
Thanks
Jay
Thanks so much for this. I’ve been trying to figure out how to get user permissions and wait for a result for a while, and I couldn’t figure out how to make the call synchronous with the async callback within the native code.
I’ve been in C# for so long these days that it’s taking a while to get back to how Java does stuff.
Thanks again!
Thanks for your post, I would like to know if it would be the same to request permission to write in “/storage/emulated/0/Download”, I have tried many plugins and I have not been able to make it work