Overview
This integration guide shows you how to how to update your existing project to:- install the ButterCMS package
- instantiate ButterCMS
- 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 topubspec.yaml:
dependencies:
flutter:
sdk: flutter
buttercms_dart: ^1.0.0
flutter_html: ^3.0.0-beta.2
cached_network_image: ^3.3.0
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
- StatefulWidget
- FutureBuilder
// 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'] ?? ''),
],
),
),
);
}
}
// 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 StatelessWidget {
final String slug;
const LandingPage({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 fields = snapshot.data!['data']['fields'];
return CustomScrollView(
slivers: [
SliverAppBar(
title: Text(fields['headline'] ?? ''),
expandedHeight: fields['hero_image'] != null ? 200 : null,
flexibleSpace: fields['hero_image'] != null
? FlexibleSpaceBar(
background: CachedNetworkImage(
imageUrl: fields['hero_image'],
fit: BoxFit.cover,
),
)
: null,
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Text(
fields['headline'] ?? '',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 8),
Text(fields['subheadline'] ?? ''),
const SizedBox(height: 16),
Html(data: fields['body'] ?? ''),
]),
),
),
],
);
},
),
);
}
}
Collections
- StatefulWidget
- FutureBuilder
// 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']),
],
),
),
);
},
),
);
}
}
// 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 StatelessWidget {
const BrandsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Our Brands')),
body: FutureBuilder<Map<String, dynamic>>(
future: butter.content.retrieve(keys: ['brands']),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final brands = (snapshot.data!['data']['brands'] as List)
.cast<Map<String, dynamic>>();
return ListView.builder(
itemCount: brands.length,
itemBuilder: (context, index) {
final brand = brands[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
leading: brand['logo'] != null
? CachedNetworkImage(
imageUrl: brand['logo'],
width: 50,
height: 50,
)
: null,
title: Text(brand['name'] ?? ''),
),
);
},
);
},
),
);
}
}
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
- Blog Post List
- Single Blog Post
- StatefulWidget
- FutureBuilder
// 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/screens/blog_list.dart
import 'package:flutter/material.dart';
import '../services/buttercms.dart';
import '../models/post.dart';
import 'blog_post.dart';
class BlogListScreen extends StatelessWidget {
const BlogListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Blog')),
body: FutureBuilder<Map<String, dynamic>>(
future: butter.post.list(
params: {'page': '1', 'page_size': '10'},
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final postsData = snapshot.data!['data'] as List;
final posts = postsData.map((p) => Post.fromJson(p)).toList();
return 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/screens/blog_post.dart
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../services/buttercms.dart';
import '../models/post.dart';
class BlogPostScreen extends StatelessWidget {
final String slug;
const BlogPostScreen({super.key, required this.slug});
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<Map<String, dynamic>>(
future: butter.post.retrieve(slug: 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 post = Post.fromJson(snapshot.data!['data']);
final dateFormat = DateFormat.yMMMMd();
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: post.featuredImage != null ? 200 : null,
pinned: true,
flexibleSpace: post.featuredImage != null
? FlexibleSpaceBar(
background: CachedNetworkImage(
imageUrl: post.featuredImage!,
fit: BoxFit.cover,
),
)
: null,
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Text(
post.title,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
'By ${post.author.firstName} ${post.author.lastName} • ${dateFormat.format(DateTime.parse(post.published))}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
Html(data: post.body),
]),
),
),
],
);
},
),
);
}
}
Navigation
// 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