DEV Community

Blazebrain
Blazebrain

Posted on

Integrating FaunaDB with Flutter

Introduction

In building mobile apps, especially with an emphasis on scalability, many factors are considered before the development process starts. One of these factors is the way data is stored. We would at a unique solution to managing data in our application, it's called Fauna. Fauna offers a free, ready to use database that can be setup and ready to go in few minutes(yes, it’s that easy to use). With Fauna, yoiu never need to worry about complex setups or difficult to use queries. They are very relatable and easily understandable. This article will teach you how to integrate and use the Fauna in our Flutter app through a step-by-step procedure.

Integrating with Flutter.

We would be building a Todo list app with Flutter and Fauna. The app would have functionalities including getting all todos, creating new todos, and deleting todos.

First, head over to create the Fauna website and create an account.

After successful signup and login, on the dashboard screen. Click the Create Database button to create a new database. We would name our database “todo”.

After creating the database, click on Security → New Key.

This is to generate a key that the app would use to access the database. It's the link between our app and Fauna. Make sure you copy the link to a secure place as it is only displayed once.

Next, create the collection by clicking the Collections tab at the left. Click the New Collection button and enter the name of the collection. We can call ours “todos”, just like the database it’s in, because we’re only using one collection here.

Next on the list is to create an index. This is a more straightforward and speedy way of sorting through the data in the database without extra stress (yeah! Fauna is sweet). Click the Indexes tab, then the New Index button. For this particular index, we would name it “all_todos” because we want it to do just that: return the complete list of todos we have stored.

With that, we've set up things on the Fauna side, so now it’s on to our codebase.

Create a new flutter project using the “flutter” command

`flutter create faunadb_sample_project`
Enter fullscreen mode Exit fullscreen mode

Once done, go to the pubspec.yaml file and start by adding the dependencies. In the dependencies block, add the following:

  • faunadb_data : This will be the link between our application and the FaunaDB. Using this package, we will be able to perform CRUD operations on our database.
  • stacked: The architectural solution we’ll use in the application.
  • stacked services: A set of ready-made services offered by stacked, which can be customized.
  • intl: For internationalization, number formatting, and some other things.
  • optional : This ensures that our functions don’t return null.

Under dev_dependencies, add

  • build_runner : This give us access to run commands to generate files needed in the app.
  • stacked generator: This enables auto generating files marked with the Stacked Annotations.
    dependencies:
     cupertino_icons: ^1.0.2
     faunadb_data: ^0.0.6
     flutter:
      sdk: flutter
     intl: ^0.17.0
     optional: ^6.1.0+1
     stacked: ^2.2.7
     stacked_services: ^0.8.15
    dev_dependencies:
     build_runner: ^2.1.5
     flutter_lints: ^1.0.0
     flutter_test:
      sdk: flutter
     stacked_generator: ^0.5.6
    flutter:
     uses-material-design: true
Enter fullscreen mode Exit fullscreen mode

We would be using the faunadb_data package to link the client's setup and perform queries on our database.

Next, create a new file named todo.dart. Inside the class, create a class and name the class TodoModel. It has five parameters that make up each Todo. That's the id, todoName, todoContent, completed(status), date.

    class TodoModel extends Entity<TodoModel> {
     final String id;
     final String todoName;
     final String todoContent;
     final bool completed;
     final String date;
     TodoModel(
      this.id,
      this.todoName,
      this.todoContent,
      this.completed,
      this.date,
     );
    }
Enter fullscreen mode Exit fullscreen mode

