Using Fauna as the Data API of a Flutter App

xinnks

James Sinkala

Posted on December 7, 2021

Using Fauna as the Data API of a Flutter App

Unless you are building an offline app or consuming someone else’s API, when developing a mobile app, most times you will have the burden of setting up a database and adding an API layer so you can consume that data.

Of all the options out there, Fauna is among the few that I have personally found myself working with the most due to various advantages it holds over others. It gives me the playground to experiment with things without having to worry much about breaking my app or the bank. I have loved seeing it grow since I first started using it.

In the mobile app environment, especially hybrid app development I have never looked back since I came across Flutter; it has been my go to toolkit when developing for fun or work. One of Flutter’s major advantages is its hot reload, which helps speed up the development process. As a developer, I appreciate that with it, I can see changes to code almost immediately without having to restart the app.

I recently worked on a mobile app with these two technologies and given that project’s complexity and how this stack simplified the work on the development side I was pleased with the results.

In this article, we are going to see how these two technologies simply work together on the making of a small data listing mobile app.

The App we are building - ‘My Movies’

To see the Flutter-Fauna stack in practice we are going to create ‘My Movies’. A simple movie information mobile app made with Flutter and Fauna as its data API.

The ‘My Movies’ app has three screens. A home screen which will be listing all of the movies on our database, a movie details screen which will be showing detailed movie information and a search page where users can search movies by title.

Preparing the database

If you do not have a Fauna account, head over to Fauna’s registration page and create one.

Create a new database and name it my_movies, and inside, add a movies collection in which we’ll be storing the movies data .Afterwards, create a new index and name it all_movies.

