8/6 livestream: AI-Powered Figma to Storybook in Minutes

Announcing Visual Copilot - Figma to production in half the time

Builder logo
builder.io
Contact sales

8/6 livestream: AI-Powered Figma to Storybook in Minutes

Announcing Visual Copilot - Figma to production in half the time

Builder logo
builder.io

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

Create a data model in Builder and serve the same data to both Next.js and Flutter applications. Builder data models deliver JSON through the Content API, providing data across multiple platforms and frameworks.

This tutorial shows how to create a Data model to store location data and serve the information across web and mobile apps.

To get the most out of this tutorial, you need:

  • A Builder account and public API key.
  • A running Next.js and Flutter apps.
  • Basic understanding of REST APIs and JSON.
  • Familiarity with Builder Data models and the Data tab.

The video below shows the entire process of creating a Data model in Builder, creating Content entries, and serving the same data across both Next.js and Flutter apps:

Create a Data model to store location entries:

  1. Go to Models and click + Create Model.
  2. Choose Data from the model type options.
  3. In the Name field, add a name for the model.
  4. In the Description field, add a short description of the model.
  5. Click Create.

Add fields to structure the location data:

  1. Go to the Fields tab within your Data model.
  2. Add a name field with type Text.
  3. Click Save.

Repeat the above steps to create additional fields according to the use case and select the relevant Type from the list.

Use the new Data model to create Content entries:

  1. Go to Content and click + New Entry.
  2. Select your model from the available options.
  3. Give your entry a descriptive name. For example, New York.
  4. Fill in the form fields and click Publish.

Repeat the process for as many entries as needed.

Install the Builder React SDK:

npm install @builder.io/sdk-react

Add your API key to .env.local:

NEXT_PUBLIC_BUILDER_API_KEY=your_builder_api_key_here

To load images from Builder's CDN in your application, configure Next.js to allow images from Builder's domain.

Update your next.config.ts to access Builder images:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.builder.io",
      },
    ],
  },
};

export default nextConfig;

The example below shows how to combine static content with dynamic data from Builder. You fetch model data and make the data available to content entries in the Visual Editor.

Create app/stores/page.tsx:

import { fetchEntries, fetchOneEntry } from "@builder.io/sdk-react";
import { Content } from "@builder.io/sdk-react";
import styles from "./stores.module.css";

export default async function Stores() {
  
  // Fetch store data from your Builder Data model
  const storeData = await fetchEntries({
    apiKey: process.env.NEXT_PUBLIC_BUILDER_API_KEY!,
    model: /* YOUR BUILDER MODEL NAME */,
  });

  // Fetch page content from Builder like sections and components
  const pageContent = await fetchOneEntry({
    apiKey: process.env.NEXT_PUBLIC_BUILDER_API_KEY!,
    model: /* YOUR BUILDER MODEL NAME */
  });

  return (
    <div className={styles.page}>
      {/* Static hero section - controlled by your code */}
      <section className={styles.hero}>
        <div className={styles.heroContent}>
          <div className={styles.heroOverlay}>
            <h1 className={styles.heroTitle}>Our Store Locations</h1>
            <p className={styles.heroSubtitle}>
              Visit us in person at any of our locations
            </p>
          </div>
        </div>
      </section>

      {/* Dynamic Builder content with store data */}
      <Content
        content={pageContent}
        apiKey={process.env.NEXT_PUBLIC_BUILDER_API_KEY!}
        model="ecom-store"
        data={{ stores: storeData }} // Makes store data available in Builder
      />
    </div>
  );
}

The example above shows several important concepts:

  • fetchEntries(): retrieves all content entries from your store-locations data model.
  • fetchOneEntry(): fetches page content from Builder such as layouts and sections.
  • data prop: passes store data to Builder components, making the data available in the Visual Editor.
  • Hybrid approach: combines static React components with dynamic Builder content.

For more information, see Data Binding.

Create app/stores/stores.module.css:

.page {
  min-height: 100vh;
  background: var(--background);
}