The class extends the Entity class, which comes from faunadb_data. The fromJson, getId, and model methods act as overrides on the Entity class.

    class TodoModel extends Entity<TodoModel> {
    //The fromJson() method returns a TodoModel object with the key parameters in the map passed in. 
     @override
     TodoModel fromJson(Map<String, dynamic> model) {
      // TODO: implement fromJson
      throw UnimplementedError();
     }
     // The getId() method returns the id making it easier to fetch the id of a particular Todo Object. 
     @override
     String getId() {
      // TODO: implement getId
      throw UnimplementedError();
     }
     // The model() method returns a map with key-value pairs of the parameters for the TodoModel.
     @override
     Map<String, dynamic> model() {
      // TODO: implement model
      throw UnimplementedError();
     }
    }



    class TodoModel extends Entity<TodoModel> {
     final String id;
     final String todoName;
     final String todoContent;
     final bool completed;
     final String date;
     TodoModel(
      this.id,
      this.todoName,
      this.todoContent,
      this.completed,
      this.date,
     );
     @override
     TodoModel fromJson(Map<String, dynamic> model) {
      return TodoModel(
       model['id'] as String,
       model['todoName'] as String,
       model['todoContent'] as String,
       model['status'] as bool,
       model['date'] as String,
      );
     }
     @override
     String getId() {
      return id;
     }
     @override
     Map<String, dynamic> model() {
      Map<String, dynamic> model = {
       'id': id,
       'todoName': todoName,
       'todoContent': todoContent,
       'status': completed,
       'date': date,
      };
      return model;
     }

    // In addition to this, we create two static variables which point to the collection name and the index we created earlier. 
     static String collection() => "todos";
     static String allTodosIndex() => "all_todos";
    }
Enter fullscreen mode Exit fullscreen mode

With that, we have our TodoModel and its overriding methods all set up.

We still need to set up the deserializer, which takes in a map and uses it to construct a TodoModel Object.

    TodoModel getTodoFromJson(Map<String, dynamic> json) {
      return TodoModel(
        json['id'] as String,
        json['todoName'] as String,
        json['todoContent'] as String,
        json['status'] as bool,
        json['date'] as String,
      );
    }
Enter fullscreen mode Exit fullscreen mode

Next thing to set up is the repository class. This call which would be named TodoRepository would extends the FaunaRepository class which comes from the fauna_data class we are using. This class would link us to the database we created in FaunaDB and also the specific index we want to use. We would pass both in the super constructor.

    class TodoRepository extends FaunaRepository<TodoModel> {
     TodoRepository() : super("todos", "all_todos");
    }
Enter fullscreen mode Exit fullscreen mode

Using the super constructor, we pass the name of the collection and the index, the two things needed for the Repository setup.

Moving forward, create a new folder named services; inside this folder, create a file named todo_service.dart with a class named TodoService. This class will be the intermediary between our app and the package we’re using. All the calls we’re making to the database would flow through this class. Methods to read, save, update, delete data from the database would go in here.

    import 'package:faunadb_data/faunadb_data.dart';
    import 'package:optional/optional.dart';
    import 'package:pilots/todo.dart';
    class TodoService {
     TodoRepository todoRepository = TodoRepository();

     //CRUD Operations in Fauna
     /// Create Operation
     saveTodo(TodoModel todo) async {
      await todoRepository.save(todo, getTodoFromJson);
     }
     /// Read Operation 1
     Future<List> getAllTodos() async {
    //The number of results per page would go into the size parameter, we are starting with 20 results per page here
      PaginationOptions po = PaginationOptions(size: Optional.of(20));
      final result = await todoRepository.findAll(po, getTodoFromJson);
      return result.data;
     }
     /// Read Operation 2
     Future<TodoModel> getSingleTodo(String id) async {
      final result = await todoRepository.find(id, getTodoFromJson);
      return result.value;
     }
     /// Update Operation
     updateTodo(TodoModel todo) async {
      await todoRepository.save(todo, getTodoFromJson);
     }
     /// Delete Operation
     deleteTodo(String id) async {
      final result = await todoRepository.remove(id, getTodoFromJson);
      return result.value;
     }
    }
Enter fullscreen mode Exit fullscreen mode

Now that we are done setting up our services and other things needed to interact with our database smooth, let's create a new folder called UI. We’ll have two screens in the app:

  • homeView, which displays the list of todos, and
  • add_todos, which is where we’ll add the details for creating a new todo object.

