Skip to main content
Mediacurrent logo
Hero Background Image

Blog Post

Decoupled Drupal with Flutter: A Technical Setup, Demo and Walkthrough

by Mediacurrent Team
October 8, 2021

This blog post will provide an in-depth look at a Flutter mobile application powered by existing Drupal 8 content. We will walk through ways Drupal 8 and Flutter can work together and create a companion app extension of your Drupal website.

If you’re looking for the benefits of Flutter and how it can help extend your existing Drupal website with a native application, see our previous blog post.

What is Flutter?

Flutter is a cross-platform mobile application framework created by Google, with its first stable release in 2018. It competes with other frameworks like Facebook’s React Native, Microsoft’s Xamarin, and Apple’s Swift and SwiftUI. Flutter is a declarative framework written with the Dart programming language, which is comparable to Javascript in syntax. It’s an open source framework that is free to use and promotes community contributions into the code repository.

“Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.” Source: https://flutter.dev/

There was a talk, Decoupled Drupal with Flutter, presented at Florida Drupal Camp 2020 Mediacurrent's Senior Director of Development, Mark Shropshire.

Benefits

One of the main benefits of Flutter over native platforms, like Swift for iOS and Java/Kotlin for Android, is the ability to compile native applications from a single codebase. While it’s already in progress, but not yet production ready, there will be support in the future for web and desktop applications that can also be driven from the same, single codebase.

Another benefit of Flutter is that there is a large developer community that drives the project and provides contributions back to it in the form of writing documentation, performing code changes, and knowledge sharing of technical challenges. There is well-written API documentation, getting started guides, code samples and snippets, and other resources available. Get involved today in the Flutter community!

The final benefit I’ll mention is that Flutter is flexible and highly customizable. It is similar to Drupal in that it’s easy to get started, but when you need to extend and customize it for your own use cases, instead of reinventing the wheel for everything, there are pre-built packages and code snippets to get you going. In Drupal, there are contributed modules on Drupal.org. In React Native, there are packages on Node Package Manager (npm). In Flutter, there are Dart and Flutter packages.

The focus of this blog is mostly on the technical details of how Drupal and Flutter can work together, but see more benefits of Flutter in our companion blog post.

Drupal

Installation and Setup

DDev-Local Local Environment

The quickest way to get up and running with a Drupal 8 site with DDEV-Local is to run through the Drupal 8 Quickstart list of steps.

Demo Content

We get a few content types out of the box with the Standard installation profile, but there isn’t any content yet. Let’s add some Article nodes.

For this demo application, we also adjusted the Body field to disallow HTML input, since HTML isn’t helpful in a Flutter application.

Note: By default, HTML can be added to the Body field. HTML is not helpful in a Flutter mobile application. It’s wise to disallow HTML input on the Body field, or you may find yourself having to do some work parsing and stripping out HTML in the Flutter code.

The end goal for this demo Flutter app is to display a list view of Articles provided by Drupal.

Exposing Drupal Data

In order for the Flutter application to know anything about Drupal, we need to enable a way for Drupal to provide its data for consumption by external systems. We can do this in a couple of ways that are included in the latest versions of Drupal core.

JSON API

JSON API is included in Drupal core (since Drupal 8.7), but if you’re still on an older version of Drupal core (which you shouldn’t be because they aren’t receiving security coverage anymore!), then you need to still use the contrib JSON API project.

JSON API is pretty much ready to work once the corresponding module is enabled. By default, JSON API only allows the reading of the Drupal data, but it is possible to enable CRUD (Create, Read, Update, Delete) operations, which would allow the app to both read/to and write/from the Drupal database.

There is also a JSON:API Flutter package available to help with the communication between Drupal JSON API and Flutter. This package was used in the Contenta Flutter demonstration in this Florida Drupal Camp 2020 talk on Decoupled Drupal with Flutter.

We can see our Drupal articles in the JSON API response at this endpoint: http://d8.ddev.site:8000/jsonapi/node/article

Drupal articles in the JSON API response

RESTful Web Services / Custom REST Resources