.hero {
  position: relative;
  width: 100%;
  height: 400px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.heroContent {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.heroOverlay {
  text-align: center;
  color: white;
  z-index: 2;
  max-width: 600px;
  padding: 0 20px;
}

.heroTitle {
  font-size: 3.5rem;
  font-weight: 700;
  margin-bottom: 1rem;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.heroSubtitle {
  font-size: 1.25rem;
  font-weight: 300;
  opacity: 0.9;
  margin: 0;
}

@media (max-width: 768px) {
  .hero {
    height: 300px;
  }
  
  .heroTitle {
    font-size: 2.5rem;
  }
  
  .heroSubtitle {
    font-size: 1.1rem;
  }
}

To run your Next.js application:

npm run dev

Your store data is now available through the data prop. Bind data to content in the Visual Editor:

  1. Go to Content in the Builder dashboard.
  2. Click + New Entry and select the Section model from the dropdown.
  3. Click the Text block to bind the data.
  4. In the Data tab, expand Element data bindings, select your store data from the FROM dropdown, and select Text from the GET dropdown.
  5. The element displays dynamic content from your store's Data model.

The data prop provides dynamic access to your Next.js data in the Visual Editor. The video below demonstrates this data binding workflow:

Install Flutter if not already installed, then create a new project. For more information, see the Flutter installation guide.

Create a new Flutter project:

flutter create store_locations_app

Open the Flutter project in your IDE:

cd store_locations_app

Add the HTTP package to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  ...
  http: ^1.1.0  #add this line to add HTTP package

Install dependencies:

flutter pub get

This step sets up the foundation for communicating with Builder's API. Create constants for the API endpoint and models to structure the data your app receives.

These constants centralize your Builder API configuration, making the configuration easy to manage and update your API endpoints.

Create lib/constants/api_constants.dart:

/// API constants for Builder integration
class ApiConstants {
  // Private constructor to prevent instantiation
  ApiConstants._();

  /// Builder API endpoint for store locations
  static const String storeLocationsUrl =
      'https://cdn.builder.io/api/v3/content/store-locations?apiKey=YOUR_API_KEY_HERE';
}

These models define the structure of your store location data and handle converting JSON responses from Builder's API into Dart objects that your Flutter app can work with.

Create lib/models/store_location.dart:

/// Model for store location data from Builder API
class StoreLocation {
  final String name;
  final String image;
  final String address;

  const StoreLocation({
    required this.name,
    required this.image,
    required this.address,
  });

  /// Create StoreLocation from API JSON response
  factory StoreLocation.fromJson(Map<String, dynamic> json) {
    final data = json['data'] as Map<String, dynamic>;
    return StoreLocation(
      name: data['name'] as String,
      image: data['image'] as String,
      address: data['address'] as String,
    );
  }
}

Create lib/models/api_response.dart:

import 'store_location.dart';

/// Model for Builder API response
class ApiResponse {
  final List<StoreLocation> results;

  const ApiResponse({required this.results});

  /// Create ApiResponse from JSON
  factory ApiResponse.fromJson(Map<String, dynamic> json) {
    final resultsJson = json['results'] as List<dynamic>;
    final results = resultsJson
        .map((item) => StoreLocation.fromJson(item as Map<String, dynamic>))
        .toList();

    return ApiResponse(results: results);
  }
}

This step builds the core functionality: a service to fetch data from Builder and UI components to display that data in your app.

This service handles the HTTP communication with Builder's Content API, fetching your store location data and handling any network errors that might occur.

Create lib/services/store_service.dart:

import 'dart:convert';
import 'package:http/http.dart' as http;

import '../models/api_response.dart';
import '../models/store_location.dart';
import '../constants/api_constants.dart';

/// Service to fetch store locations from Builder API
class StoreService {
  /// Fetch store locations from the API
  Future<List<StoreLocation>> fetchStoreLocations() async {
    try {
      // Make HTTP request
      final response = await http.get(
        Uri.parse(ApiConstants.storeLocationsUrl),
      );

      if (response.statusCode == 200) {
        // Parse JSON response
        final jsonData = json.decode(response.body) as Map<String, dynamic>;
        final apiResponse = ApiResponse.fromJson(jsonData);
        return apiResponse.results;
      } else {
        throw Exception('Failed to load store locations');
      }
    } catch (e) {
      throw Exception('Error fetching data: $e');
    }
  }
}

This reusable widget displays individual store locations in a consistent card format throughout your app.

Create lib/widgets/store_location_card.dart:

import 'package:flutter/material.dart';
import '../models/store_location.dart';

/// Card widget to display store location information
class StoreLocationCard extends StatelessWidget {
  final StoreLocation location;
  final VoidCallback? onTap;

  const StoreLocationCard({super.key, required this.location, this.onTap});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12.0),
      child: ListTile(
        leading: const CircleAvatar(
          backgroundColor: Colors.blue,
          child: Icon(Icons.location_city, color: Colors.white),
        ),
        title: Text(
          location.name,
          style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
        subtitle: Text(location.address),
        trailing: onTap != null ? const Icon(Icons.arrow_forward_ios) : null,
        onTap: onTap,
      ),
    );
  }
}

The screen uses the service to fetch data from the Builder Data model and displays data using your UI components with loading states and error handling.

Create lib/screens/store_list_screen.dart:

import 'package:flutter/material.dart';

import '../models/store_location.dart';
import '../services/store_service.dart';
import '../widgets/store_location_card.dart';

/// Main screen that displays store locations from Builder
class StoreListScreen extends StatefulWidget {
  const StoreListScreen({super.key});

  @override
  State<StoreListScreen> createState() => _StoreListScreenState();
}

class _StoreListScreenState extends State<StoreListScreen> {
  final StoreService _storeService = StoreService();

  List<StoreLocation> _storeLocations = [];
  bool _isLoading = true;
  String? _errorMessage;

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

  /// Load store locations from Builder API
  Future<void> _loadStoreLocations() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final locations = await _storeService.fetchStoreLocations();
      setState(() {
        _storeLocations = locations;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Store Locations'),
        centerTitle: true,
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _buildBody(),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadStoreLocations,
        tooltip: 'Refresh',
        child: const Icon(Icons.refresh),
      ),
    );
  }

  /// Build the main content based on current state
  Widget _buildBody() {
    if (_isLoading) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('Loading store locations...'),
          ],
        ),
      );
    }

    if (_errorMessage != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text(_errorMessage!),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: _loadStoreLocations,
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (_storeLocations.isEmpty) {
      return const Center(
        child: Text('No store locations found'),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: _storeLocations.length,
      itemBuilder: (context, index) {
        final location = _storeLocations[index];
        return StoreLocationCard(
          location: location,
          onTap: () {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Selected: ${location.name}')),
            );
          },
        );
      },
    );
  }
}

