GSD

How to Add a Blog to Your Flutter Application Using ButterCMS

Posted by Sam Victor on September 28, 2023

Blogs are a great medium for transmitting ideas, messages, and other forms of information. As a developer and also a business owner, having one is a great score for product advertising, sharing ideas, and displaying technical skills.

In this tutorial, we’ll be building a blog application with Flutter and ButterCMS. While we'll be using the ButterCMS blog engine for this tutorial, a feature that covers the vast majority of normal use cases for blogs, you can still configure your own custom blog pages using ButterCMS Page Types if you need additional fields or functionalities.

Why use Flutter?

Flutter is a cross-platform UI toolkit that enables applications to communicate directly with platform services and promotes code reuse across platforms like iOS, Android, the web, and desktop.

With its rising popularity in the market, Flutter is known for its simplicity, code reusability, and swift design, making it the best choice for mobile, web, and desktop development.

Why use ButterCMS?

Building a blog from scratch as a front-end developer can be a pain, especially when you have to set up and handle the back-end development process. This can take many months to achieve and is very stressful to accomplish for a single developer. Here, ButterCMS shines.

ButterCMS is a headless CMS, meaning you can use any technology for your site without being restricted in terms of implementation. ButterCMS offers a simple-to-use yet robust dashboard for creating pages, blog posts, and such. Furthermore, all of your data is delivered through API endpoints.

Flutter banner CTA

Tutorial prerequisites

To follow along with this tutorial, you should have the following installed:

  • Flutter SDK: The Flutter SDK is a command-line toolkit used to create and manage Flutter projects. To install the Flutter SDK, go to https://flutter.dev/docs/get-started/install.
  • Code Editor or IDE: This is a text editor for writing in programming languages that are used for building software. 
  • ButterCMS trial account: You can sign up here.

Setting up ButterCMS 

Before we proceed with building our application, let’s start with setting up our CMS. Head over to ButterCMS and click Get Started to sign up. After signing up, fill out the questionnaire in the modal and choose Flutter on the Dashboard.

Pick your stack page in ButterCMS account

After selecting the Flutter stack, you’ll be redirected to the Run Locally page where an API will be generated for you, as seen in the image below:

Run locally page in ButterCMS

Copy and save your API key.

Setting up our workspace

Let’s scaffold our Flutter application and start making some API calls. Run the command below in the terminal to create a Flutter project:

flutter create blog_app  

Once the command above is done running, run the command below to change the directory of the terminal and run the project in an emulator:

cd blog_app && flutter run

Installing dependencies

For this tutorial, we’ll be using some of Flutter’s packages to ace our development. Replace your entire pubspec.yaml file with the block below:

name: blog_app
description: A new Flutter project.

publish_to: "none" # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: ">=2.18.0 <3.0.0"

dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  http: ^0.13.5
  animated_splash_screen: ^1.3.0
  get: ^4.6.5
  flutter_html: ^2.2.1
  url_launcher: ^6.1.5
  intl: ^0.17.0

dev_dependencies:
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

Based on the code above, we’re installing some dependencies. Let’s get to know their uses.

  • http: The http package is a network library for making HTTP requests via API calls.
  • animated_splash_screen: The animated_splash_screen is a fully lightweight plugin for building fully customized splash screens with animations very quickly.
  • get: The get package, famously known as GetX, is a super-efficient and lightweight Flutter solution that efficiently and practically integrates high-performance state management, intelligent dependency injection, and route management. We’ll be using the package’s route management for this tutorial.
  • flutter_html: The flutter_html package is a Flutter widget that renders HTML and CSS content in the widget tree as Flutter widgets.
  • url_launcher:  The url_launcher is used to launch links and URLs from a Flutter application to a browser.
  • intl: The intl package is used for properly formatting message translations and dates/numbers. 

File architecture

Before heading on to development, let’s create Dart files below in our project’s lib/ folder:

📦lib
 ┣ 📜blog_card.dart
 ┣ 📜blog_screen.dart
 ┣ 📜home.dart
 ┣ 📜main.dart
 ┣ 📜secret_key.dart
 ┗ 📜splash_screen.dart

