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.

Create app flutter create runtimepermissionexample 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.

main.dart 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(), ); } } 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.

home_page.dart 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), ); } } 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.

contact.dart class Contact { final String displayName; final String mobileNumber; const Contact(this.displayName, this.mobileNumber); } 1 2 3 4 5 6 7 class Contact { final String displayName ; final String mobileNumber ; const Contact ( this . displayName , this . mobileNumber ) ; }

Looking for a Flutter job? Check out my job board dedicated to Flutter at flutterjobs.info

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.

home_contract.dart abstract class View { } abstract class Model { } abstract class Presenter { } 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.

home_contract.dart/Presenter import 'dart:async'; [...] abstract class Presenter { Future viewDisplayed(); } 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.

home_contract.dart/Model [...] 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 } 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.

home_contract.dart/View abstract class View { void showErrorMessage(); void showContactsWithPhoneNumber(List<Contact> contacts); void showLoadingContactsInProgress(); void showPermissionRationale(); } 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.

home_presenter.dart 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; } } 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.

home_model.dart 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); } } 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.

home_page.dart [...] 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(); }, ), ], ), ); } } 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.

AndroidManifest.xml [...] <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_CONTACTS"/> [...] 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.

home_model.dart [...] 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); } [...] } 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.

MainActivity.java [...] 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(); } } 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.

app/build.gradle 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' } 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.

home_model.dart @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); } 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.

MainActivity.java [...] 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(); } } 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.

Check out Being a software developer in 2020!

Related