Writing a custom Moshi Adapter

April 18, 2020

new-paltz.JPG

Recently I was working on a sample app that demonstrates how to use Kotlin Flow and Channels. The app uses Retrofit, OkHttp and Moshi.

It hits two GitHub API endpoints. One of them required a custom Moshi Codegen Adapter and the other did not. Let’s cover how I implemented both.

Auto-generated Adapter

The first feed is straightforward and does not require a custom Adapter: https://api.github.com/users/<username>/repos

The JSON payload for that endpoint contains a list of repositories where the root element is an Array:

[
  {
    "id": 1,
    "name": "kotlin"
  },
  {
    "id": 2,
    "name": "swift"
  }
]

The Retrofit service function looks like this:

@GET("users//repos?per_page=$MAX_RESULTS_PER_PAGE")
suspend fun getUserRepos(
    @Path("username") username: String
): List<Repo>

The model object Repo looks like this:

@JsonClass(generateAdapter = true)
data class Repo(
    val name: String,
    @Json(name = "stargazers_count") val stars: Int
)

My app/build.gradle contains:

// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:x.y.z'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:x.y.z'

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:x.y.z'
implementation 'com.squareup.retrofit2:converter-moshi:x.y.z'

// OkHttp
implementation 'com.squareup.okhttp3:okhttp:x.y.z'
implementation 'com.squareup.okhttp3:logging-interceptor:x.y.z'

The Dagger module that creates an instance of Retrofit is as follows:

@Provides
fun provideRetrofit(): ApiService {
  val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.BODY
  }

  val okHttpBuilder = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)

return Retrofit.Builder()
    .baseUrl(GITHUB_BASE_URL)
    .addConverterFactory(MoshiConverterFactory.create())
    .client(okHttpBuilder.build())
    .build()
    .create(ApiService::class.java)
}

That set up works great for loading a list of Repos from an Array of JSON Objects. It does not require any custom adapters since the adapter is auto-generated thanks to Moshi annotation @JsonClass(generateAdapter = true)

A case for custom Codegen Adapter

The other feed https://api.github.com/search/repositories?q=<query> has a different response format. It returns an Array called items that wraps repository items.

{
  "total_count": 2,
  "items": [
    {
      "id": 1,
      "name": "kotlin"
    },
    {
      "id": 2,
      "name": "swift"
    }
  ]
}

Here is a function that uses it in the Retrofit interface:

@GET("search/repositories?per_page=$MAX_RESULTS_PER_PAGE")
@WrappedRepoList
suspend fun getReposForQuery(@Query("q") query: String): List<Repo>

Note, the @WrappedRepoList annotation to unwrap the repository items so that the return type of the Retrofit function is List<Repo>. This is made possible by a custom Moshi Adapter:

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class WrappedRepoList

@JsonClass(generateAdapter = true)
data class RepoList(val items: List<Repo>)

class RepoListJsonAdapter {
    @WrappedRepoList
    @FromJson
    fun fromJson(json: RepoList): List<Repo> {
        return json.items
    }

    @ToJson
    fun toJson(@WrappedRepoList value: List<Repo>): RepoList {
        throw UnsupportedOperationException()
    }
}

Finally, in order to use this custom Adapter, you need to register it when creating an instance of Retrofit.

@Provides
fun provideRetrofit(): ApiService {
  val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.HEADERS
  }

  val okHttpBuilder = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)

  val moshi = Moshi.Builder()
    .add(RepoListJsonAdapter())
    .build()

  return Retrofit.Builder()
    .baseUrl(GITHUB_BASE_URL)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .client(okHttpBuilder.build())
    .build()
    .create(ApiService::class.java)
}

Note that .add(KotlinJsonAdapterFactory()) was removed when building moshi in this case since we are using Codegen, not reflection.

And that’s it. Now the getReposForQuery(q) unwraps the repository list and returns List<Repo> using custom Moshi Codegen Adapter.

You can see an example of this code used in an app in this repo.

 
Previous
Previous

Theming: Default Styles on Android

Next
Next

Implementing Dark Mode on Android