This final step configures your app's entry point to display the store locations screen you just created.

Update your lib/main.dart:

import 'package:flutter/material.dart';
import 'screens/store_list_screen.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Store Locations Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const StoreListScreen(),
    );
  }
}

Your Flutter application now displays store location data from the Data model with a native interface. To run your Flutter app:

For iOS:

flutter run -d ios

For Android:

flutter run -d android

For web:

flutter run -d web

Make sure you have the appropriate simulators or devices connected before running these commands.

Test how changes in Builder appear across both applications:

  1. Edit a store location in Builder and click Publish.
  2. Refresh your Next.js app and Flutter app to see the changes.

Both applications now display the same updated content from your central Builder data source.

You now have data serving across multiple applications with consistent content delivery. Consider these next steps:

For advanced patterns, see Custom Components and Content Models.

Was this article helpful?

Product

Visual CMS

Theme Studio for Shopify

Sign up

Login

Featured Integrations

React

Angular

Next.js

Gatsby

Get In Touch

Chat With Us

Twitter

Linkedin

Careers

© 2020 Builder.io, Inc.

Security

Privacy Policy

Terms of Service

Get the latest from Builder.io

By submitting, you agree to our Privacy Policy

  • Fusion

  • Publish

  • Product Updates

  • Figma to Code Guide

  • Headless CMS Guide

  • Headless Commerce Guide

  • Composable DXP Guide

Security

Privacy Policy

SaaS Terms

Compliance

Cookie Preferences

Gartner Cool Vendor 2024