Our lib/ folder contains 6 Dart files, including:

  • blog_card.dart: This contains a reusable card component that will be used throughout the application.
  • blog_screen.dart: This contains the blog details screen with the full article.
  • home.dart: This is the home of our application. It contains all published blog posts.
  • main.dart: This is the entry point for all Flutter applications. It executes the first screen.
  • secret_key: As the name implies, this contains our ButterCMS secret key. This file will be hidden from source control (GitHub).
  • splash_screen.dart:  This contains the app logo and a brief intro animation.

Adding assets image

Create an assets folder in the project’s root directory and download and add this logo to it.

Next, add the code below to the pubspec.yaml file: 

...

flutter:
  uses-material-design: true

  assets:
    - assets/

Building the splash screen

Giving our users a nice intro when they open our application gives the users a nice experience and confidence. Let’s achieve this by heading over to splash_screen.dart and pasting the code below into it.

import 'package:animated_splash_screen/animated_splash_screen.dart';
import 'package:blog_app/home.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:page_transition/page_transition.dart';

class SplashScreen extends StatelessWidget {
  const SplashScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return AnimatedSplashScreen(
      splashIconSize: Get.height * 0.5,
      duration: 3000,
      backgroundColor: Colors.black,
      splash: Image.asset('assets/logo.png'),
      nextScreen: const HomeScreen(),
      splashTransition: SplashTransition.slideTransition,
      pageTransitionType: PageTransitionType.fade,
    );
  }
}

In the code block above, we created a stateless widget and added our AnimatedSplashScreen widget while giving it a splash image, duration, size, and type of animation. We’re also specifying the next screen after the splash screen animation is completed.

Updating main.dart

To see all our current changes, let’s update our main.dart file. Copy and replace the main.dart file with the code block below:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'splash_screen.dart';
  
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
        title: 'ButterCMS Blog',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.teal,
          useMaterial3: true,
        ),
        home: const SplashScreen());
  }
}

In the code block above, we’re wrapping our material app with GetX. We're also setting our home route to the animated splash screen we created earlier. 

Building the blog card component

Before heading over to the Home screen, let’s build our blog card component. This will be used to populate the Home screen later on. Head over to the blog_card.dart file and add the code block below to it: 

import 'package:flutter/material.dart';

class BlogCard extends StatelessWidget {
  const BlogCard(
      {super.key,
      required this.image,
      required this.title,
      required this.desc,
      required this.author,
      required this.authorImg,
      required this.slug,
      required this.press});

  final String image, title, desc, author, authorImg, slug;
  final Function() press;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: press,
        child: Card(
          elevation: 5,
          clipBehavior: Clip.hardEdge,
          margin: const EdgeInsets.only(bottom: 20),
          surfaceTintColor: Colors.white,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Hero(
                tag: slug,
                child: Image.network(image, fit: BoxFit.cover),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 15.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const SizedBox(height: 15),
                    Text(title,
                        style: const TextStyle(
                            fontSize: 20, fontWeight: FontWeight.bold)),
                    const SizedBox(height: 5),
                    Text(desc,
                        style: const TextStyle(
                            fontSize: 16, color: Colors.blueGrey)),
                    const SizedBox(height: 10),
                    ListTile(
                        contentPadding: EdgeInsets.zero,
                        leading: CircleAvatar(
                            radius: 20,
                            backgroundImage: NetworkImage(authorImg)),
                        title: Text(author,
                            style: const TextStyle(
                              fontSize: 14,
                              fontWeight: FontWeight.w600,
                            ))),
                  ],
                ),
              )
            ],
          ),
        ));
  }
}

In the code block above, we created a card using a column, image, title, description, and author’s details in a ListView. We’ve also added some final variables that would serve as props when calling the cards in other components. We also made the cards clickable by wrapping them in GestureDetector.

Hiding the secret key

To prevent malicious attacks, it’s not ideal to expose our secret key to our source control. Head over to the secret_key.dart file and add your API key as seen in the code block below:

