Using Parse Server (Back4App) as an alternative to Firebase for your Flutter App
Getting Started with Parse using Back4App
Table of contents
- What is Parse?
- How to Set up your App on Back4App
- Setting Up on Back4App
- Building the Mobile App
- Let's write the code
- 1. main.dart
- 2. constants/api_constants.dart
- 4. models/event_object.dart
- 5. models/task.dart
- 6. modules/home/bindings/home_binding.dart
- 7. modules/home/controllers/home_controllers.dart
- 8. modules/home/controllers/home_view.dart
- 9. modules/tasks/controllers/tasks_controller.dart
- 10. modules/tasks/controllers/tasks_view.dart
- 11. routes/pages.dart
- 12. routes/routes.dart
- Conclusion
Firebase is a great backend resource for developers who are looking to setup mobile apps quickly without having to figure or worry much about the backend stuff. It is very easy to setup there and have your app running in minutes in the nosql system provided for you there. But there is a catch in vendor lock in among other stuff you may get annoyed with using that service for long. That is where Parse Server comes in.
What is Parse?
Parse used to be a popular backend developer tool that powered thousands if not hundreds of thousand mobile apps before it was acquired by the tech giant Facebook and its services shut down in 2017. However, the good news is that one can still make use of their SDKs and libraries provided for range of platforms from Android, IOS, flutter, web among many others. All you have to do is simply setup your own server. And if you can't setup your own server then why not use the easy to use Back4App system? There isn't a lot of official documentation for flutter like there is for other platforms like React, Android etc which is why I took it upon myself to write a small article to aid you with preliminary steps in giving your flutter app an open source free to use backend service. In this tutorial we are going to set up our developer account on the Back4App platform and thereafter create a flutter task list app.
How to Set up your App on Back4App
Prerequisites
I want to assume that as you are getting started with Back4App, you have a working knowledge of Flutter, SQL, Rest Api.
You also need to have the following installations in your machine:
Flutter SDK
A Development Environment in either Visual Studio Code, Android Studio
Postman app (optional) to test the api requests and responses you will be getting
Setting Up on Back4App
Sign Up or Sign In on Back4App
Once signed in click “Build a new app” and give a name to your app
You will be taken to the console where by default there are 2 classes Under Database present namely Role and User.
Create a new class named task which will store the data for each Task item.
Proceed to 2 columns namely:
title
andcontent
to the class which will store the actual task.
Building the Mobile App
Create a new Flutter project.
Add the plugins:
fluttertoast
,get
andhttp
to your to yourpubspec.yaml
.In your terminal run
flutter pub get
.In your app create the folders and files in the following format.
lib
├── app
│ ├── constants
│ │ └── api_constants.dart
│ │ └── exports.dart
│ ├── models
│ │ └── event_object.dart
│ │ └── exports.dart
│ │ └── task.dart
│ └── modules
│ └── home
│ ├── bindings
│ └── home_binding.dart
│ ├── controllers
│ └── home_controller.dart
│ ├── views
│ └── home_view.dart
│ └── tasks
│ ├── bindings
│ └── tasks_binding.dart
│ ├── controllers
│ └── tasks_controller.dart
│ ├── views
│ └── tasks_view.dart
│ ├── routes
│ │ └── pages.dart
│ │ └── routes.dart
│ ├── services
│ │ └── exports.dart
│ │ └── http_delete.dart
│ │ └── http_get.dart
│ │ └── http_post.dart
Let's write the code
1. main.dart
Go to your main.dart file and replace the code there with this one.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'app/exports.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
await GetStorage.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: AppConstants.appTitle,
theme: asLightTheme,
debugShowCheckedModeBanner: false,
initialRoute: Pages.initial,
getPages: Pages.routes,
);
}
}
2. constants/api_constants.dart
On the console go to App Settings >> Security & Keys
Copy the Application ID and paste in place of YOUR_PARSE_APPLICATION_ID
Copy the REST API key and paste in place of YOUR_PARSE_REST_API_KEY
class ApiConstants {
static const parseUrl = "https://parseapi.back4app.com/classes/";
static const parseAppID = "YOUR_PARSE_APPLICATION_ID";
static const parseApiKey = "YOUR_PARSE_REST_API_KEY";
static const task = "task";
static const tasks = "tasks";
}
3. constants/exports.dart
- For this files and other files named exports.dart we will be exporting files in that particular directory as you come later to know, this help us in the overall project a great deal
export 'app_constants.dart';
4. models/event_object.dart
Event Object for api calls. This class will be helpful in managing the http requests we will be sending.
class EventObject {
int id;
String response;
EventObject({required this.id, required this.response});
}
5. models/task.dart
This is the model class we will use to decode the data from the http responses
class TaskList {
List<Task>? results;
TaskList({this.results});
TaskList.fromJson(Map<String, dynamic> json) {
if (json['results'] != null) {
results = <Task>[];
json['results'].forEach((v) {
results!.add(Task.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (results != null) {
data['results'] = results!.map((v) => v.toJson()).toList();
}
return data;
}
}
class Task {
String? objectId;
String? title;
String? content;
String? createdAt;
String? updatedAt;
Task(
{this.objectId,
this.title,
this.content,
this.createdAt,
this.updatedAt});
Task.fromJson(Map<String, dynamic> json) {
objectId = json['objectId'];
title = json['title'];
content = json['content'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['objectId'] = objectId;
data['title'] = title;
data['content'] = content;
data['createdAt'] = createdAt;
data['updatedAt'] = updatedAt;
return data;
}
}
6. modules/home/bindings/home_binding.dart
Because of using exports its easier to reference any file/function in the app
import 'package:get/get.dart';
import '../../../exports.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(
() => HomeController(),
);
}
}
7. modules/home/controllers/home_controllers.dart
The controller for the home screen.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import '../../../exports.dart';
/// The controller for the Home screen
class HomeController extends GetxController {
final GetStorage userData = GetStorage();
final ScrollController listScrollController = ScrollController();
int _limit = 20, _limitIncrement = 20;
bool isLoading = false;
late String currentUserId;
Debouncer searchDebouncer = Debouncer(milliseconds: 300);
List<Task>? tasks = [];
@override
void onInit() {
super.onInit();
listScrollController.addListener(scrollListener);
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {}
void scrollListener() {
if (listScrollController.offset >=
listScrollController.position.maxScrollExtent &&
!listScrollController.position.outOfRange) {
_limit += _limitIncrement;
}
}
/// Get the list of tasks
Future<List<Task>?> fetchTasks() async {
bool isConnected = await hasReliableInternetConnectivity();
if (isConnected) {
final EventObject? eventObject = await httpGet(
client: http.Client(),
url: ApiConstants.task,
);
try {
if (eventObject!.id == EventConstants.requestSuccessful) {
final TaskList taskList = TaskList.fromJson(
json.decode(eventObject.response),
);
tasks = taskList.results;
}
} catch (exception) {
tasks = null;
}
} else {
showToast(
text: "You don't seem to have reliable internet connection",
state: ToastStates.error,
);
tasks = null;
}
return tasks;
}
}
8. modules/home/controllers/home_view.dart
The home screen is here
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../exports.dart';
// ignore: must_be_immutable
class HomeView extends StatelessWidget {
final HomeController controller = Get.put(HomeController());
final GetStorage userData = GetStorage();
Size? size;
HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
size = Get.size;
controller.fetchTasks();
return Scaffold(
appBar: AppBar(
title: Text(
AppConstants.homeTitle,
style: appBarTextStyle.copyWith(fontSize: 25),
),
actions: [
InkWell(
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(Icons.search),
),
onTap: () {
showSearch(
context: context,
delegate: SearchDelegater(),
);
},
),
],
),
body: SizedBox(
child: taskList(context),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Get.to(
() => TasksView(),
transition: Transition.rightToLeft,
);
},
backgroundColor: AppColors.primaryColor,
child: const Icon(Icons.add, color: AppColors.white),
),
);
}
Widget taskList(BuildContext context) {
return FutureBuilder<List<Task>?>(
future: controller.fetchTasks(),
builder: (BuildContext context, AsyncSnapshot<List<Task>?> snapshot) {
Widget? widget;
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
return ListView.builder(
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) => buildItem(
context,
snapshot.data![index],
),
itemCount: snapshot.data!.length,
controller: controller.listScrollController,
);
} else {
return const Center(
child: Text("Its emply here, no tasks yet"),
);
}
} else if (snapshot.hasError) {
widget = Container();
} else {
widget = const CircularProgress();
}
return widget;
},
);
}
Widget buildItem(BuildContext context, Task setTask) {
return SizedBox(
height: 50,
child: ListTile(
leading: const Icon(Icons.task),
title: Text(
setTask.title!,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
subtitle: Text(
setTask.content!,
style: const TextStyle(fontSize: 14),
),
onTap: () {
Get.to(
() => TasksView(currentTask: setTask),
transition: Transition.rightToLeft,
);
},
),
);
}
}
9. modules/tasks/controllers/tasks_controller.dart
This is the controller for the tasks screen which is for adding, updating, deleting or basically viewing a single task item
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import '../../../exports.dart';
/// The controller for the Tasks screen
class TasksController extends GetxController {
final GetStorage userData = GetStorage();
Task? task;
bool isLoading = false;
String? taskTitle, taskContent;
TextEditingController? titleController, contentController;
@override
void onInit() {
super.onInit();
titleController = TextEditingController();
contentController = TextEditingController();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
titleController?.dispose();
contentController?.dispose();
}
Future<void> showCurrentTask() async {
titleController!.text = task!.title!;
contentController!.text = task!.content!;
}
// function to validate creds
bool validateInput() {
bool validated = false;
if (titleController!.text.isNotEmpty) {
taskTitle = titleController!.text;
taskContent = contentController!.text;
validated = true;
} else {
validated = false;
}
return validated;
}
/// Save changes for a task be it a new one or simply updating an old one
Future<bool?> saveTask() async {
bool? success;
if (validateInput()) {
isLoading = true;
update();
bool isConnected = await hasReliableInternetConnectivity();
if (isConnected) {
final EventObject? eventObject = await httpPost(
client: http.Client(),
//appending the primary key is where the difference of updating vs new task comes in
url: task != null
? ApiConstants.task
: ApiConstants.task + "/${task!.objectId}",
data: jsonEncode(<String, String>{
'title': taskTitle!,
'content': taskContent!,
}),
);
isLoading = false;
update();
try {
// Give the user the appropriate feedback
switch (eventObject!.id) {
case EventConstants.requestSuccessful:
success = true;
showToast(
text: task != null
? "Task updated successfully"
: "New task saved successfully",
state: ToastStates.success,
);
Get.offAll(() => HomeView());
break;
case EventConstants.requestInvalid:
success = false;
showToast(
text: "Invalid request",
state: ToastStates.error,
);
break;
case EventConstants.requestUnsuccessful:
success = false;
break;
default:
showToast(
text: task != null
? "Updating new task was not successful"
: "Saving new task was not successful",
state: ToastStates.error,
);
success = null;
break;
}
} catch (exception) {
success = null;
}
} else {
showToast(
text: "You don't seem to have reliable internet connection",
state: ToastStates.error,
);
}
}
return success;
}
/// Remove a task from the records
Future<bool?> deleteTask() async {
bool? success;
if (validateInput()) {
isLoading = true;
update();
bool isConnected = await hasReliableInternetConnectivity();
if (isConnected) {
final EventObject? eventObject = await httpDelete(
client: http.Client(),
url: ApiConstants.task + "/${task!.objectId}",
);
isLoading = false;
update();
try {
switch (eventObject!.id) {
// Give the user the appropriate feedback
case EventConstants.requestSuccessful:
success = true;
showToast(
text: "Task deleted successfully",
state: ToastStates.success,
);
Get.offAll(() => HomeView());
break;
case EventConstants.requestInvalid:
success = false;
showToast(
text: "Invalid request",
state: ToastStates.error,
);
break;
case EventConstants.requestUnsuccessful:
success = false;
break;
default:
showToast(
text: "Deleting task was not successful",
state: ToastStates.error,
);
success = null;
break;
}
} catch (exception) {
success = null;
}
} else {
showToast(
text: "You don't seem to have reliable internet connection",
state: ToastStates.error,
);
}
}
return success;
}
Future<void> confirmCancel(BuildContext context) async {
if (validateInput()) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text(
'Just a Minute',
style: appBarTextStyle.copyWith(fontSize: 18),
),
content: Text(
'Are you sure you want to close without saving your changes of the task: ${titleController!.text}?',
style: appBarTextStyle.copyWith(fontSize: 14),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
saveTask();
},
child: const Text("SAVE"),
),
TextButton(
onPressed: () {
Navigator.pop(context);
Get.back();
},
child: const Text("DON'T SAVE"),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL"),
),
],
),
);
} else {
Get.back();
}
}
Future<void> confirmDelete(BuildContext context) async {
if (validateInput()) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text(
'Just a Minute',
style: appBarTextStyle.copyWith(fontSize: 18),
),
content: Text(
'Are you sure you want to delete the task: ${titleController!.text}?',
style: appBarTextStyle.copyWith(fontSize: 14),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
deleteTask();
},
child: const Text("DELETE"),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL"),
),
],
),
);
} else {
Get.back();
}
}
}
10. modules/tasks/controllers/tasks_view.dart
This is the task screen which task
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../exports.dart';
/// Tasks screen after Tasks
// ignore: must_be_immutable
class TasksView extends StatelessWidget {
final TasksController controller = Get.put(TasksController());
final GetStorage userData = GetStorage();
final Task? currentTask;
Size? size;
TasksView({Key? key, this.currentTask}) : super(key: key);
@override
Widget build(BuildContext context) {
size = Get.size;
if (currentTask != null) {
controller.task = currentTask;
controller.showCurrentTask();
}
return Scaffold(
appBar: AppBar(
title: Text(
AppConstants.taskTitle,
style: appBarTextStyle.copyWith(fontSize: 25),
),
actions: [
currentTask != null
? InkWell(
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(Icons.delete),
),
onTap: () => controller.confirmDelete(context),
)
: InkWell(
child: const Padding(
padding: EdgeInsets.all(10),
child: Icon(Icons.clear),
),
onTap: () => controller.confirmCancel(context),
),
],
),
body: SingleChildScrollView(
child: SizedBox(
child: GetBuilder<TasksController>(
builder: (controller) =>
controller.isLoading ? const CircularProgress() : inputForm(),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.saveTask();
},
backgroundColor: AppColors.primaryColor,
child: const Icon(Icons.save, color: AppColors.white),
),
);
}
Widget inputForm() {
return Container(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Column(
children: <Widget>[
FormInput(
iLabel: 'Title',
iController: controller.titleController!,
prefix: const Icon(Icons.text_fields),
iOptions: const <String>[],
),
FormInput(
iLabel: 'Description',
iController: controller.contentController!,
isMultiline: true,
iType: TextInputType.multiline,
iOptions: const <String>[],
),
],
),
);
}
}
11. routes/pages.dart
The pages.dart file is what will help us to provide better navigation on the app using the Getx statement system
import 'package:get/get.dart';
import '../exports.dart';
part 'routes.dart';
/// Pages
class Pages {
static const initial = Routes.home;
static final routes = [
GetPage(
name: Paths.home,
page: () => HomeView(),
binding: HomeBinding(),
),
GetPage(
name: Paths.tasks,
page: () => TasksView(),
binding: TasksBinding(),
),
];
}
12. routes/routes.dart
Basically the constants needed in defining the routes
part of 'pages.dart';
abstract class Routes {
static const splash = Paths.splash;
static const home = Paths.home;
static const tasks = Paths.tasks;
}
abstract class Paths {
static const splash = '/splash';
static const home = '/home';
static const tasks = '/tasks';
}
13. services/http_delete.dart This is the helper file with Future functions that will be using send the delete request when there is need to delete a task from the server
import 'dart:async';
import 'package:http/http.dart' as http;
import '../exports.dart';
Future<EventObject> httpDelete({
required http.Client client,
required String url,
}) async {
final http.Response response;
try {
response = await client.delete(
Uri.parse(ApiConstants.parseUrl + url),
headers: <String, String>{
'X-Parse-Application-Id': ApiConstants.parseAppID,
'X-Parse-REST-API-Key': ApiConstants.parseApiKey,
'Content-Type': 'application/json',
},
);
// ignore: avoid_print
print(
'Request: ${ApiConstants.parseUrl}$url \nStatusCode: ${response.statusCode}\nResponse: ${response.body}');
switch (response.statusCode) {
case 200:
case 201:
return EventObject(
id: EventConstants.requestSuccessful,
response: response.body,
);
case 400:
case 401:
return EventObject(
id: EventConstants.requestUnsuccessful,
response: response.body,
);
default:
throw Exception('Oops! Invalid response');
}
} catch (exception) {
throw Exception('Oops! Invalid response');
}
}
10. services/http_get.dart This is the helper file with Future functions to enable us get data from the server
import 'dart:async';
import 'package:http/http.dart' as http;
import '../exports.dart';
Future<EventObject> httpGet({
required http.Client client,
required String url,
}) async {
final http.Response response;
try {
response = await client.get(
Uri.parse(ApiConstants.parseUrl + url),
headers: <String, String>{
'X-Parse-Application-Id': ApiConstants.parseAppID,
'X-Parse-REST-API-Key': ApiConstants.parseApiKey,
'Content-Type': 'application/json',
},
);
// ignore: avoid_print
print(
'Request: ${ApiConstants.parseUrl}$url \nStatusCode: ${response.statusCode}\nResponse: ${response.body}');
switch (response.statusCode) {
case 200:
case 201:
return EventObject(
id: EventConstants.requestSuccessful,
response: response.body,
);
case 400:
case 401:
return EventObject(
id: EventConstants.requestUnsuccessful,
response: response.body,
);
default:
throw Exception('Oops! Invalid response');
}
} catch (exception) {
throw Exception('Oops! Invalid response');
}
}
14. services/http_post.dart This is the helper file with Future functions to sendus get data to the server
import 'dart:async';
import 'package:http/http.dart' as http;
import '../exports.dart';
/// Login a user
Future<EventObject?> httpPost({
required http.Client client,
required String url,
Object? data,
}) async {
final http.Response response;
try {
response = await client.post(
Uri.parse(ApiConstants.parseUrl + url),
headers: <String, String>{
'X-Parse-Application-Id': ApiConstants.parseAppID,
'X-Parse-REST-API-Key': ApiConstants.parseApiKey,
'Content-Type': 'application/json',
},
body: data,
);
// ignore: avoid_print
print(
'Request: ${ApiConstants.parseUrl}$url \nStatusCode: ${response.statusCode}\nResponse: ${response.body}');
switch (response.statusCode) {
case 200:
case 201:
return EventObject(
id: EventConstants.requestSuccessful,
response: response.body,
);
case 302:
return EventObject(
id: EventConstants.preconditionFailed,
response: EventMessages.preconditionFailed,
);
case 400:
case 401:
case 422:
return EventObject(
id: EventConstants.requestUnsuccessful,
response: response.body,
);
default:
throw Exception('Oops! Invalid response');
}
} catch (exception) {
throw Exception('Oops! Invalid response');
}
}
Conclusion
As we finish our app you can see that it very easy to write your flutter app with absolutely no worry of backend stuff since we have Back4App to back us up. The Api calls are also fun once you know who to use them to manage your database on the server.
The source code for the whole app is available on my Github profile:
Don't forget to star the repo
Don't fail to Follow me here and On
Twitter @JacksiroKe
Linked In Jack Siro
Github @JacksiroKe