Skip to main content

Overview

This integration guide shows you how to how to update your existing project to:
  1. install the ButterCMS package
  2. instantiate ButterCMS
  3. create components to fetch and display each of the three ButterCMS content types: Pages, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.

Installation

Add dependencies to pubspec.yaml:
dependencies:
  flutter:
    sdk: flutter
  buttercms_dart: ^1.0.0
  flutter_html: ^3.0.0-beta.2
  cached_network_image: ^3.3.0
Then run:
flutter pub get

Initialize the client

Create a singleton client to reuse throughout the app:
// lib/services/buttercms.dart
import 'package:buttercms_dart/buttercms_dart.dart';

final butter = ButterCMS(apiKey: 'your_api_token');
Store your API token securely — for example using flutter_dotenv or a secrets manager — and pass it into ButterCMS(apiKey: ...) at startup.

Models

// lib/models/post.dart
class Post {
  final String slug;
  final String title;
  final String body;
  final String summary;
  final String published;
  final String? featuredImage;
  final Author author;
  final List<Category> categories;
  final List<Tag> tags;

  Post({
    required this.slug,
    required this.title,
    required this.body,
    required this.summary,
    required this.published,
    this.featuredImage,
    required this.author,
    required this.categories,
    required this.tags,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      slug: json['slug'],
      title: json['title'],
      body: json['body'],
      summary: json['summary'],
      published: json['published'],
      featuredImage: json['featured_image'],
      author: Author.fromJson(json['author']),
      categories: (json['categories'] as List?)
          ?.map((c) => Category.fromJson(c))
          .toList() ?? [],
      tags: (json['tags'] as List?)
          ?.map((t) => Tag.fromJson(t))
          .toList() ?? [],
    );
  }
}

class Author {
  final String firstName;
  final String lastName;
  final String? email;
  final String? bio;

  Author({required this.firstName, required this.lastName, this.email, this.bio});

  factory Author.fromJson(Map<String, dynamic> json) {
    return Author(
      firstName: json['first_name'],
      lastName: json['last_name'],
      email: json['email'],
      bio: json['bio'],
    );
  }
}

class Category {
  final String name;
  final String slug;

  Category({required this.name, required this.slug});

  factory Category.fromJson(Map<String, dynamic> json) {
    return Category(name: json['name'], slug: json['slug']);
  }
}

class Tag {
  final String name;
  final String slug;

  Tag({required this.name, required this.slug});

  factory Tag.fromJson(Map<String, dynamic> json) {
    return Tag(name: json['name'], slug: json['slug']);
  }
}

Pages

// lib/screens/landing_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../services/buttercms.dart';

class LandingPage extends StatefulWidget {
  final String slug;

  const LandingPage({super.key, required this.slug});

  @override
  State<LandingPage> createState() => _LandingPageState();
}

class _LandingPageState extends State<LandingPage> {
  Map<String, dynamic>? _page;
  bool _loading = true;
  String? _error;

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

  Future<void> _loadPage() async {
    try {
      final response = await butter.page.retrieve(
        pageType: 'landing-page',
        pageSlug: widget.slug,
      );
      setState(() {
        _page = response['data'];
        _loading = false;
      });
    } catch (e) {
      setState(() {
        _error = 'Page not found';
        _loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    if (_error != null) {
      return Scaffold(body: Center(child: Text(_error!)));
    }

    final fields = _page!['fields'];

    return Scaffold(
      appBar: AppBar(title: Text(fields['headline'] ?? '')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              fields['headline'] ?? '',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
            const SizedBox(height: 8),
            Text(
              fields['subheadline'] ?? '',
              style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                color: Colors.grey[600],
              ),
            ),
            const SizedBox(height: 16),
            if (fields['hero_image'] != null)
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: CachedNetworkImage(
                  imageUrl: fields['hero_image'],
                  placeholder: (_, __) => const CircularProgressIndicator(),
                ),
              ),
            const SizedBox(height: 16),
            Html(data: fields['body'] ?? ''),
          ],
        ),
      ),
    );
  }
}

Collections

// lib/screens/brands_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../services/buttercms.dart';

class BrandsScreen extends StatefulWidget {
  const BrandsScreen({super.key});

  @override
  State<BrandsScreen> createState() => _BrandsScreenState();
}

class _BrandsScreenState extends State<BrandsScreen> {
  List<Map<String, dynamic>> _brands = [];
  bool _loading = true;

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

