Building a Flutter App on Sitecore Headless

Based on the knowledge gathered in my previous post, we can now start building a Flutter App.

Note: The approach I took to build this Flutter App is not what I would use in any production-like scenario, it is merely an approach I took to demonstrate how we could render Sitecore content in any kind of application. Also, I am by no means an expert in building Mobile Apps. Better yet, this was my first attempt…

First things first, I had to know how to get started with Flutter. The Flutter documentation is pretty extensive and provides a getting started guide with some samples: Getting Started
After I had my “Hello World” App running, I looked through something they call a cookbook: Cookbook
The cookbook is a collection of examples on how to solve different kinds of problems. One that I though was really helpful for my PoC was the Named Routes example, showing me how to build actual pages with routes in Flutter: Named Routes

Based on the Named Routes approach I was able to create a simple screen with some dummy text and navigate to a different screen after clicking on a button.
Using the example from the cookbook recipe, I did some research to see if it was possible to make these routes dynamic instead of having to hardcode the paths in the Router. And it turned out that was possible.
Using something call route generation (a function onGenerateRoute), I could call a different function which could generated a result object. The parameters of the function contained the path of the request, so in case of the homepage this was ‘/’, but it could be any kind of path. This would be helpful when linking it to the Layout Service as the Layout Service also required the path to a page as we looked at earlier.

class App extends MaterialApp {
  App()
      : super(
          initialRoute: '/',
          onGenerateRoute: SitecoreRouter.generateRoute,
        );
}

App.dart

class SitecoreRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    return MaterialPageRoute(
        builder: (_) => [build the response object here]);
  }
}

Router.dart

The RouteSettings object in this generateRoute function contains the path of the request, which we can use to do our Layout Service request to Sitecore.
I then started building out a Page object to return to the view. This object would have to trigger the Layout Service call and render different Flutter Widgets based on the response JSON.

class SitecoreLayoutServiceClient {
  Future<Response> requestLayout(String path) async {
    String apiUrl = "https://" +
        AppSettings.sitecoreCMHostname +
        "/sitecore/api/layout/render/jss";
    return Requests.get(apiUrl,
        persistCookies: true,
        queryParameters: <String, String>{
          'sc_site': AppSettings.sitecoreSite,
          'sc_apikey': AppSettings.sitecoreApiKey,
          'item': path,
          'sc_lang': AppSettings.sitecoreLanguage
        });
  }
}

Layoutservice.dart

class _DefaultPageState extends State<DefaultPage> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      builder: (context, AsyncSnapshot<Response> response) {
        if (response.hasData) {
          var widgetJson = response.data!.json();
          var contextJson = widgetJson["sitecore"]["context"];
          var routeJson = widgetJson["sitecore"]["route"];
          return Scaffold(
            appBar: AppBar(
              title: Text(routeJson["displayName"].toString()),
            ),
            drawer: MaterialDrawer(
                currentPage: routeJson["displayName"].toString()),
            body: SingleChildScrollView(
              child: SitecorePlaceholder(routeJson["placeholders"]["main"]),
            ),
          );
        } else {
          return Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
      },
      future: _getWidget(),
    );
  }
  Future<Response> _getWidget() async {
    return await SitecoreLayoutServiceClient().requestLayout(widget.path);
  }
}

Default_page.dart

Using this approach I was able to return a page with the page’s Display Name as title. When navigating to different pages I would get to see the different Display Names of my Sitecore items.

After I could see this work, I looked around on the internet to find people who did something similar, because the next part was a bit tricky if you didn’t really knew what you were doing (like myself).
It was nice to see the Display Name in the Mobile App, but that doesn’t mean the PoC was actually done. The next part that had to happen was to actually show the renderings and their content on the screen, meaning I would have to loop through all components in a placeholder and determine which Flutter Widget to render based on some field. And to make it more complicated, I wanted to get it working with multiple levels of placeholders.

