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
- Groovy (build.gradle)
- Kotlin DSL (build.gradle.kts)
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'
}
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")
}
local.properties:
BUTTERCMS_API_TOKEN=your_api_token
Initialize the client
Create a reusable API client:- Kotlin
- Java
// 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)
}
}
// ButterCMS.java
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface ButterCMSApi {
@GET("pages/{page_type}/{slug}/")
Call<PageResponse> getPage(
@Path("page_type") String pageType,
@Path("slug") String slug,
@Query("auth_token") String authToken
);
@GET("content/")
Call<CollectionResponse> getCollection(
@Query("keys") String keys,
@Query("auth_token") String authToken
);
@GET("posts/")
Call<PostsResponse> getPosts(
@Query("auth_token") String authToken,
@Query("page") int page,
@Query("page_size") int pageSize
);
@GET("posts/{slug}/")
Call<PostResponse> getPost(
@Path("slug") String slug,
@Query("auth_token") String authToken
);
}
public class ButterCMS {
private static final String BASE_URL = "https://api.buttercms.com/v2/";
public static final String API_TOKEN = BuildConfig.BUTTERCMS_API_TOKEN;
private static ButterCMSApi api;
public static ButterCMSApi getApi() {
if (api == null) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
api = retrofit.create(ButterCMSApi.class);
}
return api;
}
}
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
- Kotlin (Coroutines)
- Java (Callbacks)
// 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
}
}
}
}
// LandingPageRepository.java
public class LandingPageRepository {
public void getPage(String slug, Callback<PageResponse> callback) {
ButterCMS.getApi()
.getPage("landing-page", slug, ButterCMS.API_TOKEN)
.enqueue(callback);
}
}
// LandingPageActivity.java
public class LandingPageActivity extends AppCompatActivity {
private LandingPageRepository repository = new LandingPageRepository();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_landing);
String slug = getIntent().getStringExtra("slug");
if (slug == null) slug = "home";
repository.getPage(slug, new Callback<PageResponse>() {
@Override
public void onResponse(Call<PageResponse> call, Response<PageResponse> response) {
if (response.isSuccessful() && response.body() != null) {
PageData page = response.body().getData();
TextView headline = findViewById(R.id.headline);
headline.setText((String) page.getFields().get("headline"));
}
}
@Override
public void onFailure(Call<PageResponse> call, Throwable t) {
// Handle error
}
});
}
}
<!-- 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
- Kotlin (Coroutines)
- Java (Callbacks)
// 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)
}
}
}
// BrandsRepository.java
public class BrandsRepository {
public void getBrands(Callback<CollectionResponse> callback) {
ButterCMS.getApi()
.getCollection("brands", ButterCMS.API_TOKEN)
.enqueue(callback);
}
}
// BrandsActivity.java
public class BrandsActivity extends AppCompatActivity {
private BrandsRepository repository = new BrandsRepository();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_brands);
RecyclerView recyclerView = findViewById(R.id.brandsRecycler);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
repository.getBrands(new Callback<CollectionResponse>() {
@Override
public void onResponse(Call<CollectionResponse> call, Response<CollectionResponse> response) {
if (response.isSuccessful() && response.body() != null) {
List<Map<String, Object>> brands = response.body().getData().get("brands");
recyclerView.setAdapter(new BrandsAdapter(brands));
}
}
@Override
public void onFailure(Call<CollectionResponse> call, Throwable t) {
// Handle error
}
});
}
}
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
- Blog Post List
- Single Blog Post
- Kotlin (Coroutines)
- Java (Callbacks)
// 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)
}
}
}
}
// BlogRepository.java
public class BlogRepository {
public void getPosts(int page, Callback<PostsResponse> callback) {
ButterCMS.getApi()
.getPosts(ButterCMS.API_TOKEN, page, 10)
.enqueue(callback);
}
}
// BlogListActivity.java
public class BlogListActivity extends AppCompatActivity {
private BlogRepository repository = new BlogRepository();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blog_list);
RecyclerView recyclerView = findViewById(R.id.postsRecycler);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
repository.getPosts(1, new Callback<PostsResponse>() {
@Override
public void onResponse(Call<PostsResponse> call, Response<PostsResponse> response) {
if (response.isSuccessful() && response.body() != null) {
List<Post> posts = response.body().getData();
recyclerView.setAdapter(new PostsAdapter(posts, post -> {
Intent intent = new Intent(BlogListActivity.this, BlogPostActivity.class);
intent.putExtra("slug", post.getSlug());
startActivity(intent);
}));
}
}
@Override
public void onFailure(Call<PostsResponse> call, Throwable t) {
// Handle error
}
});
}
}
// 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
}
- Kotlin (Coroutines)
- Java (Callbacks)
// BlogRepository.kt
class BlogRepository {
suspend fun getPost(slug: String): Post? {
return try {
ButterCMS.api.getPost(slug, ButterCMS.API_TOKEN).data
} catch (e: Exception) {
null
}
}
}
// BlogPostViewModel.kt
class BlogPostViewModel : ViewModel() {
private val repository = BlogRepository()
private val _post = MutableLiveData<Post?>()
val post: LiveData<Post?> = _post
fun loadPost(slug: String) {
viewModelScope.launch {
_post.value = repository.getPost(slug)
}
}
}
// BlogPostActivity.kt
class BlogPostActivity : AppCompatActivity() {
private val viewModel: BlogPostViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_blog_post)
val slug = intent.getStringExtra("slug") ?: return finish()
viewModel.loadPost(slug)
viewModel.post.observe(this) { post ->
post?.let {
findViewById<TextView>(R.id.postTitle).text = it.title
findViewById<TextView>(R.id.postAuthor).text =
"By ${it.author.first_name} ${it.author.last_name}"
it.featured_image?.let { url ->
Glide.with(this)
.load(url)
.into(findViewById(R.id.featuredImage))
}
findViewById<WebView>(R.id.postBody).loadDataWithBaseURL(
null, it.body, "text/html", "UTF-8", null
)
}
}
}
}
// BlogPostActivity.java
public class BlogPostActivity extends AppCompatActivity {
private BlogRepository repository = new BlogRepository();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blog_post);
String slug = getIntent().getStringExtra("slug");
if (slug == null) {
finish();
return;
}
repository.getPost(slug, new Callback<PostResponse>() {
@Override
public void onResponse(Call<PostResponse> call, Response<PostResponse> response) {
if (response.isSuccessful() && response.body() != null) {
Post post = response.body().getData();
TextView title = findViewById(R.id.postTitle);
title.setText(post.getTitle());
TextView author = findViewById(R.id.postAuthor);
author.setText("By " + post.getAuthor().getFirstName() +
" " + post.getAuthor().getLastName());
WebView body = findViewById(R.id.postBody);
body.loadDataWithBaseURL(null, post.getBody(),
"text/html", "UTF-8", null);
}
}
@Override
public void onFailure(Call<PostResponse> call, Throwable t) {
// Handle error
}
});
}
}
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