const String apiKey = 'YOUR_BUTTERCMS_API_KEY';

Next, add the file path to the .gitignore file to hide the file from Git.

/lib/secret_key.dart

Building the Homescreen

As revealed earlier in the file architecture section, our Home screen will contain our list of blog posts. Head over to the home.dart file:

import 'dart:convert';
import 'package:blog_app/base.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'blog_card.dart';
import 'blog_screen.dart';
import 'secret_key.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List data = [];
  bool isLoading = true;

  Future retrieveBlogPost() async {
    try {
      final url = Uri.parse("${Base.baseUrl}/v2/posts?auth_token=$apiKey");
      final response = await http.get(url);
      if (response.statusCode == 200) {
        var result = jsonDecode(response.body);
        setState(() {
          data = result['data'] as List;
          isLoading = false;
        });
        return result;
      }
    } catch (e) {
      print(e);
    }
  }

  @override
  void initState() {
    super.initState();
    retrieveBlogPost();
  }

  @override
  Widget build(BuildContext context) {
    return isLoading
        ? const Center(child: CircularProgressIndicator())
        : Scaffold(
            appBar: AppBar(title: const Center(child: Text('Home'))),
            body: SingleChildScrollView(
                child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 15.0),
              child: Column(children: [
                const SizedBox(height: 15),
                ...List.generate(
                    data.length,
                    (index) => BlogCard(
                          image: data[index]['featured_image'],
                          title: data[index]['title'],
                          slug: data[index]['slug'],
                          desc: data[index]['summary'],
                          author:
                              "${data[index]['author']['first_name']} ${data[index]['author']['last_name']}",
                          authorImg: data[index]['author']['profile_image'],
                          press: (){}
                        ))
              ]),
            )),
          );
  }
}

In the code block above, we’re making an API call to the ButterCMS API with our apiKey to get all the available blog posts when the screen begins. We’re then pushing the results into our data list.

Next, we’re populating our Home screen using the List.generate function with the results from the data list. This multiplies our blog component according to the specified length given. In this case, we’re using the length of our array. We’re then passing all the properties from our data list to our BlogCard widget using the index as it loops.

Finally, we’re displaying a loading indicator when the data is still fetching from the API and displaying the widgets when it’s completed. 

With our progress so far, let’s restart our application to see what we’ve achieved so far.

A gif of what our blog application looks like so far

Building the BlogScreen

Let us give our users the ability to read any blog post they click on in our application. To achieve this, head over to the BlogScreen.dart file and paste the code below into it:

import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:html/dom.dart' as dom;
import 'package:intl/intl.dart';

class BlogScreen extends StatelessWidget {
  const BlogScreen({super.key, required this.data});
  final Map<String, dynamic> data;

  @override
  Widget build(BuildContext context) {
    DateTime parseDate =
        DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").parse(data['updated']);
    var inputDate = DateTime.parse(parseDate.toString());
    var outputDate = DateFormat('MM/dd/yyyy hh:mm a').format(inputDate);

    return Scaffold(
        appBar: AppBar(
          title: const Text('Blog'),
        ),
        body: SingleChildScrollView(
            child: Column(
          children: [
            Hero(
                tag: data['slug'],
                child: Image(
                    image: NetworkImage(data['featured_image']),
                    fit: BoxFit.cover,
                    width: Get.width)),
            const SizedBox(height: 10),
            Padding(
                padding: const EdgeInsets.symmetric(horizontal: 20.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    ListTile(
                        contentPadding: EdgeInsets.zero,
                        leading: CircleAvatar(
                          backgroundImage:
                              NetworkImage(data['author']['profile_image']),
                        ),
                        title: Text(
                            "${data['author']['first_name']} ${data['author']['last_name']}",
                            style: const TextStyle(
                                // color: Colors.black,
                                fontSize: 18,
                                fontWeight: FontWeight.bold)),
                        subtitle: Text(outputDate,
                            style: TextStyle(
                              color: Colors.grey[800],
                              fontSize: 16,
                            ))),
                    const SizedBox(height: 10),
                    Text(data['title'],
                        style: const TextStyle(
                            fontSize: 35, fontWeight: FontWeight.w900)),
                    const SizedBox(height: 10),
                    Html(
                        data: """
                ${data['body']}
                """,
                        onLinkTap: (String? url,
                            RenderContext context,
                            Map<String, String> attributes,
                            dom.Element? element) async {
                          await _launchURL(url);
                        }),
                  ],
                ))
          ],
        )));
  }

  _launchURL(url) async {
    if (await canLaunchUrlString(url)) {
      await launchUrlString(url, mode: LaunchMode.externalApplication);
    } else {
      throw 'Could not launch $url';
    }
  }
}