Inside the UI folder, create two folders named homeView and add_todos respectively.

In the homeView folder, create two new files titled home_view.dart and home_viewmodel.dart. The business logic and functionalities would be in the viewmodel while the UI code would be in the view file.

Also, in the add_todos folder, create two new files also titled add_todos_view.dart and add_todos_viewmodel.dart.

In the add_todo_viewmodel.dart and home_viewmodel.dart files, create a class to extend the BaseViewModel.

    class AddTodoModel extends BaseViewModel {}


    class HomeViewModel extends BaseViewModel{}
Enter fullscreen mode Exit fullscreen mode

In the add_todos_view.dart file, create a Stateless widget and return the ViewModelBuilder.reactive() function from the Stacked package. Remembering I mentioned the viewmodel files would hold the logic for the view files. The ViewModelBuilder.reactive() constructor would serve as a binding between a view file and it’s corresponding viewmodel. That way, a view can access and make use of logic declared in the viewmodel in its view file.

Here is the homeView now:

    class HomeView extends StatelessWidget {
     const HomeView({Key? key}) : super(key: key);
     @override
     Widget build(BuildContext context) {
      return ViewModelBuilder<HomeViewModel>.reactive(
       viewModelBuilder: () => HomeViewModel(),
       onModelReady: (viewModel) => viewModel.setUp(),
       builder: (context, viewModel, child) {
        return Scaffold();
       },
      );
     }
    }
Enter fullscreen mode Exit fullscreen mode

Here’s the addTodoView too:

    class AddTodoView extends StatelessWidget {
     AddTodoView({
      Key? key,
     }) : super(key: key);
     @override
     Widget build(BuildContext context) {
      return ViewModelBuilder<AddTodoModel>.reactive(
       viewModelBuilder: () => AddTodoModel(),
       builder: (context, viewModel, child) {
        return Scaffold();
       },
      );
     }
    }
Enter fullscreen mode Exit fullscreen mode

The next thing is to set up our routes and register the services, making it easier to use them across the entire codebase using the locator. Create a new folder named app. In this folder, create a new file and call it app.dart. To register the services, we’ll make use of the @StackedApp annotation. This gives us access to two parameters, routes and dependencies. In the dependencies block, we’ll register the TodoService and NavigationService. Also, we’ll declare the routes for the pages we’ll use, i.e. the HomeView and the AddTodoView.

    @StackedApp(
     routes: [
      MaterialRoute(page: AddTodoView),
      MaterialRoute(page: HomeView),
     ],
     dependencies: [
      LazySingleton(classType: NavigationService),
      LazySingleton(classType: TodoService),
     ],
    )
    class AppSetup {}
Enter fullscreen mode Exit fullscreen mode

Run the flutter command below to generate the files needed.

`flutter pub run build_runner build --delete-conflicting-outputs`
Enter fullscreen mode Exit fullscreen mode

This command generates the app.locator.dart and app.router.dart file into which our dependencies and routes are registered.

Go to your main.dart file; in the main block, above the runApp(), we need to set up the locator and the DB key that the app would use to access the DB. Recall the key we created earlier while setting up our DB on the Fauna account.

    void main() {
     WidgetsFlutterBinding.ensureInitialized();
     setCurrentUserDbKey("//YOUR-KEY-GOES-HERE");
     setupLocator();
     runApp(MyApp());
    }
Enter fullscreen mode Exit fullscreen mode

Now we are done with all the setups and configurations we need to do. We can start building the UIs and writing the business logic in the ViewModel.

