Using Parse Server (Back4App) as an alternative to Firebase for your Flutter App

Using Parse Server (Back4App) as an alternative to Firebase for your Flutter App

Getting Started with Parse using Back4App

·

13 min read

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 and content to the class which will store the actual task.

Building the Mobile App

  • Create a new Flutter project.

  • Add the plugins: fluttertoast, get and http to your to your pubspec.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:

Siro's Task App

Don't forget to star the repo


Don't fail to Follow me here and On