In this article, I am going to build two mobile apps, one in Flutter and another in React Native side by side. These apps will have the following functions:
- A home screen with a "Pick from the contacts" button and a list of the selected contacts.
- A popup that will contain a list of all contacts with a select/unselects all toggle buttons, a cancel button, and a confirm button.
Demo code is available on Github
- Flutter Contact Picker Demo: https://github.com/28harishkumar/flutter_contact_list
- React Native Contact Picker Demo: https://github.com/28harishkumar/react_native_contact_list
Content
- Prerequisite
- Create widgets/components
- Create a Home Screen
- Create Contact Picker
- Performance and APK size
- Conclusion
Prerequisite
I am assuming that
- You have React Native and Flutter installed
- You have basic knowledge of React Native and Flutter
React Native
Create a new React Native app named react_native_contact_list by using:
- npx react-native init react_native_contact_list --template=react-native-template-typescript
I prefer to use TypeScript over Javascript for making my life easier.
For accessing the contact list, we need a third-party package react-native-contacts. Don't forget to update Proguard file as follow https://www.npmjs.com/package/... as we will enable progaurd for reducing APK size.
Flutter
Create a new project named flutter_contact_list using Android Studio or Visual Studio Code as mentioned here
https://flutter.dev/docs/get-started/editor?tab=androidstudio
https://flutter.dev/docs/get-started/test-drive?tab=vscode#create-app
For accessing the contacts in Flutter, I am going to use contact_services https://pub.dev/packages/conta... and permission_handler https://pub.dev/packages/permi....
Create Widgets / Components
For this demo, we will need some basic utilities like button UI. I am creating them before writing the code for the screens.
React Native
In your react-native project, create a new directory components and open a new file button.tsx in it. React Native buttons do not provide customerization options. So I am going to use Pressable component provided by React Native. Button will take two required and two optional arguments (terms as props):
- onPress function
- Text inside button
- Button style
- Button text style
- import React from "react";
- import {
- Text,
- Pressable,
- StyleSheet,
- } from "react-native";
-
- type ButtonProps = {
- style?: any; // custom button style
- textStyle?: any; // text component style
-
- onPress: () => void; // action on press
- title: string; // text inside button
- }
-
- export default function(props: ButtonProps) {
- return (
- <Pressable
- style={[styles.button, props.style]}
- onPress={props.onPress}>
- <Text style={[styles.text, props.textStyle]}>
- {props.title}
- </Text>
- </Pressable>
- )
- }
-
- const styles = StyleSheet.create({
- // white background
- button: {
- backgroundColor: "#ffffff",
- },
- // default blue text color
- text: {
- textAlign: 'center',
- margin: 8,
- color: '#007AFF',
- fontSize: 18,
- },
- });
Other than the button, I am defining ListItem (contact) component along with TypeScript types for Contact data. Add the following in your App.tsx.
- import React, { useState, useEffect } from "react";
-
- //
- import {
- SafeAreaView,
- StyleSheet,
- View,
- Text,
- StatusBar,
- Modal,
- FlatList,
- Alert,
- Platform,
- PermissionsAndroid,
- } from "react-native";
- import Contacts from "react-native-contacts";
-
- // import button component
- import Button from "./components/button";
-
- // Contact data item
- type PickableContact = {
- name: string;
- phone: string;
- };
-
- // Contact List item props
- type ContactListItem = {
- item: PickableContact;
- index: number;
- };
-
- // contact list item component
- function renderContactListItem({ item: contact }: ContactListItem) {
- return (
- <View style={styles.listItem}>
- <View style={styles.userInfo}>
- <Text style={styles.name}>
- {contact.name}
- </Text>
- <Text style={styles.phone}>
- {contact.phone}
- </Text>
- </View>
- </View>
- )
- }
Flutter
I am creating a model file in lib/models/simplified_contract.dart.
- // This class will be used to store name and phone
- class SimplifiedContact {
- final int index;
- final String name;
- final String phone;
-
- SimplifiedContact(this.index, this.name, this.phone);
- }
Unline React Native, flutter provide option to customerize its Button widget. I still need a Button Widget for toggling purpose. I am creating a ActionButton widget in lib/widgets/action_button.dart. For keeping minimum dependencies, I am not using state managing packages like redux and mobx.
- import 'package:flutter/material.dart';
-
- class ActionButton extends StatelessWidget {
- // flag for selected contact
- final bool isSelected;
-
- // callback on de-selecting
- final Function() onRemove;
-
- // callback on selecting
- final Function() onSelect;
-
- ActionButton({this.isSelected, this.onRemove, this.onSelect});
-
- @override
- Widget build(BuildContext context) {
- if (isSelected) {
- return RaisedButton(
- color: Colors.white,
- elevation: 0,
- child: Text(
- 'Remove',
- style: TextStyle(color: Colors.redAccent),
- ),
- onPressed: onRemove,
- );
- }
-
- return RaisedButton(
- color: Colors.white,
- elevation: 0,
- child: Text(
- 'Select',
- style: TextStyle(color: Colors.blueAccent),
- ),
- onPressed: onSelect,
- );
- }
- }
Create a Home Screen
Lets create a home screen with a button and selected contact list.
React Native
Update the App.tsx file to return a button, a popup and a list
- return (
- <>
- <StatusBar barStyle="dark-content" />
- {shouldShowModal && renderContactModal()}
- <SafeAreaView style={styles.container}>
- <Button
- style={styles.contactBtn}
- title="Select Contacts"
- onPress={showContactModal}
- />
- <FlatList
- style={styles.selectedList}
- data={selectedContacts}
- renderItem={renderContactListItem}
- keyExtractor={(item, index) => index + item.name + item.phone}
- />
- </SafeAreaView>
- </>
- );
Also we need to define the state variables (inside App function):
- // all contacts
- const [allContacts, setAllContacts] = useState<PickableContact[]>([]);
-
- // temporary selected contacts indices
- const [tempSelectedContacts, setTempSelectedContacts] = useState<number[]>([]);
-
- // finally selected contact indicies
- const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
-
- // selected contacts
- const [selectedContacts, setSelectedContacts] = useState<PickableContact[]>([]);
-
- // popup (modal) state (open or closed)
- const [shouldShowModal, setModalStatus] = useState(false);
For toggling contact modal, we need to write a function as follow:
- function showContactModal() {
- // load contacts (I will write this function in next part)
- loadContacts();
-
- // open popup
- setModalStatus(true);
-
- // copy selected contact indices to temporary selected contact indices
- setTempSelectedContacts(selectedIndices);
- }
Flutter
I am creating a new file in lib directory named as home.dart and adding a StatefullWidget class.
- import 'package:flutter/cupertino.dart';
- import 'package:flutter/material.dart';
-
- // I will define this file in next section
- import 'package:flutter_contact_list/widgets/contact_list.dart';
-
- // SimpifiedContact class file
- import 'models/simplified_contact.dart';
-
- class HomePage extends StatefulWidget {
- // Get contacts permission
- @override
- _HomePageState createState() => _HomePageState();
- }
-
- class _HomePageState extends State<HomePage> {
- // for triggering submit function in contact widget
- // in professional code, I suggest to use mobx or redux for such purposes
- final GlobalKey<dynamic> contactWidgetKey = GlobalKey();
-
- // selected contacts
- List<SimplifiedContact> selectedContacts = [];
-
- // selected contact indices
- List<int> selectedContactIndices = [];
-
- // action on contact list selection
- void onContactSelect(List<SimplifiedContact> contacts, List<int> indices) {
- this.setState(() {
- selectedContacts = contacts;
- selectedContactIndices = indices;
- });
- }
-
- Future<void> onContactBtnPress(BuildContext context) async {
- // TODO: I will write this function in the next section
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: SafeArea(
- child: Column(
- mainAxisSize: MainAxisSize.max,
- children: [
-
- // Contact Pick Button
- Container(
- margin: EdgeInsets.only(top: 20),
- child: ElevatedButton(
- onPressed: () {
- onContactBtnPress(context);
- },
- child: Text('Select Contacts'),
- ),
- ),
- Container(
- child: Flexible(
-
- // selected contact list
- child: ListView.builder(
- scrollDirection: Axis.vertical,
- shrinkWrap: true,
- itemCount: selectedContacts?.length ?? 0,
- itemBuilder: (BuildContext context, int index) {
- SimplifiedContact contact =
- selectedContacts?.elementAt(index);
-
- return Column(
- children: [
- ListTile(
- contentPadding: const EdgeInsets.symmetric(
- vertical: 2,
- horizontal: 18,
- ),
- title: Text(contact.name),
- subtitle: Text(contact.phone),
- ),
- Divider(
- height: 1,
- ),
- ],
- );
- },
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
Note that I have defined only two state variable in Flutter (that is 5 in the React Native) because I don't need a popup state variable in Flutter (reduced one state variable) and I have split Contact picking code to another file (reduced 2 more state variables)
Create Contact Picker
For contact picker, I need:
- a permission request function
- a contact loading function
- a popup rendering function (widget class in case of Flutter)
- a function for contact list picker item (React Native only)
- a function for selecting contact
- a function for unselecting contact
- a function for selecting all contacts
- a function for deselecting all contacts
- a final function for returning selected contacts
React Native
- // function for loading contacts (reading from user's mobile)
- async function loadContacts() {
- // ask for contact permission on android
- if (Platform.OS === "android") {
- const granted = await PermissionsAndroid.request(
- PermissionsAndroid.PERMISSIONS.READ_CONTACTS, {
- title: "Contacts",
- message: "This app would like to view your contacts.",
- buttonNeutral: "Ask Me Later",
- buttonNegative: "Cancel",
- buttonPositive: "OK"
- });
-
- if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
- Alert.alert("Error", "Contact permission is required!");
- return null;
- }
- }
-
- // read from user's mobile
- Contacts.getAll()
- .then(contacts => {
- // get list of name and phone numbers
- // response will be a list of lists like [[{name: "", phone:""}, {...}], [...]]
- const namedContacts = contacts.map(c => {
- const name = (c.givenName + " " + c.middleName + " " + c.familyName).trim();
-
- return c.phoneNumbers.map(p => ({
- name,
- phone: p.number,
- }))
- });
-
- // reduce nested list into a linear list like [{name:"", phone: ""}, {...}]
- const simplifiedContacts = namedContacts.reduce((arr, val) => arr.concat(val));
-
- // set contacts
- setAllContacts(simplifiedContacts);
- })
- .catch(e => {
- Alert.alert("Error", e);
- });
- }
-
- // renders popup
- function renderContactModal() {
- return (
- {/* React Native Popup component */}
- <Modal
- onRequestClose={cancelSelection}>
- <SafeAreaView style={styles.modalContainer}>
-
- {/* Header (back button, title and done button */}
- <View style={styles.header}>
- <Button
- title="Back"
- style={styles.backBtn}
- onPress={cancelSelection}
- />
- <Text style={styles.title}>
- Select Contacts
- </Text>
- <Button
- title="Done"
- style={styles.doneBtn}
- onPress={confirmSelection}
- />
- </View>
-
- {/* Contact list */}
- <FlatList
- style={styles.contactList}
- contentContainerStyle={styles.contactListContent}
- data={allContacts}
- renderItem={renderPickContactItem}
- keyExtractor={(item, index) => index + item.name + item.phone}
- getItemLayout={(data, index) => ({ length: 70, offset: index * 70, index })}
- />
- {
- // select all and unselect all buttons
- tempSelectedContacts.length === allContacts.length ? (
- <Button
- style={styles.toggleBtn}
- textStyle={styles.toggleBtnText}
- title="Unselect All"
- onPress={unselectAllContacts}
- />
- ) : (
- <Button
- style={styles.toggleBtn}
- textStyle={styles.toggleBtnText}
- title="Select All"
- onPress={selectAllContacts}
- />
- )
- }
- </SafeAreaView>
- </Modal>
- )
- }
-
- // contact list item
- function renderPickContactItem({ item: contact, index }: ContactListItem) {
- return (
- <View style={styles.listItem}>
- <View style={styles.userInfo}>
- <Text style={styles.name}>
- {contact.name}
- </Text>
- <Text style={styles.phone}>
- {contact.phone}
- </Text>
- </View>
- {
- tempSelectedContacts.indexOf(index) > -1 ? (
- <Button
- style={styles.actionBtn}
- textStyle={styles.removeText}
- title="Remove"
- onPress={() => removeContact(index)}
- />
- ) : (
- <Button
- style={styles.actionBtn}
- title="Select"
- onPress={() => selectContact(index)}
- />
- )
- }
- </View>
- )
- }
-
- // select contact
- function selectContact(index: number) {
- setTempSelectedContacts([
- ...tempSelectedContacts,
- index,
- ]);
- }
-
- // unselect contact
- function removeContact(index: number) {
- const existingIndex = tempSelectedContacts.indexOf(index);
-
- if (existingIndex > -1) {
- tempSelectedContacts.splice(existingIndex, 1);
- setTempSelectedContacts([...tempSelectedContacts]);
- }
- }
-
- // select all contacts
- function selectAllContacts() {
- setTempSelectedContacts(allContacts.map((a, i) => i));
- }
-
- // unselect all contacts
- function unselectAllContacts() {
- setTempSelectedContacts([]);
- }
-
- // cancel contact picking
- function cancelSelection() {
- setModalStatus(false);
- }
-
- // confirm & set contacts
- function confirmSelection() {
- setModalStatus(false);
-
- setSelectedIndices(tempSelectedContacts);
- setSelectedContacts(
- allContacts.filter((c, i) => tempSelectedContacts.indexOf(i) > -1)
- );
- }
Flutter
I have split the code in two parts. First modify onContactBtnPress() in home.dart
- Future<void> onContactBtnPress(BuildContext context) async {
- // open al popup
- showDialog(
- context: context,
- barrierColor: Colors.white,
- barrierDismissible: false,
- builder: (_) => Scaffold(
- // header (back icon button, header, done icon button)
- appBar: AppBar(
- elevation: 0,
- backgroundColor: Colors.white,
- brightness: Brightness.light,
- textTheme: TextTheme(
- headline6: TextStyle(
- color: Colors.black,
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
- ),
- iconTheme: IconThemeData(
- color: Colors.black,
- ),
- title: Text('Select Contact'),
- actions: [
- IconButton(
- icon: Icon(Icons.check),
- onPressed: () {
- contactWidgetKey.currentState.onDone(context);
- },
- )
- ],
- ),
- body: Column(
- children: [
-
- // including ContactList widget (I am going to define it)
- Container(
- child: ContactList(
- key: contactWidgetKey,
- onDone: onContactSelect,
- selectedContactIndices: selectedContactIndices,
- ),
- ),
- ],
- ),
- ),
- );
- }
Secondly, I created a contact_list.dart in widgets as:
- import 'package:contacts_service/contacts_service.dart';
- import 'package:flutter/cupertino.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_contact_list/models/simplified_contact.dart';
- import 'package:permission_handler/permission_handler.dart';
-
- // import button
- import 'action_button.dart';
-
- class ContactList extends StatefulWidget {
- // callback on contact list submit
- final Function(List<SimplifiedContact>, List<int>) onDone;
-
- // already selected contacts
- final List<int> selectedContactIndices;
-
- ContactList({
- Key key,
- this.onDone,
- this.selectedContactIndices,
- }) : super(key: key);
-
- @override
- _ContactListState createState() => _ContactListState();
- }
-
- class _ContactListState extends State<ContactList> {
- // all contacts
- List<SimplifiedContact> contactList;
-
- // currently selected contacts
- List<int> selectedIndices = [];
-
- @override
- void initState() {
- super.initState();
-
- // copy selected indices from arguments to state
- selectedIndices = widget.selectedContactIndices;
-
- // get contacts from user device
- getContacts();
- }
-
- // ask user's permission
- Future<PermissionStatus> _getPermission() async {
- final PermissionStatus permission = await Permission.contacts.status;
- final PermissionStatus granted = PermissionStatus.granted;
- final PermissionStatus denied = PermissionStatus.denied;
- final PermissionStatus undetermined = PermissionStatus.undetermined;
-
- if (permission != granted && permission != denied) {
- // permission is not given, so ask user again
- final permissionStatusMap = await [Permission.contacts].request();
-
- return permissionStatusMap[Permission.contacts] ?? undetermined;
- } else {
- // already have permission
- return permission;
- }
- }
-
- // get contacts from user's device
- Future<void> getContacts() async {
- final PermissionStatus permissionStatus = await _getPermission();
-
- if (permissionStatus == PermissionStatus.granted) {
- // execute with permission
-
- int index = 0;
- final Iterable<Contact> contacts = await ContactsService.getContacts();
- final List<SimplifiedContact> tempContacts = [];
-
- // convert Contact list to SimplifiedContact list
- contacts.forEach((contact) {
- contact.phones.forEach((phoneData) {
- tempContacts.add(SimplifiedContact(
- index,
- contact.displayName,
- phoneData.value,
- ));
-
- index++;
- });
- });
-
- setState(() {
- contactList = tempContacts;
- });
- } else {
- // contact read permission is not granted (show error)
- showDialog(
- context: context,
- builder: (BuildContext context) => CupertinoAlertDialog(
- title: Text('Permissions error'),
- content: Text('Grant contacts permission to see the contacts'),
- actions: <Widget>[
- CupertinoDialogAction(
- child: Text('OK'),
- onPressed: () => Navigator.of(context).pop(),
- )
- ],
- ),
- );
- }
- }
-
- // on a contact selection
- void onContactSelect(int index) {
- setState(() {
- selectedIndices.add(index);
- });
- }
-
- // on a contact unselect
- void onContactRemove(int index) {
- int elementIndex = selectedIndices.indexOf(index);
-
- if (elementIndex > -1) {
- setState(() {
- selectedIndices.removeAt(elementIndex);
- });
- }
- }
-
- // select all contacts
- void selectAll(BuildContext context) {
- List<int> indexList = [];
- int index = 0;
-
- contactList.forEach((element) {
- indexList.add(index++);
- });
-
- setState(() {
- selectedIndices = indexList;
- });
- }
-
- // unselect all contacts
- void unselectAll(BuildContext context) {
- setState(() {
- selectedIndices = [];
- });
- }
-
- // callback on compelte
- void onDone(BuildContext context) {
- int index = 0;
- List<SimplifiedContact> tempSelected = [];
-
- if (contactList != null) {
- contactList.forEach((element) {
- if (selectedIndices.indexOf(index++) > -1) {
- tempSelected.add(element);
- }
- });
-
- // NOTE: use mobx or redux call instead
- // return selected contacts back to parent widget
- widget.onDone(tempSelected, selectedIndices);
- }
-
- Navigator.pop(context);
- }
-
- @override
- Widget build(BuildContext context) {
- // contact is not loaded
- if (contactList == null) {
- return Flexible(
- child: Center(
- child: const CircularProgressIndicator(),
- ),
- );
- }
-
- return Flexible(
- child: Column(
- children: [
- Flexible(
-
- // contact list
- child: ListView.builder(
- scrollDirection: Axis.vertical,
- shrinkWrap: true,
- itemCount: contactList?.length ?? 0,
- itemBuilder: (BuildContext context, int index) {
- SimplifiedContact contact = contactList?.elementAt(index);
-
- return Column(
- children: [
- ListTile(
- contentPadding: const EdgeInsets.symmetric(
- vertical: 2,
- horizontal: 18,
- ),
- title: Text(contact.name ?? ''),
- subtitle: Text(contact.phone),
- trailing: new ActionButton(
- isSelected: selectedIndices.indexOf(index) > -1,
- onRemove: () {
- onContactRemove(index);
- },
- onSelect: () {
- onContactSelect(index);
- },
- ),
- ),
- Divider(
- height: 1,
- ),
- ],
- );
- },
- ),
- ),
-
- // select all and unselect all buttons
- Container(
- height: 60,
- padding: EdgeInsets.symmetric(vertical: 15, horizontal: 50),
- child: contactList != null &&
- selectedIndices.length == contactList.length
- ? ElevatedButton(
- child: Text('Unselect All'),
- onPressed: () {
- unselectAll(context);
- },
- )
- : ElevatedButton(
- child: Text('Select All'),
- onPressed: () {
- selectAll(context);
- },
- ),
- ),
- ],
- ),
- );
- }
- }
Performance and APK size
I am creating a release version only for Android because I don't have an iOS device to test real performance. Both apps are working fine in my Simulator. For creating a release APK, we have to follow:
https://reactnative.dev/docs/signed-apk-android and https://flutter.dev/docs/deployment/android
I have set true for both enableProguardInReleaseBuilds and enableSeparateBuildPerCPUArchitecture.
For React Native my APK size is 6.6 MB to 7.5 MB

For Flutter my APK size is 5.7 MB to 6.2 MB

Clearly, Flutter APK is around 1 MB smaller in the size.
Both apps are running fine on my Android device. In debugging mode, the React Native list update was slow but it surprised me in release mode. Of course, Flutter APK is also faster in the release version. Flutter is taking almost 5-6 seconds in reading my contacts (release version). I will work on it. All other things are crazy cool. I will add more APK Analysis later.
Conclusion
I had to write almost equal code for both frameworks. I created a very simple app but all of my APKs are more than 6 MB. Clearly, both React Native and Flutter add more size than a Native app. Large lists handling in Flutter is better than React Native. Anyway, I like Flutter touches (maybe because of the Material Theme). The good thing about the Flutter is that its size is relatively small even when it is including Material Theme & Icons.