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.
#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.
#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';@freezedclass 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';@riverpodclass ShowsList extends _$ShowsList {static const String query = """query FetchShows {shows() {playlistvote_countdatecitystageidimage_url}}""";final HttpLink httpLink = HttpLink("https://api-us-west-2.hygraph.com/v2/clwrd5jw5012l07w3ba2yuj4s/master");@overrideFuture<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() {playlistvote_countdatecityidimage_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.
@overrideFuture<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
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});@overrideWidget 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 programBlog Author
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.