Adapter architecture
Purpose
This guide explains how to implement a new network service layer in a Deltatre App by creating and registering custom adapter components. It is intended for Android developers who need to integrate bespoke authentication flows, connect to different backends, or combine multiple data sources in the same app. The article provides step-by-step instructions, context, and examples.
Problem definition
When building new OTT or content-rich applications, developers often face the following challenges:
- Multiple Backends: The app must connect to various data sources (e.g., Rocket, FORGE, or custom APIs).
- Rigid Data Models: Legacy approaches tightly couple frontend models to backend APIs via code generators like Swagger, making updates risky without synchronized backend changes.
- Custom Business Logic: Certain data flows—such as login, personalization, and recommendations—require backend-specific logic that generic SDK calls cannot handle.
- Backend Independence: Apps need to evolve without being blocked by backend release cycles.
Historically, Swagger-generated models tied to a single backend created a hard dependency between app and backend, making it difficult to:
- Switch or combine data sources: Preventing flexibility in architecture.
- Extend object models for new features: Without breaking existing functionality.
- Maintain backward compatibility: When backend APIs change.
Definition of the solution and its assumptions
The Deltatre App SDK solves these issues with a pluggable adapter architecture that:
- Defines application-owned domain models: Decoupled from any backend.
- Uses a Provider and Provider Registry system: Registers and retrieves data providers by key.
- Offers DataSource interfaces and repository base classes: For overriding or extending backend logic.
- Supports multiple concurrent data sources: Within the same app instance.
Assumptions:
- Existing project setup: You have a Deltatre App project with the SDK installed.
- Language knowledge: You understand basic Kotlin syntax and Android dependency injection (Koin).
- Backend access: You have documentation for the backend(s) you’re integrating.
Step-by-step solution implementation
1. Create a data provider
1.1 Why
A Data Provider abstracts data fetching logic for a specific backend or service. It ensures that the rest of your app interacts with a unified API, regardless of where the data comes from.
1.2 What
A Data Provider implements the DataProvider interface and provides repositories for each functional area (e.g., config, profile, content).
1.3 How
- Extend: Implement
DataProvider. - Delegate: Use existing providers for shared logic.
- Register: Add your provider to
ProviderRegistry.
1.4 Sample
This snippet shows how to instantiate a RocketDataProvider and register it in the ProviderRegistry so it can be resolved and used by repositories.
val dataProvider =
RocketDataProvider(
sessionManager = sessionManager,
featureFlagManager = featureFlagManager,
configurationParams = configurationParams,
logInterceptor = logInterceptor,
previousSearchesRepository = previousSearchesRepository,
networkErrorListener = networkErrorListener
)
providerRegistry.registerProvider(ROCKET_PROVIDER, dataProvider)
This snippet shows how to create a custom ScaleDataProvider that delegates most operations to an existing RocketDataProvider while allowing selective overrides.
class ScaleDataProvider(
private val context: Context,
private val isDebugBuild: Boolean,
private val sessionManager: SessionManager,
private val configurationParams: Map<String, String>,
private val featureFlagManager: FeatureFlagManager,
private val configurationManager: ConfigurationManager,
private val selectedTenant: String?,
private val logInterceptor: Interceptor? = null,
private val networkErrorListener: NetworkErrorListener,
private val previousSearchesRepository: PreviousSearchesRepository
) : DataProvider {
private val delegateProvider = RocketDataProvider(
sessionManager, featureFlagManager, configurationParams,
logInterceptor, networkErrorListener, previousSearchesRepository
)
}
1.5 Warnings
- Unique provider keys: Ensure no duplicates in
ProviderRegistry. - Compatibility: Avoid mixing incompatible providers.
2. Implement data contracts
2.1 Why
Data contracts define application-owned domain models and interfaces for fetching data. This ensures backend independence: only the adapter layer changes when the backend changes.
2.2 What
- Interfaces: Specify required operations.
- Domain objects: Define backend-agnostic models.
2.3 How
- Define: Create contracts in SDK or project.
- Map: Convert backend responses into domain models.
- Optional: Use Swagger internally within the adapter.
2.4 Sample
This snippet defines the AuthorizationDataSource interface with the contract for authentication-related calls that a data source must implement.
interface AuthorizationDataSource {
suspend fun deviceAuthorizationCode(request: DeviceRegistrationRequest):
ResourceRequest<DeviceAuthorizationResponse>
suspend fun accountTokenByCode(request: AccountTokenByCodeRequest):
ResourceRequest<List<AccessToken>>
suspend fun accountToken(request: AccountTokenRequest):
ResourceRequest<List<AccessToken>>
suspend fun profileToken(profileTokenRequest: ProfileTokenRequest):
ResourceRequest<List<AccessToken>>
suspend fun refreshToken(tokenRefreshRequest: TokenRefreshRequest):
ResourceRequest<AccessToken>
suspend fun signOut(): ResourceRequest<Any>
suspend fun deleteAccount(): ResourceRequest<Any>
}
2.5 Warnings
- Preserve fields: Mappers must keep all required fields.
- Communicate changes: Ensure all adapter maintainers know about contract changes.
3. Extend or override repositories
3.1 Why
Repositories implement the actual fetching and caching logic. Extending them allows you to customize features such as authentication, pagination, or content retrieval.
3.2 What
- Base repositories: For default behavior.
- Custom repositories: Override only necessary parts.
3.3 How
- Inject: Pass
DataSourceinto the repository. - Reuse: Use SDK-provided repositories when possible.
3.4 Sample
This snippet shows how to instantiate repositories for content and scheduling, using both base and custom data sources.
private val contentRepository =
ContentRepository(contentDataSource)
private val scheduleRepository = ScheduleRepository(
RocketScheduleDataSource(sessionManager, featureFlagManager, configurationParams),
ScheduleInMemoryCache(configRepository)
)
3.5 Warnings
- Avoid duplication: Keep logic DRY.
- Cache strategically: Avoid unnecessary backend calls.
4. Map backend data to domain objects
4.1 Why
Domain objects keep your app independent from backend schemas.
4.2 What
- Kotlin data classes: Represent entities used in the UI.
4.3 How
- Map: Convert backend-specific models to domain objects in the adapter.
4.4 Sample
This snippet defines the Page domain model used by the UI. It is backend-agnostic and can store additional metadata for custom requirements.
data class Page(
val id: String,
val path: String,
val pageType: PageType,
val key: String? = null,
val title: String? = null,
val template: String? = null,
val isStatic: Boolean = false,
var entries: MutableList<PageRowEntry>? = null,
var breakoutItem: BreakoutItem? = null,
var additionalMeta: HashMap<String, Any?> = HashMap(),
val keywords: List<String> = emptyList(),
var list: RowEntryList? = null,
val item: ListItem? = null
)
4.5 Warnings
- Generic design: Avoid backend-specific fields.
- Optional fields: Preserve backward compatibility.
Solution’s benefits
- Backend Independence: Only adapters change when backends change.
- Multi-Source Support: Combine providers like Rocket and Forge in the same project.
- Extensibility: Override only what you need.
- Backward Compatibility: Domain models are app-owned.
- Custom Metadata: Easily extend objects without breaking compatibility.
Solution’s limitations
- Initial Complexity: Requires learning the adapter architecture before effective use.
- Maintenance Overhead: Multiple providers increase code upkeep.
- Performance Risks: Improper mapping or chaining of providers can introduce latency.
- Security: Adapters handling authentication must be carefully implemented to prevent leaks.