How I built an Android App on Sitecore XM Cloud in under 4 Hours
written by Steve Sobenko
|May 2025
The Idea
Whenever we discuss the benefits of Headless CMS platforms, like Sitecore XM Cloud, to clients, we drop that big buzzword of omni-channel delivery. We've all heard it, but do we follow through with it?
We all know the strategy gurus dropping those catchphrases, "...by orchestrating modular content, we guide users through omnichannel personalized customer touchpoints on our total digital experience AI powered framework pew pew pew". I'd find myself repeating that "omni-channel delivery" part over and over in strategy sessions, pitches and technical road mapping. Something always bothered me. We always built around only web. I knew that building with other devices in mind was actually a win theme for headless. So...
The Goal
How quickly could I take my existing Sitecore XM Cloud website and make a native Android app out of it? Turns out... less time than I thought.
4 hours to be exact.
Now, there's a lot of setup I had ahead of time that obviously doesn't go into the 4 hours such as already having development environments, SDKs, dependencies, tooling already installed.
For this adventure, I chose Sitecore XM Cloud as my Headless CMS and Flutter as my Development UI toolkit to build for Android.
Why Flutter?
Flutter is a UI toolkit from Google that allows you to build natively compiled applications for mobile, web, and desktop from a single codebase.
For us at Nishtech, that opens the door to:
- Quick prototyping of POCs for both iOS and Android
- Single Codebase of reusing content across multiple devices and OS
- Supporting beyond mobile apps, but also kiosk and embedded devices
- Learning Dart is faster than both Swift (iOS) and Kotlin/Java (Android)
And with Sitecore XM Cloud offering layout as data via GraphQL, we don’t need a rendering engine tied to .NET or React. Any client that can speak GraphQL can consume content.
The Approach - A Step-by-Step Build (Hour by Hour)
Start the clock.
Hour 1: Project Setup and Connect to Sitecore XM Cloud
Created a new Flutter project:
flutter create xmcloud_app
Added dependencies to support both GraphQL and rendering styles coming back as HTML:
dependencies:
graphql_flutter: ^5.1.2
flutter_html: ^3.0.0
Connect to Sitecore XM Cloud GraphQL Edge endpoint and render the response:
final HttpLink httpLink = HttpLink(
'https://<your-endpoint>.sitecorecloud.io/sitecore/api/graph/edge',
defaultHeaders: {'sc_apikey': '<your-api-key>'},
);
final client = GraphQLClient(
link: httpLink,
cache: GraphQLCache(),
);
I just want to dump the raw response to the screen to confirm for now, so I use a Query Widget on my HomeScreen
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql/client.dart';
import '../graphql/queries.dart'; // assume you move query there
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final client = getGraphQLClient();
return GraphQLProvider(
client: ValueNotifier(client),
child: Query(
options: QueryOptions(document: gql(homePageQuery)),
builder: (result, {fetchMore, refetch}) {
if (result.isLoading) return CircularProgressIndicator();
if (result.hasException) return Text(result.exception.toString());
final renderedHtml = result.data?['layout']['item']['rendered'];
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Text(renderedHtml), // ideally use a Flutter HTML renderer
);
},
),
);
}
}
Success. In less than an hour, I was able to connect to my Preview GraphQL Endpoint and pull back some raw response.