I am not going to write all that I found and did to make this happen, instead I am sharing the entire PoC repository on GitHub: [Sitecore Headless Flutter](https://github.com/GuidovTricht/SitecoreHeadlessFlutter)
The way it works is that there is a SitecoreWidgetRegistry(sitecore_widget_registry.dart), containing string - Widget builder classes combinations which I would trigger when rendering a rendering from any placeholder.

final _internalBuilders = <String, SitecoreWidgetBuilderContainer>{
    SitecoreHeroBannerBuilder.type: SitecoreWidgetBuilderContainer(
        builder: SitecoreHeroBannerBuilder.fromDynamic),
    SitecorePromoContainerBuilder.type: SitecoreWidgetBuilderContainer(
        builder: SitecorePromoContainerBuilder.fromDynamic),
    SitecorePromoCardBuilder.type: SitecoreWidgetBuilderContainer(
        builder: SitecorePromoCardBuilder.fromDynamic),
    SitecoreSectionHeaderBuilder.type: SitecoreWidgetBuilderContainer(
        builder: SitecoreSectionHeaderBuilder.fromDynamic),
    SitecoreFooterBuilder.type: SitecoreWidgetBuilderContainer(
        builder: SitecoreFooterBuilder.fromDynamic),
  };

The registry would figure out which builder class to use (like sitecore_hero_banner_builder.dart), and the builder class would then return a Widget object to render on the screen.

/// Returns the builder for the requested [type].  This will first search the
/// registered custom builders, then if no builder is found, this will then
/// search the library provided builders.
///
/// If no builder is registered for the given [type] then this will throw an
/// [Exception].
SitecoreWidgetBuilderBuilder getWidgetBuilder(String type) {
  var container = _customBuilders[type] ?? _internalBuilders[type];
  if (container == null) {
    return PlaceholderBuilder.fromDynamic;
  }
  var builder = container.builder;
  return builder;
}

The Widget builder would receive part of the Layout Service JSON response to render the Widget including the content as retrieved from Sitecore.

class SitecoreHeroBannerBuilder extends SitecoreWidgetBuilder {
  SitecoreHeroBannerBuilder({
    this.image,
    this.imageUrl,
    this.title,
    this.subtitle,
  }) : super(numSupportedChildren: kNumSupportedChildren);
  static const kNumSupportedChildren = 0;
  static const type = 'HeroBanner';
  final dynamic image;
  final String? imageUrl;
  final String? title;
  final String? subtitle;
  static SitecoreHeroBannerBuilder? fromDynamic(
    dynamic map, {
    SitecoreWidgetRegistry? registry,
  }) {
    SitecoreHeroBannerBuilder? result;
    if (map != null) {
      result = SitecoreHeroBannerBuilder(
        image: map["Image"],
        title: map["Title"]["value"],
        subtitle: map["Subtitle"]["value"],
      );
    }
    return result;
  }
  @override
  Widget buildCustom({
    ChildWidgetBuilder? childBuilder,
    required BuildContext context,
    required SitecoreWidgetData data,
    Key? key,
  }) {
    return Container(
        height: 150,
        width: MediaQuery.of(context).size.width,
        child: Stack(
          children: [
            Image(image: NetworkImage(image["value"]["src"])),
            Center(
              child: Column(
                children: [
                  Text(
                    title!.toUpperCase(),
                    style: GoogleFonts.ibmPlexMono(
                        color: Colors.white,
                        fontSize: 30,
                        fontWeight: FontWeight.w500),
                  ),
                  Text(
                    subtitle!.toUpperCase(),
                    style: GoogleFonts.ibmPlexMono(
                        color: Colors.white,
                        fontSize: 15,
                        fontWeight: FontWeight.w500),
                  )
                ],
              ),
            )
          ],
        ));
  }
}

This is all for now, but do let me know if you have any questions through Twitter, LinkedIn or Sitecore Slack.