We started with a goal that many e-commerce store owners share: to take our successful WordPress and WooCommerce website and build a beautiful, native mobile app for our customers. We chose Flutter, a powerful framework for building apps on both iOS and Android from a single codebase.
Our initial version was a success—it worked! But we quickly ran into a common challenge: how do we make the app feel fast and smooth, and, most importantly, how do we stop it from breaking every time we update a plugin on our WordPress site?
This is the story of our journey—analysing our app's architecture and discovering the modern, future-proof approach to make it faster, more reliable, and a joy to maintain.
Our Starting Point: A Look at Our Project's DNA
Before we dive into the changes, let's look at the foundation we built. Our app, caliber_app, has a very professional and well-organized structure. Understanding this is key to seeing why we're making these upgrades.
-
lib/blocs/: The "brain" of our app. Each feature has its own Business Logic Component (BLoC):ProductBloc,CartBloc,AuthenticationBloc, and more. This keeps our logic clean and organized. -
lib/domain/: The heart of our application, containing core business rules.repositories/: Data managers that decide whether to fetch data from the internet (remote) or local database.models/: Blueprints for our data (Product,Store,User), built withfreezedto prevent bugs.data/: Our storage rooms — e.g.,local_data_source.dart(Hive database) andremote_data_source(CaliberApi).
-
lib/network/: Communications layer. UsingDioandRetrofitincaliber_api.dartto talk to WordPress. -
lib/screens/: Everything the user sees — UI organized by feature (home, cart, profile, etc.).
The Challenge: The Traditional REST API Approach
Our app was built on the standard WordPress REST API. To load our home screen (home_body.dart), the app had to make many separate network requests:
- Banners (
BannerBlocBloc) - Promos (
PromoBloc) - New arrivals & featured products (
ProductBloc) - Stores (
StoreBloc) - Blogs (
BlogsBloc)
This is a "chatty" API with two major drawbacks:
- Slow: Multiple round trips make loading screens feel sluggish.
- Brittle: Changes in plugins (like Dokan or banners) could break the app, making it fragile and high-maintenance.
The Solution: Embracing a Headless Approach with GraphQL 🚀
We upgraded to a modern Headless CMS approach using GraphQL. Instead of many endpoints, GraphQL gives us one powerful endpoint. Our Flutter app can now fetch all data for a screen in a single query.
This solves:
- Speed: One request is much faster than five.
- Durability: The app isn’t tied to plugin-specific endpoints. If a plugin changes, only the server logic needs updating.
Our 3-Step Action Plan to Go Headless
Step 1: Prep Our WordPress Backend
Install and activate these free plugins:
- WPGraphQL: Creates the
/graphqlendpoint. - WooGraphQL: Exposes WooCommerce products, variations, and cart data.
- Dokan GraphQL Extension: For store data (custom or community-built).
Step 2: Equip Our Flutter App
Update Flutter to speak GraphQL:
- Add
graphql_flutterinpubspec.yaml. - Configure the client in
dependency_injection.dart:
// in dependency_injection.dart
final HttpLink httpLink = HttpLink('https://calibershoes.com/graphql');
final client = GraphQLClient(
link: httpLink,
cache: GraphQLCache(store: HiveStore()),
);
inject.registerLazySingleton<GraphQLClient>(() => client);
Step 3: Rewrite Our Home Screen Logic
One GraphQL query replaces 5+ REST calls:
query GetHomeScreenData {
newArrivals: products(first: 5, where: {orderby: {field: DATE, order: DESC}}) {
nodes { id, name, price, images { nodes { sourceUrl } } }
}
bestOffers: products(first: 5, where: {featured: true}) {
nodes { id, name, price, images { nodes { sourceUrl } } }
}
stores(first: 5) {
nodes { id, storeName, bannerImage }
}
blogs: posts(first: 4) {
nodes { id, title, date, featuredImage { node { sourceUrl } } }
}
}
We then replace multiple BLoCs with a single HomeBloc managing this query, simplifying home_body.dart.
// home_body.dart
NewArrival(),
CaliberOutlets(),
BlogsScreen(),
AFTER:
// home_body.dart
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state.isLoading) return LoadingShimmers();
if (state.hasData) {
return Column(
children: [
NewArrivalWidget(products: state.newArrivals),
CaliberOutletsWidget(stores: state.stores),
BlogsWidget(blogs: state.blogs),
],
);
}
}
)
Our Path Forward
By embracing a headless architecture with GraphQL, we’ve made our app faster, more durable, and easier to maintain. What was once a fragile bridge is now a modern, future-proof superhighway ready for whatever changes WordPress or WooCommerce bring.