Skip to main content

Adapter developer‑ready guide

Goal: show you, end‑to‑end, how to plug in a new network/service layer (“adapter”) to the Deltatre Apps with a runnable “Hello Adapter”, an architecture diagram, a mapping checklist, and test scaffolding.

Key ideas you’ll see throughout:

  • Adapters are registered in a ProviderRegistry and invoked via a thin RequestExecutor.
  • An adapter exposes repositories that use DataSource interfaces (e.g., PageDataSource, AuthorizationDataSource). You can subclass/delegate to reuse behavior.
  • The domain model is owned by the app (not generated from backend Swagger); adapters map backend responses into these contracts.

Architecture at a glance

  • ProviderRegistry and RequestExecutor simplify selecting a provider by key rather than plumbing calls through multiple layers.
  • A provider returns repositories; repositories consume DataSource interfaces that you implement or delegate. The SDK’s base repos give out‑of‑the‑box behavior like pagination and can be extended.
  • You can mix providers (e.g., “Scale delegates to Rocket but overrides auth”), avoiding re‑implementing everything.

Quickstart: “Hello Adapter” (runnable)

This is a copy‑pasteable minimal project slice that compiles in a plain JVM unit test (no Android device required). It mirrors the real structure but stays tiny so you can learn the shape before wiring real APIs.

1) Gradle (Kotlin/JVM module or Android app)

// build.gradle.kts (module)
plugins {
kotlin("jvm") version "1.9.23" // or Android plugin in an app
}

repositories { mavenCentral() }

dependencies {
implementation("io.insert-koin:koin-core:3.5.6")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation("io.mockk:mockk:1.13.10")
}

Real Deltatre Apps inject RequestExecutor via Koin and pick a provider from the registry. References: Koin DI (koin.io), Coroutines Test (kotlinlang.org).

2) Domain contracts (app‑owned)

Keep them small for the demo; the production contracts are richer and mostly optional with additionalMeta for integrator data.

// domain/Page.kt
enum class PageType { HOME, LIST, DETAILS }

data class Page(
val id: String,
val path: String,
val pageType: PageType,
val title: String? = null,
val entries: MutableList<PageRowEntry>? = null,
val additionalMeta: HashMap<String, Any?> = HashMap()
)

data class PageRowEntry(
val template: String,
val title: String? = null,
val list: List<ListItem>? = null,
val additionalMeta: HashMap<String, Any?> = HashMap()
)

data class ListItem(
val id: String? = null,
val title: String? = null,
val path: String? = null,
val additionalMeta: HashMap<String, Any?> = HashMap()
)

In the SDK, domain objects like Page, PageRowEntry, and ListItem are broad, mostly optional, and include additionalMeta.

3) Contracts and plumbing

DataSources define what an adapter must offer. Repositories are thin wrappers over DataSources. Provider exposes repositories. Registry stores providers. RequestExecutor picks/executes by key.

// core/ResourceRequest.kt
sealed class ResourceRequest<out T> {
data class Success<T>(val data: T) : ResourceRequest<T>()
data class Error(val throwable: Throwable) : ResourceRequest<Nothing>()
}

// contracts/datasource/PageDataSource.kt
interface PageDataSource {
suspend fun read(pageRoute: String): ResourceRequest<Page>
}

// repositories/PageRepository.kt
class PageRepository(private val ds: PageDataSource) {
suspend fun page(path: String) = ds.read(path)
}

// provider/DataProvider.kt
interface DataProvider {
fun providePageRepository(): PageRepository
}

// provider/ProviderRegistry.kt
class ProviderRegistry {
private val providers = mutableMapOf<String, DataProvider>()
fun registerProvider(key: String, provider: DataProvider) { providers[key] = provider }
fun getProvider(key: String): DataProvider =
providers[key] ?: error("No provider registered for key: $key")
}

// provider/RequestExecutor.kt
class RequestExecutor(private val registry: ProviderRegistry) {
suspend fun <T> execute(providerKey: String, block: suspend (DataProvider) -> ResourceRequest<T>)
: ResourceRequest<T> = block(registry.getProvider(providerKey))
}

The real RequestExecutor is a thin wrapper above the registry to execute tasks by named provider.

4) “Hello” DataSource + Provider

