We're transitioning Studio from Beta to Early Availability

Hygraph for Fluttering Parrotheads

Let's architect a simple Jimmy Buffett Fan Club app in Flutter, powered by Hygraph.
Adam Smith

Written by Adam Smith

Jun 12, 2024
Hygraph for Fluttering Parrotheads

There’s not a lot of articles online about Jimmy Buffett and production-ready Flutter architecture. Excuse me. Jimmy Buffett GraphQL and production-ready Flutter architecture – common mistake.

We’re here to remedy all that, so buckle up Parrot Heads, let's architect a simple Jimmy Buffett Fan Club app in Flutter, powered by Hygraph, our favorite GraphQL CMS.

The Github repo for this tutorial can be found here.

#Architecture

There is no consensus on the best architecture for Flutter, but we’re going to go with Riverpod architecture today for a few reasons.

1. Scalability and flexibility: Riverpod offers a highly scalable architecture that is well-suited for large and complex applications. It provides a clear separation of concerns, making it easy to manage state across different parts of the app.

2. Performance and testing: Riverpod is designed with performance in mind, reducing unnecessary rebuilds and optimizing resource usage. Additionally, it simplifies the testing process by providing a more predictable and maintainable state management solution. With Riverpod, you can write unit tests for your state logic without relying on Flutter-specific code.

3. 🤌 Developer experience: Riverpod's robust tooling and integration with the Dart ecosystem improve the overall developer experience. It supports features like hot reload, state persistence, and dependency injection out of the box.

Riverpod has proven to be a solid choice in our Flutter apps and has held up well.

#Parrot Head fan club

The app we’re building today is very simple. It will display a list of the best Jimmy Buffett shows for our Parrot Head fan club. You can then click through to get details about the set list and how many fans have voted for this show.

We’ll be building this using Flutter Web but the same codebase is used to create iOS, Android, and desktop apps.

Parrot Head Fan Club

Parrot Head Fan Club preview

#CMS data model

To power this, we need to define the Show model in our Hygraph CMS, which will need things like a City, a Date, a Playlist, etc.

Our Hygraph schema for a Show is seen here.

Parrot Head Fan Club data model in CMS

#Building our app

In our Flutter app, we’re going to model this GraphQL schema using the Freezed package. To get started just add Freezed to your pubspec.yaml. Freezed is typically used with REST APIs, but there’s no reason you can’t use it with GraphQL. It provides all the same great time-saving benefits.

With this simple model file and the Freezed codegen tool:

import 'package:freezed_annotation/freezed_annotation.dart';
part 'show.freezed.dart';
part 'show.g.dart';
@freezed
class Show with _$Show {
const factory Show({
required String id,
required String city,
required String playlist,
required int vote_count,
required String date,
required String image_url,
}) = _Show;
factory Show.fromJson(Map<String, dynamic> json) => _$ShowFromJson(json);
}

We get the following out of the box (AKA we save a lot of typing):

  • a constructor + the properties
  • override toString, operator ==, hashCode
  • a copyWith method to clone the object
  • handling de/serialization

Now, in order to set up Riverpod and the Riverpod generator add the latest versions to your pubspec.yaml and simply wrap your entire app in a ProviderScope

import 'package:flutter/material.dart';
import 'package:graphqlparrot_head_flutter_tutorialtut/my_app.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(ProviderScope(
child: MyApp(),
));
}

On now to setting up our Riverpod Provider.

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:parrot_head_flutter_tutorial/models/show/show.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'shows_list_provider.g.dart';
@riverpod
class ShowsList extends _$ShowsList {
static const String query = """
query FetchShows {
shows() {
playlist
vote_count
date
city
stage
id
image_url
}
}
""";
final HttpLink httpLink = HttpLink(
"https://api-us-west-2.hygraph.com/v2/clwrd5jw5012l07w3ba2yuj4s/master");
@override
Future<List<Show>> build() async {
final client = GraphQLClient(
link: httpLink,
cache: GraphQLCache(),
);
final result = await client.query(QueryOptions(document: gql(query)));
final showsJSON = result.data!['shows'] as List<dynamic>;
return showsJSON.map((showJSON) => Show.fromJson(showJSON)).toList();
}
}

