Going fullstack with Flutter and MongoDB Atlas Data API
Demola Malomo
Posted on March 29, 2023
Application Programming Interface (API) has revolutionized how applications are built. It enables software modules to communicate with each other using sets of standards. APIs have allowed companies to integrate new applications with existing software systems, provide an avenue for rapid innovation, and act as a gateway for enhancing customer experience across multiple platforms (mobile, web, etc.).
With the possibilities that APIs offer, it has also come with its share of difficulties. Developers are burdened with selecting the right database, integrating security, logging and monitoring, etc.
In this post, we will learn how to build a mobile phonebook using MongoDB Data API and Flutter. The project’s GitHub repository can be found here.
What is MongoDB Data API
MongoDB Data API is a fully managed platform-agnostic service that provides a secure and fast standard HTTPS for managing data stored on MongoDB. Web browsers, web servers, CI/CD pipelines, serverless & edge computing environments, mobile applications, or any HTTPS-enabled platforms can connect to the Data API using standard HTTPS requests. The Data API also ships with the following:
- Authorization and authentication mechanism
- Data validation when writing and reading from the API
- Support for JSON or EJON API response
Prerequisites
To fully grasp the concepts presented in this tutorial, the following are required:
- Basic understanding of Dart and Flutter
- Flutter SDK installed
- MongoDB account. Signup is completely free
- Either iOS Simulator, Android Studio, or Chrome web browser to run the application
Getting started
To get started, we need to clone the project by navigating to the desired directory and running the command below:
git clone https://github.com/Mr-Malomz/flutter_mongo && cd flutter_mongo
Running the project
First, we need to install the project dependencies by running the command below:
flutter pub get
Then, run the project using the following command:
flutter run
The command above will run the application on the selected device.
Setting up MongoDB Data API
Create a Database
With our application up and running, we need to log in or sign up into our MongoDB account. Click the project dropdown menu and click on the New Project button.
Enter the flutter_realm
as the project name, click Next, and click Create Project.
Click on Build a Database
Select Shared as the type of database and Create to set up a cluster.
Next, we need to navigate to the Database menu, click the Browse Collection tab, and click the Add My Own Data button to add sample data to our database.
To add sample data, first, we need to create a phonebook
database and a phonebookCollection
collection.
Lastly, we need to add sample data to our phonebookCollection
as shown below:
Key | Value | Object Type |
---|---|---|
fullname | John Travolta | String |
phonenumber | 907865744546 | Int64 |
Setup the Data API
With our database fully set up, first, we need to navigate to the Data API tab, select the Cluster our database was created in and click the Enable Data Access from the Data API button.
With that done, MongoDB automatically provides us with a fast and secure API service. By default, the provisioned API is inaccessible. We need to create an API key to connect to the API securely. To do this, click the Create API Key button, input flutter_mongo
as the name and Generate API Key.
PS: We must copy the generated API key. It will come in handy when integrating the API with our Flutter application.
Integrating MongoDB Data API with Flutter
With all that done, we can start building our application using the API. First, we need to create a model to convert the response sent from the Data API to a Dart object. The model will also cater to JSON serialization. To do this, we need to create a utils.dart
file in the lib
folder and add the snippet below:
class PhoneBook {
String? id;
String fullname;
int phonenumber;
PhoneBook({
this.id,
required this.fullname,
required this.phonenumber,
});
Map<dynamic, dynamic> toJson() {
return {
"fullname": fullname,
"phonenumber": phonenumber,
};
}
factory PhoneBook.fromJson(Map<dynamic, dynamic> json) {
return PhoneBook(
id: json['_id'],
fullname: json['fullname'],
phonenumber: json['phonenumber'],
);
}
}
Next, we must create a service file to separate the application core logic from the UI. To do this, create a phone_service.dart
file inside the lib
directory. Then, add the snippet below:
import 'dart:convert';
import 'package:flutter_mongo/utils.dart';
import 'package:dio/dio.dart';
class PhoneService {
final dio = Dio();
final String _dataSource = "Cluster0";
final String _database = "phonebook";
final String _collection = "phonebookCollection";
final String _endpoint = "<REPLACE WITH THE ENDPOINT URL>";
static const _apiKey = "REPLACE WITH THE API KEY";
var headers = {
"content-type": "application/json",
"apiKey": _apiKey,
};
Future<List<PhoneBook>> getPhoneContacts() async {
var response = await dio.post(
"$_endpoint/action/find",
options: Options(headers: headers),
data: jsonEncode(
{
"dataSource": _dataSource,
"database": _database,
"collection": _collection,
"filter": {},
},
),
);
if (response.statusCode == 200) {
var respList = response.data['documents'] as List;
var phoneList = respList.map((json) => PhoneBook.fromJson(json)).toList();
return phoneList;
} else {
throw Exception('Error getting phone contacts');
}
}
Future<PhoneBook> getSinglePhoneContact(String id) async {
var response = await dio.post(
"$_endpoint/action/find",
options: Options(headers: headers),
data: jsonEncode(
{
"dataSource": _dataSource,
"database": _database,
"collection": _collection,
"filter": {
"_id": {"\$oid": id}
},
},
),
);
if (response.statusCode == 200) {
var resp = response.data\['documents'\][0];
var contact = PhoneBook.fromJson(resp);
return contact;
} else {
throw Exception('Error getting phone contact');
}
}
Future updatePhoneContact(String id, String fullname, int phonenumber) async {
var response = await dio.post(
"$_endpoint/action/updateOne",
options: Options(headers: headers),
data: jsonEncode(
{
"dataSource": _dataSource,
"database": _database,
"collection": _collection,
"filter": {
"_id": {"\$oid": id}
},
"update": {
"\$set": {"fullname": fullname, "phonenumber": phonenumber}
}
},
),
);
if (response.statusCode == 200) {
return response.data;
} else {
throw Exception('Error getting phone contact');
}
}
Future createPhoneContact(String fullname, int phonenumber) async {
var response = await dio.post(
"$_endpoint/action/insertOne",
options: Options(headers: headers),
data: jsonEncode(
{
"dataSource": _dataSource,
"database": _database,
"collection": _collection,
"document": {"fullname": fullname, "phonenumber": phonenumber}
},
),
);
if (response.statusCode == 201) {
return response.data;
} else {
throw Exception('Error creating phone contact');
}
}
Future deletePhoneContact(String id) async {
var response = await dio.post(
"$_endpoint/action/deleteOne",
options: Options(headers: headers),
data: jsonEncode(
{
"dataSource": _dataSource,
"database": _database,
"collection": _collection,
"filter": {
"_id": {"\$oid": id}
},
},
),
);
if (response.statusCode == 200) {
return response.data;
} else {
throw Exception('Error deleting phone contact');
}
}
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
PhoneService
class with_dataSource
,_database
,_collection
,_endpoint
,_apikey
, andheaders
properties. - Creates the
getPhoneContacts
,getSinglePhoneContact
,updatePhoneContact
,createPhoneContact
, anddeletePhoneContact
methods that uses theDio
package to configure permissions and make secure HTTPS request to the Data API configured earlier and returns the appropriate responses
Consuming the service
With that done, we can use the service to perform the required operation.
Get all contacts
To get started, we need to modify the home.dart
file in the screens
directory and update it by doing the following:
First, we need to import the required dependencies and create a method to get the list of contacts saved in the database:
import 'package:flutter/material.dart';
import 'package:flutter_mongo/phone_service.dart';
import 'package:flutter_mongo/screens/create.dart';
import 'package:flutter_mongo/screens/detail.dart';
import 'package:flutter_mongo/utils.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late List<PhoneBook> contacts;
bool _isLoading = false;
bool _isError = false;
@override
void initState() {
getContacts();
super.initState();
}
getContacts() {
setState(() {
_isLoading = true;
});
PhoneService().getPhoneContacts().then((value) {
setState(() {
contacts = value;
_isLoading = false;
});
}).catchError((onError) {
setState(() {
_isLoading = false;
_isError = true;
});
});
}
@override
Widget build(BuildContext context) {
// UI CODE GOES HERE
}
}
The snippet above does the following:
- Imports the required dependencies
- Lines 14-16: Create the
contacts
,_isLoading
, and_isError
properties to manage the application state - Lines 19-41: Create a
getContacts
method to get the list of available contacts on the database using thePhoneService().getPhoneContacts
and set states accordingly
Lastly, we need to modify the UI to use the states and method created to get the contacts list.
//imports goes here
class Home extends StatefulWidget {
//code goes here
}
class _HomeState extends State<Home> {
//state goes here
@override
void initState() {
/code goes here
}
getContacts() {
//code goes here
}
@override
Widget build(BuildContext context) {
return _isLoading
? const Center(
child: CircularProgressIndicator(
color: Colors.blue,
))
: _isError
? const Center(
child: Text(
'Error getting phone contacts',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
)
: Scaffold(
appBar: AppBar(
title: const Text('Phonebook'),
backgroundColor: Colors.black,
),
body: ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
Detail(id: contacts[index].id!),
),
);
},
child: Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(width: .5, color: Colors.grey),
),
),
padding: EdgeInsets.fromLTRB(10, 20, 10, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 7,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
contacts[index].fullname,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800),
),
SizedBox(height: 10.0),
Text(contacts[index].phonenumber.toString())
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(height: 10.0),
Icon(Icons.arrow_forward_ios_rounded)
],
),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Create(),
),
);
},
backgroundColor: Colors.black,
tooltip: 'Create contact',
child: const Icon(Icons.add),
),
);
}
}
Create contacts
To create a contact, we need to modify the create.dart
file in the screen
directory and update it by doing the following:
First, we need to import the required dependency and create a method to save the contact to the database:
import 'package:flutter/material.dart';
import 'package:flutter_mongo/phone_service.dart'; //ADD THIS
import 'package:flutter_mongo/screens/home.dart';
class Create extends StatefulWidget {
const Create({
Key? key,
}) : super(key: key);
@override
State<Create> createState() => _CreateState();
}
class _CreateState extends State<Create> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _fullname = TextEditingController();
final TextEditingController _phonenumber = TextEditingController();
bool _isLoading = false;
createContact() {
setState(() {
_isLoading = true;
});
PhoneService()
.createPhoneContact(_fullname.text, int.parse(_phonenumber.text))
.then((value) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact created successfully!')),
);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Home()),
);
}).catchError((onError) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error creating contact!')),
);
});
}
@override
Widget build(BuildContext context) {
//UI CODE GOES HERE
}
}
The snippet above does the following:
- Import the required dependency
- Lines 15-17: Create the
_fullname
,_phonenumber
, and_isLoading
properties to manage the application state - Lines 17-38: Create a
createContact
method to save the contact using thePhoneService().createPhoneContact
service, set states accordingly
Lastly, we need to modify the UI to use the method and states created to process the form.
//import goes here
class Create extends StatefulWidget {
//code goes here
}
class _CreateState extends State<Create> {
//state goes here
createContact() {
//code goes here
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Create contact"),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30.0),
child: Form(
key: _formKey,
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Fullname',
style: TextStyle(
color: Colors.grey,
fontSize: 14.0,
),
),
const SizedBox(height: 5.0),
TextFormField(
controller: _fullname,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input your fullname';
}
return null;
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20),
hintText: "input name",
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
keyboardType: TextInputType.text,
maxLines: null,
),
const SizedBox(height: 30.0),
const Text(
'Phone number',
style: TextStyle(
color: Colors.grey,
fontSize: 14.0,
),
),
const SizedBox(height: 5.0),
TextFormField(
controller: _phonenumber,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input your phone number';
}
return null;
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20),
hintText: "input phone number",
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.red),
),
),
keyboardType: TextInputType.number,
),
],
),
],
),
const SizedBox(height: 30.0),
SizedBox(
height: 45,
width: double.infinity,
child: TextButton(
onPressed: _isLoading
? null
: () {
if (_formKey.currentState!.validate()) {
createContact();
}
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.black),
),
child: const Text(
'Create contact',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
),
),
),
],
),
),
),
);
}
}
Get a contact, edit a contact and delete contacts
To perform the stated operations in our application, we need to modify the detail.dart
file in the screens
directory and update it by doing the following:
First, we need to import the required dependencies and create methods to get, edit, and delete contacts.
import 'package:flutter/material.dart';
import 'package:flutter_mongo/phone_service.dart';
import 'package:flutter_mongo/screens/home.dart';
import 'package:flutter_mongo/utils.dart';
class Detail extends StatefulWidget {
const Detail({Key? key, required this.id}) : super(key: key);
final String id;
@override
State<Detail> createState() => _DetailState();
}
class _DetailState extends State<Detail> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _fullname = TextEditingController();
final TextEditingController _phonenumber = TextEditingController();
late PhoneBook contact;
bool _isLoading = false;
bool _isSubmitting = false;
bool _isError = false;
@override
void initState() {
getContacts();
super.initState();
}
getContacts() {
setState(() {
_isLoading = true;
});
PhoneService().getSinglePhoneContact(widget.id).then((value) {
setState(() {
contact = value;
_isLoading = false;
});
_fullname.text = value.fullname;
_phonenumber.text = value.phonenumber.toString();
}).catchError((onError) {
setState(() {
_isLoading = false;
_isError = true;
});
});
}
updateContact(String fullname, int phonenumber) {
setState(() {
_isSubmitting = true;
});
PhoneService()
.updatePhoneContact(widget.id, fullname, phonenumber)
.then((value) {
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact updated successfully!')),
);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Home()),
);
}).catchError((onError) {
setState(() {
_isSubmitting = false;
_isError = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error updating contact!')),
);
});
}
deleteContact() {
setState(() {
_isSubmitting = true;
});
PhoneService().deletePhoneContact(widget.id).then((value) {
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact deleted successfully!')),
);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Home()),
);
}).catchError((onError) {
setState(() {
_isSubmitting = false;
_isError = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error deleting contact!')),
);
});
}
@override
Widget build(BuildContext context) {
//UI GOES HERE
}
}
The snippet above does the following:
- Import the required dependencies
- Lines 15-20: Create the
_fullname
,_phonenumber
,contact
,_isLoading
,_isSubmitting
, and_isError
properties to manage the application state - Lines 17-38: Create the
getSingleContact
,updateContact
, anddeleteContact
methods to get details of the selected contact, update it and delete it using thePhoneService().getSinglePhoneContact
,PhoneService().updatePhoneContact
, andPhoneService().deletePhoneContact
service respectively, set states accordingly
Lastly, we need to modify the UI to use the methods and states created to process the operations.
//import goes here
class Detail extends StatefulWidget {
//code goes here
}
class _DetailState extends State<Detail> {
//states goes here
@override
void initState() {
//code goes here
}
getSingleContact() {
//code goes here
}
updateContact(String fullname, int phonenumber) {
//code goes here
}
deleteContact() {
//code goes here
}
@override
Widget build(BuildContext context) {
return _isLoading
? const Center(
child: CircularProgressIndicator(
color: Colors.blue,
))
: _isError
? const Center(
child: Text(
'Error getting phone contacts',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
)
: Scaffold(
appBar: AppBar(
title: const Text("Details"),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 30.0),
child: Form(
key: _formKey,
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Fullname',
style: TextStyle(
color: Colors.grey,
fontSize: 14.0,
),
),
const SizedBox(height: 5.0),
TextFormField(
controller: _fullname,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input your fullname';
}
return null;
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20),
hintText: "input name",
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.red),
),
),
keyboardType: TextInputType.text,
maxLines: null,
),
const SizedBox(height: 30.0),
const Text(
'Phone number',
style: TextStyle(
color: Colors.grey,
fontSize: 14.0,
),
),
const SizedBox(height: 5.0),
TextFormField(
controller: _phonenumber,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please input your phone number';
}
return null;
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 20),
hintText: "input phone number",
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.grey),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
const BorderSide(color: Colors.red),
),
),
keyboardType: TextInputType.number,
),
],
),
],
),
const SizedBox(height: 30.0),
SizedBox(
height: 45,
width: double.infinity,
child: TextButton(
onPressed: _isSubmitting
? null
: () {
if (_formKey.currentState!.validate()) {
updateContact(
_fullname.text,
int.parse(_phonenumber.text),
);
}
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Colors.black),
),
child: const Text(
'Update contact',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14.0,
),
),
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _isSubmitting
? null
: () {
deleteContact();
},
backgroundColor: Colors.red,
tooltip: 'Delete',
child: const Icon(Icons.delete),
),
);
}
}
With that done, we restart the application using the code editor or run the command below:
flutter run
Conclusion
This post discussed how to build a fullstack mobile application using MongoDB Data API and Flutter. With the Data API, organizations can quickly create secure and scalable services that can be used across platforms.
These resources may also be helpful:
Posted on March 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.