Hour 2: Query a Site Root Item in XM Cloud, Parse the Data, Do Something With it?
Now that I have a connection to XM Cloud and getting raw JSON, it's time to do something with that.
I'll need to parse the response and map each Component Name. I'll render dynamic components based on componentName. I decided to put everything into a big switch statement to start, as I parsed various components, I'd render them appropriately using some basic widgets in Flutter.
Widget build(BuildContext context) {
Widget build(BuildContext context) {
final type = component['componentName'];
final fields = component['fields'] ?? {};
switch (type) {
case 'RichText':
final htmlValue = fields['Text']?['value'] ?? '';
return Html(data: htmlValue);
case 'Image':
final image = fields['Image']?['value'];
final imageUrl = image?['src'];
if (imageUrl == null) return SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CachedNetworkImage(imageUrl: imageUrl),
);
case 'Promo':
final text = fields['PromoText']?['value'] ?? '';
final img = fields['PromoIcon']?['value'] ?? {};
final imageUrl = img['src'];
return Card(
///... additional components
Nothing fancy to see here, just parsing the values our and rendering them into basic Widgets
Hour 3: The big switch statement got too big
Once I got to placeholders, I realized I needed to break this out to be more modular. At this point I decided that every component rendering needed a 1:1 match so I could render my placeholders and components in the same order on how the context item came back.
switch (component['componentName']) {
case 'RichText':
return RichTextComponent(html: fields['Text']?['value'] ?? '');
case 'Promo':
return PromoComponent(
htmlContent: '${fields['PromoText']?['value'] ?? ''}<p>${fields['PromoText2']?['value'] ?? ''}
</p>',
imageUrl: fields['PromoIcon']?['value']?['src'] ?? '',
);
// ... additional components
}
Built reusable widgets: RichTextComponent
, PromoComponent
, ImageComponent
and a few others
In Sitecore, some components, like Containers or Partial Designs, include nested content areas defined as “placeholders,” each containing an array of components. This Flutter logic detects whether a component includes placeholders, then loops through each one and renders its child components recursively using the same ComponentRenderer. Each placeholder region is wrapped in a styled Container to provide visual separation, spacing, and padding. This approach allows the app to support arbitrarily deep component nesting and match the dynamic structure defined in the CMS without hardcoding any layout logic.
// Handle nested placeholders
final placeholders = component['placeholders'];
if (placeholders != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: placeholders.entries.map((entry) {
final components = entry.value as List;
return Container(
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: components.map((nestedComponent) {
return ComponentRenderer(
component: Map<string,>.from(nestedComponent),
);
}).toList(),
),
);
}).toList(),
);
}
Hour 4: Time to make things pretty
I decided to start playing around with a few things for extra credit on my POC
Added an AppTheme to mimic CSS Styles from the XM Cloud App
class AppTheme {
static const Color dark = Color(0xFF000000);
static const Color light = Color(0xFFF7F7F7);
static const Color primary = Color(0xFF0050EF);
static const double borderRadius = 8.0;
static const double gutter = 16.0;
static ThemeData get theme {
return ThemeData(
fontFamily: 'Roboto',
primaryColor: primary,
scaffoldBackgroundColor: light,
textTheme: const TextTheme(
displayLarge: TextStyle(
// same as old `headline1`
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.black,
),
bodyLarge: TextStyle(
// replaces `bodyText2`
fontSize: 16,
height: 1.5,
color: Colors.black,
),
),
);
}
}
Changed my container code to add some shadow boxes
Added some code in to render HTML as HTML
The final result, in android looked like this

My Final Code Structure
lib/
|-- main.dart # Entry point
|-- screens/
| |-- home_screen.dart # Loads layout and renders route tree
|-- components/
| |-- component_renderer.dart # Dynamic component resolution
| |-- promo_component.dart # Renders image + rich text
| |-- rich_text_component.dart # Renders HTML
| |-- image_component.dart # Image with size/error handling
| |-- container_component.dart # Placeholder UI
| |-- xyz.dart # ...other SXA components
|-- theme/
| |-- app_theme.dart # ThemeData
| |-- colors.dart # Brand colors
What I learned
I changed my mind about my intent of my POC by the end.
Mirroring the website experience felt pointless.
My initial goal was a vision of a native mobile app in Android / iOS that mirrored a web experience. As I moved from SXA component to SXA component in my conversion, I started to question why?
I was taking robust HTML markup from my NextJS components and attempting to recreate them pixel for pixel in native mobile. For what purpose? The mobile experience was just fine, so why create a native app as an exact mirror? This usecase isn't immediately apparent to me and now I'm maintaining 2 component libraries in different code bases. This required a lot of simplification.
XM Cloud Pages is geared towards a WYSIWYG drag-and-drop web experience. While the fields and content can still be managed either in that experience or content editor, you won't see this other HEAD application for the native app pop up in a live-preview out of the box is Sitecore.
This also meant that anyone making updates in Sitecore, inherently knew that there was a website being updated, obviously. There is no way to know about that omni-channel delivery and that there was another consumer of these data objects, a mobile app.
If I were to apply this in a real-world enterprise context, I’d start with a clear architectural pattern that acknowledges content is no longer just for one channel.
Some suggestions
1. A completely different Site node for the mobile app that exclusively feeds content to the app. That doesn't really promote shared data sources of publish once, view everywhere though does it? Alternatively...
2. Annotate Fields with descriptions to indicate distribution channel for used in "iOS App" or "Kiosk" so authors know they are editing content and should preview it in more than just web and WYSIWYG.
In the end, I think this opens up a lot of possibilities. Curious to hear your feedback or thoughts or how others have used Sitecore XM Cloud for more than just their website backend.