  Future<void> _loadBrands() async {
    final response = await butter.content.retrieve(keys: ['brands']);
    setState(() {
      _brands = (response['data']['brands'] as List)
          .cast<Map<String, dynamic>>();
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('Our Brands')),
      body: ListView.builder(
        itemCount: _brands.length,
        itemBuilder: (context, index) {
          final brand = _brands[index];
          return Card(
            margin: const EdgeInsets.all(8),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  if (brand['logo'] != null)
                    CachedNetworkImage(
                      imageUrl: brand['logo'],
                      height: 100,
                      fit: BoxFit.contain,
                    ),
                  const SizedBox(height: 8),
                  Text(
                    brand['name'] ?? '',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  if (brand['description'] != null)
                    Html(data: brand['description']),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

Dynamic components

Component renderer

// lib/widgets/component_renderer.dart
import 'package:flutter/material.dart';
import 'hero_component.dart';
import 'features_component.dart';
import 'cta_component.dart';

class ComponentRenderer extends StatelessWidget {
  final List<Map<String, dynamic>> components;

  const ComponentRenderer({super.key, required this.components});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: components.map((component) {
        final type = component['type'] as String;
        final fields = component['fields'] as Map<String, dynamic>;

        switch (type) {
          case 'hero':
            return HeroComponent(fields: fields);
          case 'features':
            return FeaturesComponent(fields: fields);
          case 'cta':
            return CTAComponent(fields: fields);
          default:
            return const SizedBox.shrink();
        }
      }).toList(),
    );
  }
}

Example component

// lib/widgets/hero_component.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';

class HeroComponent extends StatelessWidget {
  final Map<String, dynamic> fields;

  const HeroComponent({super.key, required this.fields});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          Text(
            fields['headline'] ?? '',
            style: Theme.of(context).textTheme.displaySmall,
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          Text(
            fields['subheadline'] ?? '',
            style: Theme.of(context).textTheme.bodyLarge,
            textAlign: TextAlign.center,
          ),
          if (fields['button_label'] != null) ...[
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                // Handle button tap - navigate or open URL
              },
              child: Text(fields['button_label']),
            ),
          ],
          if (fields['image'] != null) ...[
            const SizedBox(height: 24),
            ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: CachedNetworkImage(
                imageUrl: fields['image'],
                fit: BoxFit.cover,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

Using in screens

// lib/screens/component_page.dart
import 'package:flutter/material.dart';
import '../services/buttercms.dart';
import '../widgets/component_renderer.dart';

class ComponentPage extends StatelessWidget {
  final String slug;

  const ComponentPage({super.key, required this.slug});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<Map<String, dynamic>>(
        future: butter.page.retrieve(
          pageType: 'landing-page',
          pageSlug: slug,
        ),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }

          final page = snapshot.data!['data'];
          final components = (page['fields']['body'] as List?)
              ?.cast<Map<String, dynamic>>() ?? [];

          return SingleChildScrollView(
            child: ComponentRenderer(components: components),
          );
        },
      ),
    );
  }
}

Blog

// lib/screens/blog_list.dart
import 'package:flutter/material.dart';
import '../services/buttercms.dart';
import '../models/post.dart';
import 'blog_post.dart';

class BlogListScreen extends StatefulWidget {
  const BlogListScreen({super.key});

  @override
  State<BlogListScreen> createState() => _BlogListScreenState();
}

class _BlogListScreenState extends State<BlogListScreen> {
  List<Post> _posts = [];
  bool _loading = true;
  int _currentPage = 1;
  bool _hasNextPage = true;

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

  Future<void> _loadPosts() async {
    final response = await butter.post.list(
      params: {'page': '$_currentPage', 'page_size': '10'},
    );
    final postsData = response['data'] as List;
    final meta = response['meta'];

    setState(() {
      _posts = postsData.map((p) => Post.fromJson(p)).toList();
      _hasNextPage = meta['next_page'] != null;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('Blog')),
      body: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) {
          final post = _posts[index];
          return ListTile(
            title: Text(post.title),
            subtitle: Text('By ${post.author.firstName} ${post.author.lastName}'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => BlogPostScreen(slug: post.slug),
                ),
              );
            },
          );
        },
      ),
    );
  }
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'screens/landing_page.dart';
import 'screens/brands_screen.dart';
import 'screens/blog_list.dart';
import 'screens/blog_post.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ButterCMS Flutter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      initialRoute: '/',
      onGenerateRoute: (settings) {
        if (settings.name == '/') {
          return MaterialPageRoute(
            builder: (_) => const LandingPage(slug: 'home'),
          );
        }
        if (settings.name?.startsWith('/page/') ?? false) {
          final slug = settings.name!.replaceFirst('/page/', '');
          return MaterialPageRoute(
            builder: (_) => LandingPage(slug: slug),
          );
        }
        if (settings.name == '/brands') {
          return MaterialPageRoute(builder: (_) => const BrandsScreen());
        }
        if (settings.name == '/blog') {
          return MaterialPageRoute(builder: (_) => const BlogListScreen());
        }
        if (settings.name?.startsWith('/blog/') ?? false) {
          final slug = settings.name!.replaceFirst('/blog/', '');
          return MaterialPageRoute(
            builder: (_) => BlogPostScreen(slug: slug),
          );
        }
        return null;
      },
    );
  }
}

Resources

Dart SDK

Full SDK reference and method documentation

iOS Guide

iOS-specific patterns

Android Guide

Android-specific patterns

Content API

REST API documentation