CMS for SPAs: Building Flutter Apps with CrafterCMS 4.0.x

Introduction

Flutter is an open-source software development kit created by Google. Flutter applications are written in the Dart programming language. The framework makes use of many advanced features from the language to build beautiful, natively complied, and multi-platform applications from a single codebase. While developers can build with Flutter on iOS and Android simultaneously without sacrificing quality or performance, we can also build the same experience for the web through the power of Flutter on the web. With the web support, developers can also easily integrate with headless CMS such as CrafterCMS.


CrafterCMS is a modern content management platform, with a Git-based content repository that supports DevOps for content-managed applications. CrafterCMS provides a full-featured editing toolset for content authors combined with comprehensive headless CMS capabilities for developers, and this combination is quite unique. In this tutorial, you will learn how to create a content-rich, Flutter-based web application with in-context editing and other easy to use authoring capabilities.


Let’s get started.

Prerequisites

In this tutorial, you will need:


  • A CrafterCMS Studio 4 stable version (v4.0.0 RC2 or later) running in port 8080 on your local machine

  • A Flutter stable version installed

  • Chrome browser

CrafterCMS projects with Flutter integration

For evaluating purposes, we have created a CrafterCMS project integrated with Flutter. You can easily create a new project with this blueprint and check out how it works.


Here is a sample project with Flutter integration: 

https://github.com/phuongnq/craftercms-example-flutter


Let’s take a look at how to create a new project from this repository.

Demo projects with Flutter


Log in to Studio, from the Projects tab, click Create Project button and select Remote Git Repository.




Note: Project name can be any but inputting “Flutter Sample” makes it easier for the setup. Otherwise, you will need to update the project name in the Flutter application.



Click Review > Create Project.


When the project “Flutter Sample  is created. You should see the following screen:



There are instructions on Readme to follow:


  1. In the CrafterCMS site sandbox directory, you'll find a directory called app, which is the Flutter app. Visit that directory on your terminal and run `flutter pub get`

  2. Update app/lib/constant.dart to have your Crafter Studio base URL and project name. If you named your project flutter-sample and CrafterCMS is running on localhost:8080, no further edits are necessary; otherwise, change the file accordingly.

  3. Run `flutter run -d chrome --web-renderer html --web-port 3000` to start the node server on localhost:3000

  4. Open Site Tools and select "Configuration"

  5. Search for "Proxy Config"

  6. Comment line 58 and uncomment line 59

  7. Close the pop-up and refresh the page. You'll now see the Flutter application in this area.

In file app/lib/constant.dart, you should have the following content:


const String baseUrl = 'http://localhost:8080';

const String apiDescriptor = '/api/1/site/content_store/descriptor.json';

const String siteName = 'flutter-sample';


Follow the steps and run your Flutter application on port 3000:



cd <YOUR_AUTHORING>/data/repos/sites/flutter-sample/sandbox/app
flutter pub get
flutter run -d chrome --web-renderer html --web-port 3000



Update proxy configuration:


Project Tools > Configuration > Proxy Config



Do as these instructions, you should be able to see the following screen while previewing your site in port 8080:


Notes:

  • Make sure that you have Crafter Studio and Flutter application open in different tabs of the same browser instance.

  • If you have a CORS issue while opening the Flutter application in http://localhost:3000, make sure to set the Chrome flag to disable CORS as follows:


  1. Make sure that there is no Flutter application running in port 3000.

  2. Run `flutter doctor -v` to check Flutter SDK location. For example: `/home/pnguyen/snap/flutter/common/flutter`.

  3. Go to Flutter SDK and remove file `bin/cache/flutter_tools.stamp`

  4. Update file `packages/flutter_tools/lib/src/web/chrome.dart` to include tag `--disable-web-security`

  1. Run your Flutter application again with `flutter run -d chrome --web-renderer html --web-port 3000`


While the green pencil is active, click on the whole screen component, you should see a green box appears showing that the text is editable. Click on it and change your content to “Welcome to Flutter on CrafterCMS 4“.


Click on the About tag, you should see the green box as well, click on the Edit button to edit the About model content.



The feature here is In-Context Editing (ICE) of CrafterCMS. ICE enables developers to edit the components’ content while in preview mode. If you open the same site in port 3000, ICE will not be available since it is opened outside of a Studio’s preview frame.

In order to make a cross API call from the application on port 3000 to Studio, we also update Engine Project Configuration to allow origin http://localhost:3000.


How CrafterCMS works with Flutter


CrafterCMS provides craftercms/craftercms-sdk-js and the Experience Builder with built-in libraries to work with CrafterCMS content, including features such as In-Context Editing. The SDK is tested working with Flutter. Refer to the GitHub repository for more information about supported modules and their usages.