// hello/HelloPageDataSource.kt
class HelloPageDataSource : PageDataSource {
override suspend fun read(pageRoute: String): ResourceRequest<Page> {
val page = Page(
id = "hello-1",
path = pageRoute,
pageType = PageType.HOME,
title = "Hello Adapter",
entries = mutableListOf(
PageRowEntry(
template = "HERO",
title = "Welcome",
list = listOf(ListItem(id = "item-1", title = "It works!", path = "/somewhere"))
)
)
)
return ResourceRequest.Success(page)
}
}

// hello/HelloDataProvider.kt
class HelloDataProvider : DataProvider {
private val pageRepository = PageRepository(HelloPageDataSource())
override fun providePageRepository(): PageRepository = pageRepository
}

5) Wire with Koin

// di/Modules.kt
import org.koin.core.qualifier.named
import org.koin.dsl.module

val helloModule = module {
single { ProviderRegistry() }
single { RequestExecutor(get()) }
single<DataProvider>(named("HELLO")) { HelloDataProvider() }
}

Register the provider at app startup (or in tests):

// AppStart.kt (or test setup)
import org.koin.core.context.startKoin
import org.koin.java.KoinJavaComponent.get
import org.koin.core.qualifier.named

fun startApp() {
startKoin { modules(helloModule) }
val registry: ProviderRegistry = get(ProviderRegistry::class.java)
val hello: DataProvider = get(DataProvider::class.java, named("HELLO"))
registry.registerProvider("HELLO", hello)
}

In production, multiple providers can coexist (e.g., Rocket, Scale, Mock). “Scale” can delegate to “Rocket” for most calls while overriding auth.

6) Run it (unit test)

// test/HelloAdapterTest.kt
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.java.KoinJavaComponent.get
import org.koin.core.qualifier.named

class HelloAdapterTest {
@Test fun helloAdapter_endToEnd() = runTest {
startKoin { modules(helloModule) }
try {
val registry: ProviderRegistry = get(ProviderRegistry::class.java)
val hello: DataProvider = get(DataProvider::class.java, named("HELLO"))
registry.registerProvider("HELLO", hello)
val executor: RequestExecutor = get(RequestExecutor::class.java)

val result = executor.execute("HELLO") { provider ->
provider.providePageRepository().page("/home")
}

val page = (result as ResourceRequest.Success).data
assertEquals("Hello Adapter", page.title)
assertEquals("/home", page.path)
} finally {
stopKoin()
}
}
}

Mapping checklist (from real backend → domain)

Use this when you replace HelloPageDataSource with a Retrofit/OkHttp client, GraphQL, or any SDK.

Contracts & scope

  • Identify the DataSources you must implement (PageDataSource, AuthorizationDataSource, etc.). Keep method signatures identical to project contracts.
  • Decide what to delegate to an existing provider vs. implement yourself (auth is a common override).

Domain model ownership

  • Do not expose raw backend DTOs to the app. Map to app‑owned domain objects (Page, PageRowEntry, ListItem).
  • Prefer optional fields; stash backend‑specific bits in additionalMeta.

Data shaping

  • You can reshape payloads (e.g., flatten/restructure “ItemSummary”/“Page” for efficiency) prior to mapping.
  • Chain queries if needed (e.g., recommendations pipeline) before mapping; contracts don’t dictate endpoints.

Parameters & context

  • Pass through context such as device, user segments, featureFlags, language, and subscription where applicable.

Repositories & caching

  • Reuse or extend base repositories for behaviors like pagination and in‑memory caches (e.g., schedule cache).

Error handling

  • Map transport errors onto a domain‑level ResourceRequest.Error. Keep retries/timeouts/cancellation in your DataSource.

Performance

  • Avoid over‑fetching; select/expand fields server‑side when possible (the Rocket adapter shows targeted request params).

Note: Using Swagger/OpenAPI inside your adapter is fine as long as you map to the domain. What you must avoid is letting generated models become your domain.


Test scaffolding (patterns + examples)

1) Mapper unit test (“golden”)

// test/MappingTest.kt
import org.junit.Assert.assertEquals
import org.junit.Test

// Example mapper
data class RemotePage(val id: String, val route: String, val title: String)
fun RemotePage.toDomain(): Page =
Page(id = id, path = route, pageType = PageType.HOME, title = title)

class MappingTest {
@Test fun mapsRemoteToDomain() {
val remote = RemotePage("42", "/home", "Welcome")
val domain = remote.toDomain()
assertEquals("42", domain.id)
assertEquals("/home", domain.path)
assertEquals("Welcome", domain.title)
}
}

