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—analyzing 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/
: This is the "brain" of our app. We correctly separated every feature into its own Business Logic Component (BLoC). We have aProductBloc
,CartBloc
,AuthenticationBloc
, and many more. This keeps our logic clean and organized. lib/domain/
: This is the heart of our application, containing all the core business rules.repositories/
: These are our "data managers." They decide whether to fetch data from the internet (remote) or a local database (local). models/
: These are the "blueprints" for our data, defining what aProduct
,Store
, orUser
looks like. We use freezed
here, which is a fantastic tool for preventing bugs.data/
: This contains our "storage rooms"—thelocal_data_source.dart
for connecting to our on-device Hive database and theremote_data_source
(which is implicitly ourCaliberApi
).
lib/network/
: This is our app's "communications department." UsingDio
andRetrofit
incaliber_api.dart
, it handles all the technical details of talking to our WordPress website. lib/screens/
: This is everything the user sees—all our UI code, neatly organized by feature (home, cart, profile, etc.).
This structure is solid. But as we'll see, it led to some challenges that a new approach can solve.
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
), our app had to make many separate network requests:
Ask for banners (using
BannerBlocBloc
).Ask for promos (using
PromoBloc
).Ask for new arrivals and featured products (using
ProductBloc
).Ask for stores (using
StoreBloc
).Ask for blogs (using
BlogsBloc
).
This is what we call a "chatty" API. It works, but it has two major drawbacks:
It’s Slow: Each of those five calls is a separate round trip to our server. The user feels this delay as a slow loading screen.
It’s Brittle: This was our biggest worry. If we changed the Dokan plugin, our
StoreBloc
might break. If we changed our custom banner system, ourBannerBlocBloc
would break. The app was too tightly coupled to the WordPress backend, making it fragile and high-maintenance.
The Solution: Embracing a Headless Approach with GraphQL ๐
We decided to upgrade to a modern architecture: a Headless CMS approach using GraphQL.
Instead of a dozen separate "service windows," GraphQL gives us a single, powerful endpoint. Our Flutter app can now send one detailed request (a "query") and get back all the data it needs for an entire screen in one go.
This solves our two biggest problems:
Speed: One network request is dramatically faster than five.
Durability: Our Flutter app is no longer tied to specific plugin endpoints. It just asks for the data it needs, and the GraphQL server figures out how to get it. If a plugin changes, we only have to update the logic in one place on our server, not in our app.
Our 3-Step Action Plan to Go Headless
Here's how we are refactoring our app, starting with the home screen.
Step 1: Prep Our WordPress Backend
We need to install and activate these free plugins on our WordPress site:
WPGraphQL: The core engine that creates the
/graphql
endpoint.WooGraphQL: An essential add-on that exposes all our WooCommerce products, variations, and cart data.
A GraphQL extension for Dokan: To get our store data, we'll need to find or build a GraphQL extension for the Dokan plugin.
With these active, we can use the built-in GraphiQL IDE in our WordPress dashboard to build and test our queries.
Step 2: Equip Our Flutter App
Next, we update our Flutter project to speak GraphQL.
Add the Package: In
pubspec.yaml
, we addgraphql_flutter
.Configure the Client: In our
dependency_injection.dart
file, we'll set up theGraphQLClient
.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
This is where the magic happens.
First, we define one query to get all the data for our home screen (home_body.dart
):
// This query replaces 5+ separate API 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 } } }
}
}
Next, we replace the 5+ BLoCs for the home screen with a single HomeBloc
. This BLoC will run the query above and emit a single state containing all the data.
Finally, we simplify our home_body.dart
widget. Instead of multiple BlocConsumer
s, we'll have just one.
BEFORE:
// home_body.dart
// ... a BlocConsumer for Products ...
NewArrival(),
// ... a BlocConsumer for Stores ...
CaliberOutlets(),
// ... a BlocConsumer for Blogs ...
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),
],
);
}
}
)
The individual widgets like NewArrivalWidget
become much simpler. They no longer need their own BLoCs; they just receive the data they need to display.
Our Path Forward
This journey to a headless architecture is a significant upgrade. By investing the time to learn and refactor, we are building an app that is not only faster for our users but also dramatically more stable and easier for us to maintain. We're turning a brittle bridge into a modern superhighway, ensuring our app can easily handle whatever updates and changes come its way.