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
ProviderRegistryandRequestExecutorsimplify selecting a provider by key rather than plumbing calls through multiple layers.- A provider returns repositories; repositories consume
DataSourceinterfaces 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
RequestExecutorvia 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, andListItemare broad, mostly optional, and includeadditionalMeta.
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
RequestExecutoris 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, usersegments,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)
- 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. - 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.
- Iterate to Auth. Implement
AuthorizationDataSourcecalls (device code, token exchange, refresh, sign‑out, delete account). If you have another provider with working defaults, delegate everything else and only override auth. - Reuse repos. Prefer base repositories to inherit pagination/caching behavior (e.g., schedule cache) instead of re‑implementing from scratch.
- Post‑processing hook. If needed, post‑process
Pageafter fetch (Rocket’sPageDataSourceexposes 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
RocketDataProviderbut 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.
AuthorizationDataSourceandPageDataSourcesignatures, with a Retrofit‑based example for pages (including params likesegments,featureFlags, andlanguage).- Domain object outlines (
Page,PageRowEntry,ListItem) with optional fields andadditionalMeta.
References (further reading)
- Koin (Dependency Injection): https://insert-koin.io/
- Kotlin Coroutines Test: https://kotlinlang.org/docs/coroutines-test.html
- Retrofit (HTTP): https://square.github.io/retrofit/
- MockK (mocking): https://mockk.io/
- JUnit 4: https://junit.org/junit4/
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.