2) DataSource contract test (fake client)

// test/PageDataSourceTest.kt
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Test

class FakeApi { fun getPage(path: String) = RemotePage("1", path, "Title") }

class RetrofitLikePageDataSource(private val api: FakeApi) : PageDataSource {
override suspend fun read(pageRoute: String): ResourceRequest<Page> =
ResourceRequest.Success(api.getPage(pageRoute).toDomain())
}

class PageDataSourceTest {
@Test fun returnsSuccess() = runTest {
val ds = RetrofitLikePageDataSource(FakeApi())
val res = ds.read("/home")
assertTrue(res is ResourceRequest.Success)
}
}

The Rocket DataSource uses Retrofit and may use Swagger‑generated interfaces for the transport, but still maps to the app domain contracts.

3) Repository test (with fake DataSource)

// test/PageRepositoryTest.kt
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test

class FakePageDS : PageDataSource {
override suspend fun read(pageRoute: String) =
ResourceRequest.Success(Page("1", pageRoute, PageType.HOME, "Repo OK"))
}

class PageRepositoryTest {
@Test fun repositoryDelegates() = runTest {
val repo = PageRepository(FakePageDS())
val res = repo.page("/p")
val page = (res as ResourceRequest.Success).data
assertEquals("Repo OK", page.title)
}
}

4) ProviderRegistry + RequestExecutor integration test

// test/ProviderIntegrationTest.kt
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test

class ProviderIntegrationTest {
@Test fun executesAgainstNamedProvider() = runTest {
val registry = ProviderRegistry()
registry.registerProvider("HELLO", HelloDataProvider())
val executor = RequestExecutor(registry)

val res = executor.execute("HELLO") { it.providePageRepository().page("/home") }
val page = (res as ResourceRequest.Success).data
assertEquals("Hello Adapter", page.title)
}
}

5) Koin module verification (optional)

// test/KoinWiringTest.kt
import org.junit.Test
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.test.verify.verify
import org.koin.dsl.koinApplication

class KoinWiringTest {
@Test fun definitionsAreResolvable() {
koinApplication { modules(helloModule) }.checkModules()
}
}

Adapting a real backend (field guide)

  1. Start with the Page flow. Implement PageDataSource.read(route) using your client (Retrofit/GraphQL/SDK). Add request params for device, segments, featureFlags, language, and subscription when available; these are commonly used in the Rocket adapter’s call signature.
  2. Map DTO → domain. Keep domain stable; focus all breaking changes in the adapter. Prior ref apps that generated domain from Swagger were “fatally flawed” due to tight coupling—avoid repeating this.
  3. Iterate to Auth. Implement AuthorizationDataSource calls (device code, token exchange, refresh, sign‑out, delete account). If you have another provider with working defaults, delegate everything else and only override auth.
  4. Reuse repos. Prefer base repositories to inherit pagination/caching behavior (e.g., schedule cache) instead of re‑implementing from scratch.
  5. Post‑processing hook. If needed, post‑process Page after fetch (Rocket’s PageDataSource exposes a callback for page postprocessing).

Advanced notes (from the AXIS docs)

  • Multiple providers (Rocket, Scale, Mock) can be registered; the registry selects which to use via a key at runtime.
  • ScaleAdapter is implemented by delegating most calls to RocketDataProvider but overriding auth‑related pieces—use this pattern to minimize new code.
  • RequestExecutor abstracts registry lookups, so call sites don’t know about providers directly.

What’s in the official article that this guide builds upon

  • Provider/ProviderRegistry + instantiation and registration patterns.
  • Repository wiring (Config, Page, Authorization, Account, Profile, Content, Schedule) and a Rocket example including an in‑memory schedule cache.
  • AuthorizationDataSource and PageDataSource signatures, with a Retrofit‑based example for pages (including params like segments, featureFlags, and language).
  • Domain object outlines (Page, PageRowEntry, ListItem) with optional fields and additionalMeta.

References (further reading)


Appendix: Real‑world call shape (for your adapters)

The Rocket PageDataSource.read() example calls out typical request params you may need to pass through (device, subscription code, segments, feature flags, language) before mapping into Page. This logic is transport‑agnostic at the contract level, enabling query chaining and reshaping, then mapping to the domain.

Was this page helpful?