In the code block above, we’re populating the Blog screen with the data we’ll be getting from the Home screen. We’re also formatting our updated time using the intl library.

The data['body'] string returns an HTML string that doesn't work, so here is where Flutter_html shines. The Flutter_html package converts the HTML strings to Flutter widgets which are compatible with the widget tree. 

The raw data['body'] string also contains some hyperlinks that might come in handy for users. To make this clickable, we’re using url_launcher to launch the hyperlinks in an external browser application.

Note: ButterCMS shortens hyperlinks, and this confuses the url_launcher. To fix this, head over to the Blog Post section in ButterCMS. 

To fix this URL issue, head over to the Blog Post section and click on any of the hyperlinks in the WYSIWYG editor, and hit the chain link icon in the editor's toolbar.

Click the url icon in the blog engine WYSIWYG editor

This should bring up this link modal as seen in the image below:

Insert link into the pop up

Finally, click on the Save button and accept the changes to correct the link. 

Url prefix confirmation pop up

Once this is done, and once you review the link again, the complete URL should be displayed.

Finally, let’s route our blog post to the Blog screen whenever a user clicks on a post. To achieve this, head over to home.dart and update the press function in the BlogCard component.

...List.generate(
                    data.length,
                    (index) => BlogCard(
                          image: data[index]['featured_image'],
                          title: data[index]['title'],
                          slug: data[index]['slug'],
                          desc: data[index]['summary'],
                          author:
                              "${data[index]['author']['first_name']} ${data[index]['author']['last_name']}",
                          authorImg: data[index]['author']['profile_image'],
                          press: () =>
                              Get.to(() => BlogScreen(data: data[index])), //add this
                        ))

In the code block above, we’re routing our app to BlogScreen whenever a user clicks on a blog card.  We’re also passing the clicked data across to the BlogScreen.

Flutter banner CTA

Creating our blog posts

With our application complete, let’s create our own custom blog post using the ButterCMS blog engine. Click on the Blog Post section from the sidebar of your dashboard and click on New Post located at the top right of the page.

Click "New Post" in the blog engine to create a new blog post

In the WYSIWYG editor, add your blog contents, metadata, and SEO data.

Insert your content and then press publish to publish your blog post

When you're ready to publish your post, click Publish, or Save Draft if you want to save your progress and continue editing later.

With these actions done, let’s restart our application to see what we’ve achieved so far with a new blog post added:

Final Flutter blog home page and individual blog post page.

Closing thoughts

We’ve successfully built our blog app from start to finish in a couple of minutes without the stress of setting up a back-end server and managing our blog posts because ButterCMS has handled the necessary setup for us. 

As we all know, blogs are a fantastic medium for transferring thoughts, messages, and other types of information, and with the simplicity and flexibility of Flutter, which allows us to build cross-platform apps using a single code base, developing and deploying a blog app is simple and quick.

Here is the link to the complete source code on GitHub.

Make sure you receive the freshest Butter product updates and Flutter tutorials.
Sam Victor

Sam Victor is a Full-stack Blockchain, mobile, and web developer who works with languages such as Solidity, Rust, Flutter, and most Javascript languages. He is passionate about innovation, community, developer advocacy, and open-source technologies.

ButterCMS is the #1 rated Headless CMS

G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award

Don’t miss a single post

Get our latest articles, stay updated!