RESTful Web Services is another module that’s provided by Drupal core. It is very easy to enable and it just works.

There’s a good overview in this blog post comparing Drupal’s core REST resources and custom REST resources and explains some reasons why you may need to go with a more custom solution for your use case.

But there are cases when this would not be a workable solution.

  1. The client has predefined endpoints that must be matched. This is common on new builds when clients have previously built applications and processes that rely on the endpoints provided by Drupal to match whatever is already in place. In those instances, adding the _format=json parameter isn’t an option.
  2. The client relies on more complex permissions.

But, let us add a #3 to that list, which is the client has predefined data structures that must be matched. In this case, using custom Normalizer classes allow us to manipulate the data structures to better fit the data models of the external system:

We will focus on exposing Drupal data via JSON API for the purposes of this blog post. Next up, we need to get our demo Flutter app running.

Flutter

Installation and Setup

Now that we have the Drupal 8 installation running and exposing data via JSON, we now need to set up our Flutter development environment to get an application running: https://flutter.dev/docs/get-started/install

After installation, you can create a new project that can be used as the basis for your own project: https://flutter.dev/docs/get-started/test-drive

Setup data model

Create data model for Drupal content

Since the goal of the Flutter application is to display a list of Articles, we need to first make Flutter aware of the structure for a single Article. The listing of Articles will just be a collection of single Articles.

Create an article_model.dart file in your project.

class Article {
 final String id;
 final String name;
 final String description;
 
 Article({this.id, this.name, this.description});
}

This should look very familiar if you’ve used the object-oriented design pattern before, this is a typical class declaration with initializer.

We’re not yet hooked up to the API to read data, but we can make use of the data model already by creating new Article instances right inside of Flutter. This will simulate what will happen when we read the data from the API and integrate it with our data model.

Article article1 = Article(id: "1", name: "First Article", description: "This is the first article.");
Article article2 = Article(id: "2", name: "Second Article", description: "This is the second article.");

But, this is really just for a quick test. We don’t just want to create articles on the fly like this manually. We want the articles to come from Drupal dynamically from reading the JSON API source and creating the data objects.

Read Drupal Data into Flutter

In Flutter, we can make use of the Drupal data to populate widgets and allow for user interaction and manipulation, although we’ll just be listing the articles on the screen. But first, we need to read in the data from Drupal and instantiate new Article instances from the results.

Parse and convert JSON to data model objects

To begin, we need to provide a way in Flutter to fetch and parse the Drupal JSON.

Future<List<Article>> fetchArticles(http.Client client) async {
 final response = await client.get('http://d8.ddev.site:8000/jsonapi/node/article');
 
 return parseArticles(response.body);
}

The `fetchArticles()` method will return a Future List of Articles.

“Future is a core Dart class for working with async operations. A Future object represents a potential value or error that will be available at some time in the future.” - flutter.dev

The `parseArticles()` method is responsible for generating the List of Articles from the `data` array, which contains the list of article nodes in the JSON response. We care about the `data` array, but not the `jsonapi` object.

List<Article> parseArticles(String responseBody) {
 Map<String, dynamic> json = jsonDecode(responseBody);
 
 List<dynamic> jsonData = json['data'];
 
 return jsonData.map<Article>((json) => Article.fromJson(json)).toList();
}

Next, we need to do some work to convert the data structures of the `data` JSON response over to our Article data structure set in our model class. We can do this with a factory method called `fromJson()`.

class Article {
 final String id;
 final String name;
 final String description;
 
 Article({this.id, this.name, this.description});
 
 factory Article.fromJson(Map<String, dynamic> json) {
   Map<String, dynamic> attributes = json['attributes'];
   Map<String, dynamic> body = attributes['body'];
   return Article(
     id: json['id'] as String,
     name: attributes['title'] as String,
     description: body['value'] as String,
   );
 }
}

As you can see, the `fromJson()` method’s purpose is to parse and convert the JSON data provided by Drupal into the structure set forth in our Article data model. For instance, the `title` key in the JSON response is mapped to the `name` key in Flutter.