Here we’re setting up the Provider where we define how to fetch and manage the list of Shows using GraphQL. Let's break down the key parts of this setup:

We start by defining the GraphQL query that will fetch the list of shows from our Hygraph server. This query retrieves various details about each show, such as the playlist, vote count, date, city, ID, and image URL.

static const String query = """
query FetchShows {
shows() {
playlist
vote_count
date
city
id
image_url
}
}
""";

Next, we create an HttpLink to specify the endpoint of our Hygraph API. This will be used by the GraphQLClient to send requests to Hygraph. We can then instantiate a GraphQLClient which will be responsible for making the actual network requests.

final HttpLink httpLink = HttpLink(
"https://api-us-west-2.hygraph.com/v2/clwrd5jw5012l07w3ba2yuj4s/master");
final client = GraphQLClient(
link: httpLink,
cache: GraphQLCache(),
);

In the build method, we use the client to perform the query. The result is parsed from JSON into a list of Show objects.

Here are some key benefits of using Riverpod in this context:

  • Caching: Reduces network requests by storing fetched data in memory, allowing for reuse across different parts of the app.

  • Scalability: Manages complex state efficiently, making it easy to scale your app and maintain a clean architecture.

  • Consistency: Ensures uniform state management across the app, making the codebase more predictable and maintainable.

  • Performance: Minimizes unnecessary rebuilds, leading to a smoother user experience.

@override
Future<List<Show>> build() async {
final result = await client.query(QueryOptions(document: gql(query)));
final showsJSON = result.data!['shows'] as List<dynamic>;
return showsJSON.map((showJSON) => Show.fromJson(showJSON)).toList();
}

Now in the UI, the ShowsScreen widget listens to the showsListProvider using ref.watch. This provider returns an AsyncValue<List>, which represents the state of the asynchronous operation.

The when method is used to handle the different states of the AsyncValue:

data: Displays a list of shows using a ListView.builder. loading: Shows a CircularProgressIndicator while the data is being fetched. error: Displays an error message if there was an issue fetching the data.

import 'package:flutter/material.dart';
import 'package:parrot_head_flutter_tutorial/features/shows/providers/shows_list_provider.dart';
import 'package:parrot_head_flutter_tutorial/features/show/ui/show_screen.dart';
import 'package:parrot_head_flutter_tutorial/models/show/show.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ShowsScreen extends ConsumerWidget {
ShowsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<Show>> showsRepo = ref.watch(showsListProvider);
return showsRepo.when(
data: (List<Show> shows) {
return Scaffold(
appBar: AppBar(
title: const Text("Best of Jimmy"),
),
body: ListView.builder(
itemCount: shows.length,
itemBuilder: (BuildContext context, int index) {
final Show show = shows[index];
return ListTile(
title: Text(show.city),
subtitle: Text(show.date),
leading: Image.network(show.image_url),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ShowScreen(show: show),
),
);
},
);
},
),
);
},
loading: () => const CircularProgressIndicator(),
error: (Object error, StackTrace? stackTrace) {
return Scaffold(
appBar: AppBar(),
body: Center(child: Text("Error")),
);
},
);
}
}

Each show is displayed in a ListTile with the city name, date, and an image. When a show is tapped, it navigates to a ShowScreen with the details of the selected show. In a production app, we’d use the go_router package, but that’s outside the scope of this article.

#Summary

This guide delved into creating a production-ready Flutter app using Riverpod and Hygraph, focusing on state management and GraphQL integration. Riverpod is highlighted for its scalability, performance optimization, and enhanced developer experience. It ensures a clear separation of concerns, minimizes unnecessary rebuilds, and simplifies testing.

Hygraph is a great fit for easily serving up GraphQL for a Flutter mobile or web application. The powerful schema definition tools, the flexibility in managing content, and the efficient query performance make it an ideal choice for developers.

🎸🍹

Become a Hygraph partner

Discover the benefits of partnering with us and unlock new growth opportunities

Find out more about our partner program

Blog Author

Adam Smith

Adam Smith

Head of Engineering

Adam is the Head of Engineering at Beta Acid and is an MIT-trained technologist with a 20 year track record of successfully delivering digital products. His career has taken him around the world from Paris to Hawaii as a startup founder, product manager, engineering lead and Chief Operating Officer.

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.