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

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:okhttp:4.11.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
Add your API token to local.properties:
BUTTERCMS_API_TOKEN=your_api_token

Initialize the client

Create a reusable API client:
// ButterCMS.kt
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface ButterCMSApi {
    @GET("pages/{page_type}/{slug}/")
    suspend fun getPage(
        @Path("page_type") pageType: String,
        @Path("slug") slug: String,
        @Query("auth_token") authToken: String
    ): PageResponse

    @GET("content/")
    suspend fun getCollection(
        @Query("keys") keys: String,
        @Query("auth_token") authToken: String
    ): CollectionResponse

    @GET("posts/")
    suspend fun getPosts(
        @Query("auth_token") authToken: String,
        @Query("page") page: Int = 1,
        @Query("page_size") pageSize: Int = 10
    ): PostsResponse

    @GET("posts/{slug}/")
    suspend fun getPost(
        @Path("slug") slug: String,
        @Query("auth_token") authToken: String
    ): PostResponse
}

object ButterCMS {
    private const val BASE_URL = "https://api.buttercms.com/v2/"
    val API_TOKEN = BuildConfig.BUTTERCMS_API_TOKEN

    val api: ButterCMSApi by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ButterCMSApi::class.java)
    }
}
For complete API documentation including all available endpoints and parameters, see the REST API Reference.

Data models

// Models.kt
data class PageResponse(val data: PageData)
data class PageData(val slug: String, val fields: Map<String, Any>)

data class CollectionResponse(val data: Map<String, List<Map<String, Any>>>)

data class PostsResponse(val data: List<Post>, val meta: Meta)
data class PostResponse(val data: Post)

data class Post(
    val slug: String,
    val title: String,
    val body: String,
    val summary: String,
    val published: String,
    val featured_image: String?,
    val author: Author,
    val categories: List<Category>,
    val tags: List<Tag>
)

data class Author(val first_name: String, val last_name: String, val email: String?)
data class Category(val name: String, val slug: String)
data class Tag(val name: String, val slug: String)
data class Meta(val next_page: Int?, val previous_page: Int?, val count: Int)

Pages

// LandingPageRepository.kt
class LandingPageRepository {
    suspend fun getPage(slug: String): PageData? {
        return try {
            val response = ButterCMS.api.getPage("landing-page", slug, ButterCMS.API_TOKEN)
            response.data
        } catch (e: Exception) {
            null
        }
    }
}

// LandingPageViewModel.kt
class LandingPageViewModel : ViewModel() {
    private val repository = LandingPageRepository()

    private val _page = MutableLiveData<PageData?>()
    val page: LiveData<PageData?> = _page

    fun loadPage(slug: String) {
        viewModelScope.launch {
            _page.value = repository.getPage(slug)
        }
    }
}

// LandingPageActivity.kt
class LandingPageActivity : AppCompatActivity() {
    private val viewModel: LandingPageViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_landing)

        val slug = intent.getStringExtra("slug") ?: "home"
        viewModel.loadPage(slug)

        viewModel.page.observe(this) { page ->
            page?.let {
                findViewById<TextView>(R.id.headline).text = it.fields["headline"] as? String
                findViewById<TextView>(R.id.subheadline).text = it.fields["subheadline"] as? String
                // Load hero_image with Glide/Coil
                // Render body HTML in WebView
            }
        }
    }
}
<!-- res/layout/activity_landing.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/headline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/subheadline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textColor="@android:color/darker_gray" />

    <ImageView
        android:id="@+id/heroImage"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="16dp"
        android:scaleType="centerCrop" />

    <WebView
        android:id="@+id/bodyContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp" />
</LinearLayout>

Collections

// BrandsRepository.kt
class BrandsRepository {
    suspend fun getBrands(): List<Map<String, Any>>? {
        return try {
            val response = ButterCMS.api.getCollection("brands", ButterCMS.API_TOKEN)
            response.data["brands"]
        } catch (e: Exception) {
            null
        }
    }
}

// BrandsViewModel.kt
class BrandsViewModel : ViewModel() {
    private val repository = BrandsRepository()

    private val _brands = MutableLiveData<List<Map<String, Any>>>()
    val brands: LiveData<List<Map<String, Any>>> = _brands

    fun loadBrands() {
        viewModelScope.launch {
            _brands.value = repository.getBrands() ?: emptyList()
        }
    }
}

// BrandsActivity.kt
class BrandsActivity : AppCompatActivity() {
    private val viewModel: BrandsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_brands)

        val recyclerView = findViewById<RecyclerView>(R.id.brandsRecycler)
        recyclerView.layoutManager = LinearLayoutManager(this)

        viewModel.loadBrands()

        viewModel.brands.observe(this) { brands ->
            recyclerView.adapter = BrandsAdapter(brands)
        }
    }
}

Dynamic components

Component renderer

// ComponentRenderer.kt
sealed class ButterComponent {
    data class Hero(
        val headline: String,
        val subheadline: String,
        val image: String?,
        val buttonLabel: String?,
        val buttonUrl: String?
    ) : ButterComponent()

    data class Features(val items: List<Map<String, Any>>) : ButterComponent()
    data class CTA(val title: String, val buttonText: String, val buttonUrl: String) : ButterComponent()
}