Note: There was some extra manipulation needed to handle the JSON API formatting vs. the example we provided earlier. In the `fromJson()` method, we needed to create additional maps to access some deeper structures in the JSON. For instance, the node’s Title values are inside an `attributes` object and the node’s Body values are inside a `body` object within the `attributes`.

In implementation, we will call the `fetchArticles()` method from a FutureBuilder widget that we will set up on our ListView widget which will render the list of articles.

Render the Drupal data in Flutter

We will demonstrate three ways to accomplish the task of loading a list widget populated by Article data from Drupal.

FutureBuilder with simple list view

We create a new Stateless widget to render out the list of articles in a list view called `ArticleListView`. This widget returns a `FutureBuilder`, and within the FutureBuilder, it returns a `ListView.builder()`.

The FutureBuilder provides the list of articles from the API call. Notice we set the `future` property to the `fetchArticles()` method? Once the FutureBuilder has the data returned from the API call, it uses a `ListView.builder()` to list each article. The ListView.builder() loops through each article in the list using a lazy loading technique, which is really beneficial when there are very large sets of data being rendered. The ListView.builder() provides a ListTile widget for each article, where we can set a title and subtitle.

class ArticleListView extends StatelessWidget {
 ArticleListView({Key key}) : super(key: key);
 
 @override
 Widget build(BuildContext context) {
   return FutureBuilder<List<Article>>(
     builder: (context, snapshot) {
       if (snapshot.hasError) print(snapshot.error);
 
       if (snapshot.hasData) {
         return ListView.builder(
           itemCount: snapshot.data.length,
           itemBuilder: (context, index) {
             return ListTile(
               title: Text('${snapshot.data[index].name}'),
               subtitle: Text('${snapshot.data[index].description}'),
             );
           },
         );
       }
       else {
         return Center(child: CircularProgressIndicator());
       }
     },
     future: fetchArticles(http.Client()),
   );
 }
}

Until the FutureBuilder has data to render, it will just display a circular progress indicator, aka spinner to show the user it’s loading.

One thing you’ll notice with using a Future and FutureBuilder as we’ve done so far is that once the data is loaded and displayed the first time, if the data changes in Drupal, Flutter doesn’t know about it. The problem is that the API call only happens once. But what if you wanted to make sure the user is viewing the latest content from Drupal?

There are a few ways to accommodate this, such as user-initiated operations, periodically polling the Drupal API, or using web sockets.

Future/FutureBuilder with Pull-to-refresh

The pull-to-refresh technique is a widely used concept with apps that list lots of content, such as Facebook and Twitter. When the user scrolled down enough where they are seeing older content they might have seen before, they can pull-to-refresh to request fresh content.

For the implementation of the pull-to-refresh technique, we changed the `ArticleListView` widget from a Stateless widget to a StatefulWidget. This allows the widget to track its State, which is important because we set the initial state to the initial list of articles, but then if the user opts to refresh, we need a way to update the list of articles, and when the state changes, the widget will rebuild.

So, now our ArticleListView widget looks like this:

class ArticleListView extends StatefulWidget { 
  ArticleListView({Key key}) : super(key: key);
 
 @override
 _ArticleListViewState createState() => _ArticleListViewState();
}
 
class _ArticleListViewState extends State<ArticleListView> {
 
 Future<List<Article>> _articles = fetchArticles(http.Client());
 
 @override
 Widget build(BuildContext context) {
 
   return FutureBuilder<List<Article>>(
     builder: (context, snapshot) {
       if (snapshot.hasError) print(snapshot.error);
 
       return snapshot.hasData ? RefreshIndicator(
         child: ArticlesList(articles: snapshot.data),
         onRefresh: _refreshhandle,
       ) : Center(child: CircularProgressIndicator());
     },
     future: _articles,
   );
 }
 
 Future<Null> _refreshhandle() async {
   setState(() {
     _articles = fetchArticles(http.Client());
   });
   return null;
 }
}
 