CreateIndex({
 name: "all_movies",
 unique: false,
 serialized: true,
 source: Collection(""movies"),
 values: [
  {
   field: ["data", "title"]
  },
  {
   field: ["data", "runtime"]
  },
  {
   field: ["data", "plot"]
  },
  {
   field: ["data", "genre"]
  },
  {
   field: ["data", "poster"]
  },
  {
   field: ["ref", "id"]
  }
 ]
})
Enter fullscreen mode Exit fullscreen mode

Next, create a new Admin key by clicking on Security in the left sidebar, followed by clicking the New key button. Copy the key’s secret displayed on the next page as it is never displayed again, secure it safely.

Creating a new security key in Fauna

Run the Fauna FQL (Fauna Query Language) script found on this gist on the dashboard shell or locally if you have installed Fauna-shell to populate the movies collection with some initial data.

Accessing Fauna's online shell

Setting up the Flutter project

Start by creating a new flutter project.

flutter create my_movies_app
Enter fullscreen mode Exit fullscreen mode

We are going to use an unofficial Fauna package for Flutter flutterdb_http to make FQL queries against our database.

Add the package to the dependencies:

flutter pub add faunadb_http
Enter fullscreen mode Exit fullscreen mode

We’ll use Bloc to separate our business logic from the presentational layer, hence proceed to installing the following packages:

flutter pub add flutter_bloc
Enter fullscreen mode Exit fullscreen mode
flutter pub add bloc
Enter fullscreen mode Exit fullscreen mode
flutter pub add equatable
Enter fullscreen mode Exit fullscreen mode

Let’s proceed with setting up our Movie data model:

/// Movie model

class Movie {
  final String title;
  final double runtime;
  final String plot;
  final String genre;
  final String poster;
  final String refId;
  final double rating;

  Movie(this.title,
      {required this.runtime,
      required this.plot,
      required this.genre,
      required this.poster,
      required this.refId,
      this.rating = 0.0});

  Map<String, Object> toJson() {
    return {
      "title": title,
      "runtime": runtime,
      "plot": plot,
      "genre": genre,
      "poster": poster,
      "refId": refId,
      "rating": rating
    };
  }

  factory Movie.fromJson(Map<String, dynamic> json) => Movie(json["title"],
      runtime: double.parse(json["runtime"].toString()),
      plot: json["plot"],
      genre: json["genre"],
      poster: json["poster"] ??
          'default_movie_poster_url',
      refId: json['ref_id']);
}
Enter fullscreen mode Exit fullscreen mode

The App’s Events and States

Next we’ll create the MoviesEvent class which will work with the bloc layer to define our app’s expected events.
These are the events that we expect to be performed in our app.

/// blocs/movies/event.dart

abstract class MoviesEvent extends Equatable {
  const MoviesEvent();
  @override
  List<Object> get props => [];
}

class HomeScreenInitial extends MoviesEvent {}

class FetchMovies extends MoviesEvent {}

class SearchScreenInitial extends MoviesEvent {}

class SubmitSearch extends MoviesEvent {
  final String searchQuery;
  const SubmitSearch(this.searchQuery);
  @override
  List<Object> get props => [searchQuery];
  @override
  String toString() => 'MoviesLoad { searchQuery: $searchQuery }';
}
Enter fullscreen mode Exit fullscreen mode

Let’s also set up the bloc’s state for all the states we expect the app to have resulting from the above events and data transactions with the data repositories.

/// blocs/movies/state.dart

class MoviesState extends Equatable {
  const MoviesState();
  @override
  List<Object> get props => [];
}

class MoviesStateInitial extends MoviesState {}

class LoadingState extends MoviesState {}

class MoviesFetched extends MoviesState {
  final List<Movie> movies;
  const MoviesFetched([this.movies = const []]);
  @override
  List<Object> get props => [movies];
  @override
  String toString() => "MoviesLoadSuccess {Movies: $movies.toString()}";
}


class MoviesFetchingFailure extends MoviesState {
  final String errorMessage;
  const MoviesFetchingFailure({this.errorMessage = "Experienced some error"});
  @override
  List<Object> get props => [errorMessage];
  @override
  String toString() => 'MoviesFetchingFailure { Movie: $Movie.toString() }';
}

class SearchScreenInitialState extends MoviesState {
  final String message;
  const SearchScreenInitialState(
      {this.message = "Type above to search for movies"});
  @override
  List<Object> get props => [message];
  @override
  String toString() => 'SearchScreenInitialState { message: $message }';
}

class SearchFailure extends MoviesState {
  final String errorMessage;
  const SearchFailure({this.errorMessage = "Experienced some error"});
  @override
  List<Object> get props => [errorMessage];
  @override
  String toString() => 'MoviesFetchingFailure { Movie: $Movie.toString() }';
}

class SearchResultsFetched extends MoviesState {
  final List<Movie> movies;
  const SearchResultsFetched([this.movies = const []]);
  @override
  List<Object> get props => [movies];
  @override
  String toString() => "SearchResultsFetched {Movies: $movies.toString()}";
}
Enter fullscreen mode Exit fullscreen mode

Lets create a router class that’s going to help us navigate around our app.

/// app_router.dart

class AppRouter {
  Route onGenerateRoute(RouteSettings settings) {
    switch (settings.name) {
      case "movie-details":
        return MaterialPageRoute(
            builder: (_) =>
                MovieDetailsScreen(movie: settings.arguments as Movie));
      case "search":
        return MaterialPageRoute(builder: (_) => const SearchScreen());
      default:
        return MaterialPageRoute(builder: (_) => const HomeScreen());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Create an intermediary AppState widget that will be the root node providing the MoviesBloc to the rest of the widget tree and disposing it when not needed.

/// app_state.dart

class AppState extends StatefulWidget {
  const AppState({Key? key}) : super(key: key);
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<AppState> {
  final _appRouter = AppRouter();
  late final MoviesBloc _moviesBloc;
  _AppState()
      : _moviesBloc = MoviesBloc(moviesRepository: MovieDataRepository()),
        super();

  @override
  Widget build(BuildContext context) {
    return BlocProvider.value(
      value: _moviesBloc,
      child: MaterialApp(
        onGenerateRoute: _appRouter.onGenerateRoute,
        debugShowCheckedModeBanner: false,
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
    _moviesBloc.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

Set AppState as the child widget of our root widget MyApp.

/// main.dart

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const AppState();
  }
}
Enter fullscreen mode Exit fullscreen mode

Setting up the data layer

To consume data for our app in a clean and functional manner we’ll set up a data layer that involves a movies provider which will be fetching raw data for us from Fauna and a movies repository which will be transacting data between the provider and bloc. From the bloc, data can be presented to our pages through states.

Start off by configuring the Fauna client that will enable us to send FQL queries to Fauna, place this in it’s own class to prevent code repetition.

/// faunadb_service.dart


class FaunaDBInstance {
  late final FaunaConfig config;
  late final FaunaClient client;

  FaunaDBInstance(String secret) {
    config = FaunaConfig.build(
      secret: secret,
    );
    client = FaunaClient(config);
  }

  FaunaClient faunaDBClient() {
    return client;
  }

  faunaClose() {
    client.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

We follow this up by setting up our data provider FaunaDBMoviesProvider.

/// providers/faunadb_movies_provider.dart

class FaunaDBMoviesProvider {
  late FaunaDBInstance _fauna;
  late FaunaClient _client;

  FaunaDBMoviesProvider() {
    _fauna = FaunaDBInstance('SECRET_KEY');
    _client = _fauna.faunaDBClient();
  }

  Future<dynamic> movies() async {
    final requestQuery = Map_(
        Paginate(Match(Index("all_movies"))),
        Lambda(
            "movieDoc",
            Let(
                {'movie': Var('movieDoc')},
                Obj({
                  'title': Select(0, Var('movie')),
                  'runtime': Select(1, Var('movie')),
                  'plot': Select(2, Var('movie')),
                  'genre': Select(3, Var('movie')),
                  'poster': Select(4, Var('movie')),
                  'ref_id': Select(5, Var('movie')),
                }))));
    try {
      FaunaResponse? response = await _client.query(requestQuery);
      return response;
    } catch (e) {
      rethrow;
    }
  }

  Future<dynamic> searchForMovies(String searchQuery) async {
    final requestQuery = Map_(
        Paginate(Match(Index("all_movies"))),
        Lambda(
            "movieDoc",
            Let(
                {'movie': Var('movieDoc')},
                Obj({
                  'title': Select(0, Var('movie')),
                  'runtime': Select(1, Var('movie')),
                  'plot': Select(2, Var('movie')),
                  'genre': Select(3, Var('movie')),
                  'poster': Select(4, Var('movie')),
                  'ref_id': Select(5, Var('movie')),
                }))));
    try {
      FaunaResponse? response = await _client.query(requestQuery);
      return response;
    } catch (e) {
      rethrow;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Choose any amongst the many possible ways to secure your secret_key, a topic I won’t be getting into here. You should never hardcode it inside your code.

Next we’ll set up a repository MovieDataRepository that will be in charge of supplying data to the MoviesBloc.

With this setup we get the advantage of having multiple providers supplying data to a single repository without having to change much of the App’s core codebase.

/// repositories/faunadb_movies_repository.dart

class MovieDataRepository {
  final FaunaDBMoviesProvider _faunaDBMoviesProvider;

  MovieDataRepository()
      : _faunaDBMoviesProvider = FaunaDBMoviesProvider(),
        super();

  Future<dynamic> movies() async {
    FaunaResponse response = await _faunaDBMoviesProvider.movies();
    if (response.hasErrors) {
      return response.errors.toString();
    } else {
      if (response.asMap()['resource']['data'].length > 0) {
        return List<Movie>.from(
            response.asMap()['resource']['data'].map((item) {
          return Movie.fromJson(item);
        }));
      }
      return [];
    }
  }

  Future<dynamic> searchForMovies(String searchQuery) async {
    FaunaResponse response =
        await _faunaDBMoviesProvider.searchForMovies(searchQuery);
    if (response.hasErrors) {
      return response.errors.toString();
    } else {
      if (response.asMap()['resource']['data'].length > 0) {
        List<Movie> searchresults = [];
        List<Movie> allMovies =
            List<Movie>.from(response.asMap()['resource']['data'].map((item) {
          return Movie.fromJson(item);
        }));
        for (Movie movie in allMovies) {
          if (movie.title.toLowerCase().contains(searchQuery.toLowerCase())) {
            searchresults.add(movie);
          }
        }
        return searchresults;
      }
      return [];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Inside the MovieDataRepository class we see the faunadb_http package at work helping us make native FQL queries just as we would have on any other setting involving Fauna data and FQL queries.

For any function that conflicts with Dart’s, just add an underscore as the function’s suffix. E.g we’d have Map_() instead of Map() so as not to conflict the two.

Finally we set up our MoviesBloc which up to this point has been referenced countlessly.

/// bloc/movies/bloc.dart

class MoviesBloc extends Bloc<MoviesEvent, MoviesState> {
  final MovieDataRepository moviesRepository;
  MoviesBloc({required this.moviesRepository}) : super(MoviesStateInitial());

  @override
  Stream<MoviesState> mapEventToState(MoviesEvent event) async* {
    if (event is MoviesScreenInitial) {
      yield* _mapMoviesStateInitialToState(event);
    }
    if (event is FetchMovies) {
      yield* _mapFetchMoviesToState();
    }
    if (event is SearchScreenInitial) {
      yield const SearchScreenInitialState();
    }
    if (event is SubmitSearch) {
      yield LoadingState();
      yield* _mapSubmitSearchToState(event);
    }
  }

  Stream<MoviesState> _mapMoviesStateInitialToState(
      MoviesScreenInitial event) async* {
    yield LoadingState();
    var movies = await moviesRepository.movies();
    if (movies is! List<Movie>) {
      yield const MoviesFetchingFailure(errorMessage: "Some Error!");
    }
    if (movies is List<Movie>) {
      yield MoviesFetched(movies);
    }
  }

  Stream<MoviesState> _mapFetchMoviesToState() async* {
    var movies = await moviesRepository.movies();
    if (movies is! List<Movie>) {
      yield MoviesFetchingFailure(errorMessage: movies.toString());
    }
    if (movies is List<Movie>) {
      yield MoviesFetched(movies);
    }
  }

  Stream<MoviesState> _mapSubmitSearchToState(SubmitSearch event) async* {
    var movies = await moviesRepository.searchForMovies(event.searchQuery);
    if (movies is! List<Movie>) {
      yield MoviesFetchingFailure(errorMessage: movies.toString());
    }
    if (movies is List<Movie>) {
      yield SearchResultsFetched(movies);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

All that is left is to add our presentational layer to render all the above configuration useful.

The Presentational Layer

On the HomeScreen we use a BlocConsumer that gives us the ability to listen to states and react programmatically to them on top of the builder that enables us to rebuild our widgets.

/// screens/home_screen.dart

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
  @override
  _HomeScreen createState() => _HomeScreen();
}

class _HomeScreen extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    final _moviesBloc = BlocProvider.of<MoviesBloc>(context);
    _moviesBloc.add(MoviesScreenInitial());

    return BlocConsumer<MoviesBloc, MoviesState>(
        listener: (context, state) {},
        builder: (context, state) {
          Widget widgetToDisplay = const SizedBox();
          if (state is LoadingState) {
            widgetToDisplay = Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [
                Expanded(
                  child: Center(
                    child: CircularProgressIndicator(),
                  ),
                )
              ],
            );
          }
          if (state is MoviesFetchingFailure) {
            widgetToDisplay = Column(
              children: [
                Expanded(
                  child: Center(
                    child: Text(state.errorMessage),
                  ),
                )
              ],
            );
          }
          if (state is MoviesFetched) {
            widgetToDisplay = state.movies.isNotEmpty
                ? ListView(
                    children: List<Widget>.from(
                        state.movies.map((movie) => MovieListItem(movie))))
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: const [
                      Expanded(
                        child: Center(
                          child: Text("No Movies in here!"),
                        ),
                      )
                    ],
                  );
          }
          return Scaffold(
            appBar: AppBar(
              title: const Text("My Movies"),
              elevation: .5,
              actions: [
                IconButton(
                    onPressed: () {
                      Navigator.pushNamed(context, "search");
                    },
                    icon: const Icon(
                      Icons.search,
                      color: Colors.white,
                    ))
              ],
            ),
            body: SafeArea(
              child: widgetToDisplay,
            ),
          );
        });
  }
}
Enter fullscreen mode Exit fullscreen mode

We discern the states we receive from MoviesBloc and convey the received states to the screen.

We do the same for the SearchScreen along with adding a form field which the searches will be typed into.

/// screens/search_screen.dart

class SearchScreen extends StatefulWidget {
  const SearchScreen({Key? key}) : super(key: key);
  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  @override
  Widget build(BuildContext searchScreenContext) {
    final _moviesBloc = BlocProvider.of<MoviesBloc>(searchScreenContext);
    _moviesBloc.add(SearchScreenInitial());
    TextEditingController searchQuery = TextEditingController();
    void _onSubmission(String query) {
      _moviesBloc.add(SubmitSearch(query));
    }

    return BlocConsumer<MoviesBloc, MoviesState>(
        listener: (context, state) {},
        builder: (context, state) {
          Widget widgetToDisplay = const Center();
          if (state is SearchScreenInitialState) {
            widgetToDisplay = Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Expanded(
                  child: Center(
                    child: Text(state.message),
                  ),
                )
              ],
            );
          }
          if (state is LoadingState) {
            widgetToDisplay = Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [
                Expanded(
                  child: Center(
                    child: CircularProgressIndicator(),
                  ),
                )
              ],
            );
          }
          if (state is SearchResultsFetched) {
            widgetToDisplay = Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [
                Expanded(
                  child: Center(
                    child: CircularProgressIndicator(),
                  ),
                )
              ],
            );
          }
          if (state is MoviesFetchingFailure) {
            widgetToDisplay = Column(
              children: [
                Expanded(
                  child: Center(
                    child: Text(state.errorMessage),
                  ),
                )
              ],
            );
          }
          if (state is SearchResultsFetched) {
            widgetToDisplay = state.movies.isNotEmpty
                ? ListView(
                    children: List<Widget>.from(
                        state.movies.map((movie) => MovieListItem(movie))))
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: const [
                      Expanded(
                        child: Center(
                          child: Text("No Movies in here with that title!"),
                        ),
                      )
                    ],
                  );
          }

          return Scaffold(
            appBar: AppBar(
              leading: BackButton(
                onPressed: () => Navigator.of(context)
                    .pushNamedAndRemoveUntil("home", (route) => false),
              ),
              title: TextFormField(
                controller: searchQuery,
                onFieldSubmitted: _onSubmission,
                cursorColor: Colors.white,
                decoration: const InputDecoration(
                    hintText: "Search",
                    hintStyle: TextStyle(
                      color: Colors.white,
                    )),
                style: const TextStyle(
                  color: Colors.white,
                ),
              ),
            ),
            body: SafeArea(
              child: widgetToDisplay,
            ),
          );
        });
  }
}
Enter fullscreen mode Exit fullscreen mode

For the movie details screen just as seen on the AppRoute, we are passing RouteSettings to it with the movie data as the arguments parameter.

/// app_router.dart
...
case "movie-details":
        return MaterialPageRoute(
            builder: (_) =>
                MovieDetailsScreen(movie: settings.arguments as Movie));
...
Enter fullscreen mode Exit fullscreen mode

Let’s create the MovieListItem widget used to plot a movie item on the movie lists.

/// components/movie_list_item.dart

class MovieListItem extends StatefulWidget {
  const MovieListItem(this.movie, {Key? key}) : super(key: key);
  final Movie movie;
  @override
  _MovieListItemState createState() => _MovieListItemState();
}

class _MovieListItemState extends State<MovieListItem> {
  @override
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: () => Navigator.pushNamed(context, "movie-details",
            arguments: widget.movie),
        child: ListTile(
          leading: SizedBox(
            width: 40,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(10.0),
              child: Image.network(
                      widget.movie.poster,
                      fit: BoxFit.cover,
                    ),
            ),
          ),
          title: Text(widget.movie.title),
          subtitle: Text(widget.movie.genre),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

And lastly we have our MovieDetailsScreen which will be presenting the movie data passed from the above component.

/// screens/movie_details_screen.dart

class MovieDetailsScreen extends StatefulWidget {
  const MovieDetailsScreen({Key? key, required this.movie}) : super(key: key);
  final Movie movie;

  @override
  _MovieDetailsScreenState createState() => _MovieDetailsScreenState();
}

class _MovieDetailsScreenState extends State<MovieDetailsScreen> {

  @override
  Widget build(BuildContext context) {

    Size deviceConstraints = MediaQuery.of(context).size;
    double topContainerHeight =
        deviceConstraints.height > 600.0 ? deviceConstraints.height / 2 : 300.0;
    double coverWidth = deviceConstraints.width * (2 / 3) - 45;
    double detailsWidth = deviceConstraints.width * (1 / 3) - 45;

    return Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.white,
          elevation: .5,
          leading: const BackButton(color: Colors.black),
        ),
        body: SafeArea(
            child: ListView(
          padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
          children: [
            Container(
                height: topContainerHeight,
                decoration: BoxDecoration(
                    color: Colors.yellow[50],
                    borderRadius: const BorderRadius.all(Radius.circular(30))),
                child: Row(
                  children: [
                    Container(
                      padding: const EdgeInsets.only(right: 10),
                      child: Column(children: [
                        SizedBox(
                          width: coverWidth,
                          child: ClipRRect(
                            clipBehavior: Clip.hardEdge,
                            borderRadius:
                                const BorderRadius.all(Radius.circular(30)),
                            child: Image.network(
                              widget.movie.poster,
                              fit: BoxFit.cover,
                              height: topContainerHeight,
                            ),
                          ),
                        )
                      ]),
                    ),
                    Container(
                      padding: const EdgeInsets.only(left: 10),
                      margin: const EdgeInsets.symmetric(vertical: 10),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Container(
                              height: 80,
                              width: detailsWidth,
                              child: Column(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceEvenly,
                                children: [
                                  const Text("runtime"),
                                  Text(
                                    "${(widget.movie.runtime).toInt()} min",
                                    style: const TextStyle(
                                        fontWeight: FontWeight.bold,
                                        fontSize: 16),
                                  )
                                ],
                              ),
                              decoration: const BoxDecoration(
                                  color: Colors.yellow,
                                  borderRadius:
                                      BorderRadius.all(Radius.circular(20)))),
                          Container(
                              height: 80,
                              width: detailsWidth,
                              child: Column(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceEvenly,
                                children: [
                                  const Text("rating"),
                                  Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: const [
                                      Text(
                                        "8",
                                        style: TextStyle(
                                            fontWeight: FontWeight.bold,
                                            fontSize: 20),
                                      ),
                                      Icon(Icons.star)
                                    ],
                                  )
                                ],
                              ),
                              decoration: const BoxDecoration(
                                  color: Colors.yellow,
                                  borderRadius:
                                      BorderRadius.all(Radius.circular(20)))),
                          Container(
                            height: 80,
                            width: detailsWidth,
                            child: Column(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceEvenly,
                                children: [
                                  const Text("year"),
                                  Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: const [
                                      Text(
                                        "2021",
                                        style: TextStyle(
                                            fontWeight: FontWeight.bold,
                                            fontSize: 20),
                                      )
                                    ],
                                  )
                                ],
                              ),
                            decoration: const BoxDecoration(
                                color: Colors.yellow,
                                borderRadius:
                                    BorderRadius.all(Radius.circular(20))),
                          ),
                        ],
                      ),
                    )
                  ],
                )),
            const SizedBox(
              height: 20,
            ),
            Row(
              children: [
                Expanded(
                  child: Text(
                    widget.movie.title,
                    style: Theme.of(context).textTheme.headline6,
                  ),
                )
              ],
            ),
            Container(
              margin: const EdgeInsets.symmetric(vertical: 10),
              height: 1,
              color: Colors.grey[400],
            ),
            Row(
              children: [
                Expanded(
                  child: Text(
                    "Synopsis",
                    style: Theme.of(context).textTheme.subtitle1,
                  ),
                )
              ],
            ),
            const SizedBox(height: 10),
            Row(
              children: [
                Expanded(
                  child: Text(
                    widget.movie.plot,
                    style: Theme.of(context).textTheme.bodyText1,
                  ),
                )
              ],
            )
          ],
        )));
  }
}
Enter fullscreen mode Exit fullscreen mode

If we have all the above code intact, we’ll have the app functioning as follows when we run it:

My Movies app preview

In summary we have learnt the following from the above example:

  • Consuming FaunaDB data with FQL from a Flutter app by using an unofficial Fauna package for Flutter.
  • Bloc state management of a Flutter app and the advantages it brings from separating logic and responsibilities within the app to maintaining clean code.
  • Setting up a Fauna database, its collections, indexes and keys.
  • Requesting data from Fauna by using its Query Language (FQL).

I believe there is so much in store that this stack can offer, Fauna’s custom attribute-based access control (ABAC) for user authentication being one of them with its expansive support for third party authentication options providing the flexibility on that end.

With the swift scaling capabilities of Fauna and the agility provided in mobile development by Flutter, this stack can possibly be the go to when it comes to creating small to large scale projects on the mobile environment.

Finally I hope that an official support for the Dart language is in the works, or at the least in plan so that developers and the like can explore this stack in building solutions.

I for one will personally keep exploring this combination for a long foreseeable future.

💖 💪 🙅 🚩
xinnks
James Sinkala

Posted on December 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related