object ComponentParser {
    fun parse(components: List<Map<String, Any>>): List<ButterComponent> {
        return components.mapNotNull { component ->
            val type = component["type"] as? String
            val fields = component["fields"] as? Map<String, Any> ?: return@mapNotNull null

            when (type) {
                "hero" -> ButterComponent.Hero(
                    headline = fields["headline"] as? String ?: "",
                    subheadline = fields["subheadline"] as? String ?: "",
                    image = fields["image"] as? String,
                    buttonLabel = fields["button_label"] as? String,
                    buttonUrl = fields["button_url"] as? String
                )
                "cta" -> ButterComponent.CTA(
                    title = fields["title"] as? String ?: "",
                    buttonText = fields["button_text"] as? String ?: "",
                    buttonUrl = fields["button_url"] as? String ?: ""
                )
                else -> null
            }
        }
    }
}

Component views

// HeroView.kt
class HeroView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    init {
        orientation = VERTICAL
        LayoutInflater.from(context).inflate(R.layout.component_hero, this, true)
    }

    fun bind(hero: ButterComponent.Hero) {
        findViewById<TextView>(R.id.heroHeadline).text = hero.headline
        findViewById<TextView>(R.id.heroSubheadline).text = hero.subheadline
        hero.image?.let { url ->
            Glide.with(this).load(url).into(findViewById(R.id.heroImage))
        }
        hero.buttonLabel?.let { label ->
            findViewById<Button>(R.id.heroButton).apply {
                text = label
                visibility = VISIBLE
                setOnClickListener {
                    // Handle button click
                }
            }
        }
    }
}

Using in activities

// ComponentPageActivity.kt
class ComponentPageActivity : AppCompatActivity() {
    private val viewModel: LandingPageViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_component_page)

        val container = findViewById<LinearLayout>(R.id.componentsContainer)
        val slug = intent.getStringExtra("slug") ?: "home"

        viewModel.loadPage(slug)

        viewModel.page.observe(this) { page ->
            page?.let {
                @Suppress("UNCHECKED_CAST")
                val bodyComponents = it.fields["body"] as? List<Map<String, Any>> ?: emptyList()
                val components = ComponentParser.parse(bodyComponents)

                container.removeAllViews()
                components.forEach { component ->
                    when (component) {
                        is ButterComponent.Hero -> {
                            val heroView = HeroView(this)
                            heroView.bind(component)
                            container.addView(heroView)
                        }
                        is ButterComponent.CTA -> {
                            // Add CTA view
                        }
                        else -> {}
                    }
                }
            }
        }
    }
}

Blog

// BlogRepository.kt
class BlogRepository {
    suspend fun getPosts(page: Int = 1): PostsResponse? {
        return try {
            ButterCMS.api.getPosts(ButterCMS.API_TOKEN, page, 10)
        } catch (e: Exception) {
            null
        }
    }
}

// BlogListViewModel.kt
class BlogListViewModel : ViewModel() {
    private val repository = BlogRepository()

    private val _posts = MutableLiveData<List<Post>>()
    val posts: LiveData<List<Post>> = _posts

    private val _meta = MutableLiveData<Meta?>()
    val meta: LiveData<Meta?> = _meta

    fun loadPosts(page: Int = 1) {
        viewModelScope.launch {
            repository.getPosts(page)?.let { response ->
                _posts.value = response.data
                _meta.value = response.meta
            }
        }
    }
}

// BlogListActivity.kt
class BlogListActivity : AppCompatActivity() {
    private val viewModel: BlogListViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_blog_list)

        val recyclerView = findViewById<RecyclerView>(R.id.postsRecycler)
        recyclerView.layoutManager = LinearLayoutManager(this)

        viewModel.loadPosts()

        viewModel.posts.observe(this) { posts ->
            recyclerView.adapter = PostsAdapter(posts) { post ->
                val intent = Intent(this, BlogPostActivity::class.java)
                intent.putExtra("slug", post.slug)
                startActivity(intent)
            }
        }
    }
}
// PostsAdapter.kt
class PostsAdapter(
    private val posts: List<Post>,
    private val onClick: (Post) -> Unit
) : RecyclerView.Adapter<PostsAdapter.ViewHolder>() {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val title: TextView = view.findViewById(R.id.postTitle)
        val summary: TextView = view.findViewById(R.id.postSummary)
        val author: TextView = view.findViewById(R.id.postAuthor)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_post, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val post = posts[position]
        holder.title.text = post.title
        holder.summary.text = post.summary
        holder.author.text = "By ${post.author.first_name} ${post.author.last_name}"
        holder.itemView.setOnClickListener { onClick(post) }
    }

    override fun getItemCount() = posts.size
}

Caching

Use OkHttp’s caching for better performance:
// ButterCMS.kt
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File

object ButterCMS {
    private const val BASE_URL = "https://api.buttercms.com/v2/"
    val API_TOKEN = BuildConfig.BUTTERCMS_API_TOKEN

    fun createApi(context: Context): ButterCMSApi {
        val cacheSize = 10L * 1024 * 1024 // 10 MB
        val cache = Cache(File(context.cacheDir, "butter_cache"), cacheSize)

        val okHttpClient = OkHttpClient.Builder()
            .cache(cache)
            .addInterceptor { chain ->
                val request = chain.request().newBuilder()
                    .header("Cache-Control", "public, max-age=3600") // 1 hour
                    .build()
                chain.proceed(request)
            }
            .build()

        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ButterCMSApi::class.java)
    }
}

Resources

Kotlin SDK

Kotlin SDK reference

Java SDK

Official Java SDK reference

GitHub Repository

View source code

Content API

REST API documentation