class ArticlesList extends StatelessWidget {
 final List<Article> articles;
 
 ArticlesList({Key key, this.articles}) : super(key: key);
 
 @override
 Widget build(BuildContext context) {
   return ListView.builder(
     itemCount: articles.length,
     itemBuilder: (context, index) {
       return ListTile(
         title: Text('${articles[index].name}'),
         subtitle: Text('${articles[index].description}'),
       );
     },
   );
 }
}

A lot happened here. But mainly the changes are we moved to use a StatefulWidget to manage the state changes and implemented a RefreshIndicator widget. The RefreshIndictor widget provides the ability to pull to refresh, and when that operation happens, it runs the function set in the `onRefresh` property, which is `_refeshHandle()`. Essentially, this pings to the Drupal API again and updates the list of articles in the state, which triggers a rebuild of the ListView.builder(). This provides the user with a refreshed list of articles.

This is a legit solution, but it does put the onus on the user to decide when they wish to request fresh content. Depending on your use case, you may want this to happen more automatically and not require the user to trigger the refresh operation.

Stream/StreamProvider with API Polling

The simplest way to automatically refresh the UI when the API changes is to periodically poll the API. The frequency in which it polls may vary per use case, but in this example, I’ve set it to poll every 5 seconds.

Note: It is very important with a technique like this to take server load and caching into consideration, but works fine in a simple example like this one. For example, it would be more ideal if instead of hitting the JSON API each time, which in turn may query the database, if there’s a way to check if the data even changed, and if not, serve a static file or cached response, but this is outside the scope of this example.

To start with, instead of using a Future, we can use a Stream. The Stream will provide Flutter a continuous stream of data using `Stream.periodic()`. To keep our code organized, we’ve created a `services/data_services.dart` in our codebase. Inside of there, we have this code:

class DataService {
 final Stream<List<Article>> articleStream = Stream.periodic(Duration(seconds: 5)).asyncMap((_) async {
   return await fetchArticles(http.Client());
 });
}

This creates the stream by using a `Stream.periodic()` factory method to perform the API call every 5 seconds.

We can make use of the stream over in our ArticleListView widget using a StreamProvider. We could have also used a StreamBuilder, which would allow us access to the snapshot data like we did back in the FutureBuilder, but the StreamProvider() works fine in this case.

Still using the same StatefulWidget (although technically we could switch it back to a StatelessWidget at this point), we make use of StreamProvider() and Consumer():

class _ArticleListViewState extends State<ArticleListView> {
 
 @override
 Widget build(BuildContext context) {
 
   return StreamProvider<List<Article>>.value(
     value: DataService().articleStream,
     child: Consumer<List<Article>>(
       builder: (context, value, child) {
         if (value != null) {
           return ListView.builder(
             itemCount: value.length,
             itemBuilder: (context, index) => ListTile(
               title: Text(value[index].name),
               subtitle: Text(value[index].description),
             ),
           );
         } else {
           return Center(child: CircularProgressIndicator());
         }
       },
     ),
   );
 }
}

We basically tell the StreamProvider what stream to read from, and when that stream changes, and we use a Consumer to listen to changes from the Provider and rebuild the UI when the data changes.

Stream/StreamProvider with web sockets

If you’re looking for truly real-time data refreshing, similar to Firebase, you’ll likely need to implement web sockets on your server that will emit events when the API changes, so Flutter can subscribe to those events and rebuild its UI components when the data changes. Keep an eye out for a future blog post on this topic!

Honestly, it comes down to your own use cases and what solution makes the most sense and results in the best user experience.

Final thoughts

It’s worth mentioning that we’ve used the Provider and StreamProvider techniques throughout this demonstration. Those are provided via the Provider package on https://pub.dev. It’s really simple to add new packages in a package management way that should feel familiar to npm for React, Cocoapods for Swift, and Composer for Drupal.

I hope this blog post was informational and provided a good overview and walkthrough of setting up a Drupal 8 environment, a Flutter environment, and showing how they can be used to communicate with each other to have a companion native application using the Drupal data source.

Related Insights