In HomeViewModel, we define methods to get all the todos currently in the database anytime we start the app (read operation on the DB). Another function is to delete a particular Todo. Using the locator, we inject the services and use them to access the methods we've set up in the TodoService class earlier. The list of todos is what the UI screen will use in building the screen. Also, since we want the current list of todos in our DB to show once we start the application, we create a function called runBusyFuture, which tells the ViewModel*, "Hey, I'm currently fetching essential data,* so hold on and display a loading indicator". This function is safer to adjust our UI based on the state, the list of todos.

    class HomeViewModel extends BaseViewModel {
     final _todoService = locator<TodoService>();
     final _navigationService = locator<NavigationService>();
     List<dynamic> todosList = [];

     Future<void> setUp() async {
      await runBusyFuture(getTodos());
     }

     Future<void> getTodos() async {
      todosList = await _todoService.getAllTodos();
     }

     Future<void> deleteTodo(String id) async {
      await _todoService.deleteTodo(id);
      todosList = await _todoService.getAllTodos();
      notifyListeners();
     }

     navigateToAddTodoPage() {
      _navigationService.navigateTo(Routes.addTodoView);
     }
    }
Enter fullscreen mode Exit fullscreen mode

In the addNewTodo ViewModel, we declare the method to save the details of the new Todo and create the Todo; after the Todo gets completed, we route the user to the HomeView, which displays the list of all the Todos.

    class AddTodoModel extends BaseViewModel {
     final service = locator<TodoService>();
     TodoRepository repo = TodoRepository();
     bool status = false;
     NavigationService navigationService = NavigationService();

     Future<void> createTodo(String name, String content) async {
      Optional<String> uniqueId = await repo.nextId();
      String id = uniqueId.value;
      TodoModel newTodo = TodoModel(id, name, content, status, formatDate());
      final result = await service.saveTodo(newTodo);
      navigateToHome();
     }

     String formatDate() {
      DateTime now = DateTime.now();
      String formattedDate = DateFormat('MMMM d' + ', ' + 'y').format(now);
      return formattedDate;
     }

     void navigateToHome() {
      navigationService.navigateTo(Routes.homeView);
     }
    }
Enter fullscreen mode Exit fullscreen mode

Next, to fill the UI screen, we would use a ListViewBuilder to display the elements of the list we get from the database. You can check the gist for the complete code here.

For the addNewTodo, we would be using a Form. At the top of the file, use the @FormView annotation; this gives access to the fields. In the field block, add the fields in the form using the FormTextField and attach the generated mixin to the class.

    class AddTodoView extends StatelessWidget with $AddTodoView {


    @FormView(fields: [
     FormTextField(name: 'todoName'),
     FormTextField(name: 'todoContent'),
    ])
Enter fullscreen mode Exit fullscreen mode

Then run the flutter command to generate the file.

`flutter pub run build_runner build --delete-conflicting-outputs`
Enter fullscreen mode Exit fullscreen mode

It would contain the mixin for managing the controllers, which you would assign to the fields in the Form.

For the todoName FormField:

    TextFormField(
     controller: todoNameController,
     focusNode: todoNameFocusNode,
     decoration: const InputDecoration(
      label: Text('Title'),
      border: OutlineInputBorder(
       borderSide: BorderSide(),
       borderRadius: BorderRadius.all(
        Radius.circular(16.0),
       ),
      ),
     ),
    ),
Enter fullscreen mode Exit fullscreen mode

and for the todoContent FormField:

    TextFormField(
         controller: todoContentController,
         focusNode: todoContentFocusNode,
         decoration: const InputDecoration(
          label: Text('Content'),
          border: OutlineInputBorder(
           borderSide: BorderSide(),
           borderRadius: BorderRadius.all(
            Radius.circular(16.0),
           ),
          ),
         ),
        ),
Enter fullscreen mode Exit fullscreen mode

You can check out the complete code for the UI screen in this gist here.

Save and run the application, and you should see something like this:

You can check out the complete code for the sample app here.

Conclusion

And that's a wrap! We've officially integrated the Fauna into our Flutter app and built a fully functional Todo List Application along the way. Fauna is fast, smooth and easy to use and takes just a few steps to set up and integrate. Check out the official documentation for further study. Keep learning, keep building and definitely, keep using Fauna.

If you have any questions, don't hesitate to reach out to me on Twitter: @Blazebrain or LinkedIn: @Blazebrain.

Cheers!

Written in connection with the Write with Fauna Program.

Discussion (0)