Now, let’s take a look at the source code of our Flutter application.



In this application, we additionally include the following components:


  • Two routes:

    • /: for home page

    • /about: for about page


// lib/main.dart

void main() {

 setUrlStrategy(PathUrlStrategy());

 runApp(MaterialApp(

   debugShowCheckedModeBanner: false,

   title: 'Flutter + CrafterCMS Demo',

   initialRoute: '/',

   onGenerateRoute: (RouteSettings settings) {

     final List<String> pathElements = settings.name!.split('/');

 

     if (pathElements[0] != '') {

       return null;

     }

 

     if (pathElements[1] == '' || pathElements[1] == 'home') {

       return MaterialPageRoute<void>(

         settings: settings,

         builder: (BuildContext context) => const MainApp(pageName: 'home'),

       );

     }

 

     if (pathElements[1] == 'about') {

       return MaterialPageRoute<void>(

         settings: settings,

         builder: (BuildContext context) => const MainApp(pageName: 'about'),

       );

     }

     return null;

   },

 ));

}

  • lib/pages/home.dart: Model and data fetching for home page

  • lib/pages/about.dart: Model and data fetching for about page

  • app/web/js/app.js: Enable ICE for each page


Other files and components are just standard Flutter files structure. Check out the official documentation here to learn more about developing a Flutter application.

Data Fetching and Experience Builder

Let’s take a closer look at  how we can fetch and display data from CrafterCMS as well as the usage of Experience Builder feature to build the ICE.


Data Fetching


Due to the fact that Flutter applications are written mainly in Dart language, it is not easy to just use the JavaScript SDK for fetching data from a Crafter project. Alternatively, we will use standard HTTP calls, thanks to CrafterCMS APIs supporting.


Let’s take a look at `lib/pages/about.dart`:


import 'package:flutter/material.dart';

import 'dart:async';

import 'dart:convert';

import 'package:http/http.dart' as http;

 

// ignore: avoid_web_libraries_in_flutter

import 'dart:js' as js;

 

import 'package:app/constant.dart' as craftercms_constants;

 

class AboutModel {

 final String id;

 final String path;

 final String navLabel;

 final String title;

 

 const AboutModel({

   required this.id,

   required this.path,

   required this.navLabel,

   required this.title,

 });

 

 factory AboutModel.fromJson(path, Map<String, dynamic> json) {

   return AboutModel(

     id: json['page']['objectId'],

     path: path,

     navLabel: json['page']['navLabel'],

     title: json['page']['title_s'],

   );

 }

}

 

Future<AboutModel> fetchAboutModel(path) async {

 final String url = craftercms_constants.baseUrl + craftercms_constants.apiDescriptor + '?crafterSite=' + craftercms_constants.siteName + '&flatten=true&url=' + path;

 final response = await http.get(

   Uri.parse(url)

 );

 

 if (response.statusCode == 200) {

   final json = jsonDecode(response.body);

   return AboutModel.fromJson(path, json);

 } else {

   throw Exception('Failed to load model');

 }

}

 

class AboutPage extends StatefulWidget {

 const AboutPage({Key? key}) : super(key: key);

 

 @override

 _AboutPageState createState() => _AboutPageState();

}

 

class _AboutPageState extends State<AboutPage> {

 late Future<AboutModel> _futureModel;

 

 _AboutPageState();

 

 @override

 void initState() {

   super.initState();

 

   const path = '/site/website/about/index.xml';

   _futureModel = fetchAboutModel(path);

   Stream.fromFuture(_futureModel).listen((model) {

     final id = model.id;

     final label = model.navLabel;

     js.context.callMethod('initICE', [path, id, label]);

   });

 }

 

 @override

 Widget build(BuildContext context) {

   return Scaffold(

     body: ListView(

       children: [

         Center(

           child: FutureBuilder<AboutModel>(

             future: _futureModel,

             builder: (context, snapshot) {

               if (snapshot.hasData) {

                 return Column(

                   children: [

                     Text(

                       snapshot.data!.title,

                       style: Theme.of(context).textTheme.headline4

                     ),

                   ],

                 );

               } else if (snapshot.hasError) {

                 return Text("${snapshot.error}");

               }

 

               // By default, show a loading spinner.

               return const CircularProgressIndicator();

             },

           ),

         ),

       ],

     ),

   );

 }

}

 

`AboutPage` widget extends to `StatefulWidget` class. This widget has a model of type `AboutModel`. This model is designed to have similar structure compared with the About page content type from Crafter. In order to fetch the model content, we will use the API Get Descriptor:


