> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter

> Build content-driven Flutter applications with ButterCMS. Covers setup, fetching pages, collections, components, and blog content for iOS and Android.

## 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](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

## Installation

Add dependencies to `pubspec.yaml`:

```yaml theme={null}
dependencies:
  flutter:
    sdk: flutter
  buttercms_dart: ^1.0.0
  flutter_html: ^3.0.0-beta.2
  cached_network_image: ^3.3.0
```

Then run:

```bash theme={null}
flutter pub get
```

## Initialize the client

Create a singleton client to reuse throughout the app:

```dart theme={null}
// lib/services/buttercms.dart
import 'package:buttercms_dart/buttercms_dart.dart';

final butter = ButterCMS(apiKey: 'your_api_token');
```

<Info>
  Store your API token securely — for example using `flutter_dotenv` or a secrets manager — and pass it into `ButterCMS(apiKey: ...)` at startup.
</Info>

## Models

```dart theme={null}
// 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

<Tabs>
  <Tab title="StatefulWidget">
    ```dart theme={null}
    // 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'] ?? ''),
              ],
            ),
          ),
        );
      }
    }
    ```
  </Tab>

  <Tab title="FutureBuilder">
    ```dart theme={null}
    // 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'] ?? ''),
                      ]),
                    ),
                  ),
                ],
              );
            },
          ),
        );
      }
    }
    ```
  </Tab>
</Tabs>

## Collections

<Tabs>
  <Tab title="StatefulWidget">
    ```dart theme={null}
    // 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']),
                    ],
                  ),
                ),
              );
            },
          ),
        );
      }
    }
    ```
  </Tab>

  <Tab title="FutureBuilder">
    ```dart theme={null}
    // 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'] ?? ''),
                    ),
                  );
                },
              );
            },
          ),
        );
      }
    }
    ```
  </Tab>
</Tabs>

## Dynamic components

### Component renderer

```dart theme={null}
// 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

```dart theme={null}
// 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

```dart theme={null}
// 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

<Tabs>
  <Tab title="Blog Post List">
    <Tabs>
      <Tab title="StatefulWidget">
        ```dart theme={null}
        // 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),
                        ),
                      );
                    },
                  );
                },
              ),
            );
          }
        }
        ```
      </Tab>

      <Tab title="FutureBuilder">
        ```dart theme={null}
        // 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),
                            ),
                          );
                        },
                      );
                    },
                  );
                },
              ),
            );
          }
        }
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Single Blog Post">
    ```dart theme={null}
    // 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),
                      ]),
                    ),
                  ),
                ],
              );
            },
          ),
        );
      }
    }
    ```
  </Tab>
</Tabs>

## Navigation

```dart theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Dart SDK" icon="mobile" href="../sdks/dart-sdk">
    Full SDK reference and method documentation
  </Card>

  <Card title="iOS Guide" icon="apple" href="./swift">
    iOS-specific patterns
  </Card>

  <Card title="Android Guide" icon="android" href="./android">
    Android-specific patterns
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
