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.

Android App 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: RichTextComponentPromoComponentImageComponent 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

Android App Finished

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.

Headshot of Steve Sobenko

Steve Sobenko

Steve is a seasoned technology professional with over 20 years of experience leading cross-functional teams and delivering enterprise web solutions. With expertise in front-end and back-end development, cloud computing, security, and analytics, he’s been at the forefront of digital transformation since the early days of the web. Steve is passionate about helping clients achieve their business goals through innovative, scalable technology solutions.

X
Cookies help us improve your website experience.
By using our website, you agree to our use of cookies.
Confirm