HTTP GET /api/1/site/content_store/descriptor.json


This API call is done within `fetchAboutModel` function that return a `Future<AboutModel>`. Learn more about fetching data from the internet with Flutter here.

Experience Builder


In the above sample project, we introduced two pages: A home page with the root route / and an About page with route /about. Each page has been integrated with ICE.



While previewing in Studio, the active green pencil demonstrates ICE (In-Context Editing) has been enabled. You can then click upon the green box to open forms for editing.


To use ICE for a Flutter application, first of all, we have to register the XB library `craftercms-xb.umd.js`, and then define a JavaScript function to initialize it.


// app/web/index.html

<script src="http://localhost:8080/studio/static-assets/scripts/craftercms-xb.umd.js"></script>

<script src="js/app.js"></script>




// app/web/js/app.js

let callback;

 

function initICE(path, id, label) {

 let elm = document.querySelector('flt-glass-pane');

 if (!elm) {

   return;

 }

 const attributes = {

   'data-craftercms-model-id': id,

   'data-craftercms-model-path': path,

   'data-craftercms-label': label

 };

 for (let i = 0; i < Object.keys(attributes).length; i++) {

   const key = Object.keys(attributes)[i];

   const value = attributes[key];

   elm.setAttribute(key, value);

 }

 if (callback) {

   callback.unmount();

 }

 callback = craftercms?.xb?.initExperienceBuilder({ path });

}


Check out this documentation to learn more about how to create XB enabled projects. The basic idea is that we have:


  • Path: the path to the content item (e.g. /site/website/about/index.xml)

  • Model ID (a.k.a. object ID, local ID): the internal GUID that every content object in CrafterCMS has (e.g. 8d7f21fa-5e09-00aa-8340-853b7db302da)

  • Then we call the function `craftercms?.xb?.initExperienceBuilder` to initialize it.


Above `initICE` function implement how we can register an ICE page for Flutter and attach it to the project root element `flt-glass-pane`. We also `unmount` the XB before using it as all ICEs are attached to a same dom with tag `flt-glass-pane`.


We then call this function from Dart source code:


// app/lib/pages/about.dart

@override

 void initState() {

   super.initState();

 

   const path = '/site/website/about/index.xml';

   _futureModel = fetchAboutModel(path);

   Stream.fromFuture(_futureModel).listen((model) {

     final id = model.id;

     final label = model.navLabel;

     js.context.callMethod('initICE', [path, id, label]);

   });

 }


Inspect a page DOM to verify the attributes are attached:



CrafterCMS’ Experience Builder (XB) provides a UI layer on top of your applications that enables authors with in-context editing (ICE) for all the model fields defined in the content types of pages and components. CrafterCMS developers must integrate their applications with XB, essentially telling XB what field of the model each element on the view represents. 


While there is more support with integrated components for JavaScript-based applications such as React, Angular or NuxtJS, it is proved that the XB also works with Flutter web application as in the above example.

Create a new project with Flutter integration

In the previous session, we create a project from a blueprint that has Flutter enabled. So how about working with Flutter from scratch? 


Let's continue this session on how we can create a new project from scratch!


Go back to the Global Menu, Select Projects > Create Project, this time with an Empty blueprint.



Input as follows and click Review > Create Project:


  • Site Name: demo

  • Site ID: demo


Now let’s create our `app` folder with a branch new Flutter application. Thanks to Flutter CLI, creating a new application is straightforward:


cd {YOUR_STUDIO_DIR}/data/repos/sites/demo/sandbox
flutter create app


When completed, go to `app` directory then start Flutter in port 3000 using web render:


flutter run -d chrome --web-renderer html --web-port 3000


If the application is created successfully, you should see the following screen when browsing http://localhost:3000


Update Proxy Configuration from demo project in Studio, you will see the same screen while previewing in port 8080:




As the Flutter web application is up and previewable within Crafter Studio, you can implement more logic without restraint, including the integration with CrafterCMS using CrafterCMS SDK and the Experience Builder as well as using Crafter Engine API. Take the reference from the above flutter-sample project source code on how to do it. 


One last thing you must do when modifying a site outside of Studio is to commit the change:


cd {YOUR_SITE_SANDBOX}
git add app
git commit -m "Add Flutter application to CrafterCMS project"


Conclusion

In this tutorial, we have introduced how Flutter works with CrafterCMS using CrafterCMS SDK and the Experience Builder.


Are you interested in building Flutter applications on a headless CMS platform? Download the open source CrafterCMS project here. Get started by reading about the Crafter Engine APIs. And check out the Crafter Studio APIs to customize your content authoring experience. For commercial support and training, visit https://craftercms.com.