It’s been a while since I wrote something Kotlin related. Let’s learn more about delegation pattern and using it as a Facade.
Integrating SDK’s into your application might become a challenge if the API surface of the SDK is not properly configured for the consumer. In this blog post, we will talk about Delegation in Kotlin and couple of nifty workarounds to properly integrate a callback based SDK to our Application. First, lets set the stage.
What is this SDK?
Let’s imagine an SDK that exposes an interface which contains roughly 30 callback functions. You might say that it is a terrible way to design an API and you may be right. But that is not our focus here. Usually to instantiate the SDK, we would need to integrate this interface to our Activity class, so that the context will have the correct Callback signature on initialization, like so:
interface SDKCallbacks { fun initialize() fun onInitializeSuccess() fun onInitializeFail() fun onStartFeatureA() //... remaining 26 methods}
class OurActivity: SDKCallbacks { //... oh my god}Implementing these in the activity and use it throughout the app can be done, but directly implementing it in Activity will couple any additional logic we might add to processing the SDK. we want things decoupled and easy to test.
What we can do is that we can create an interface that exposes the states, and emit the states as the callbacks are called.
interface SDKBridge { val state: StateFlow<SDKState>}
class SDKBridgeImpl: SDKCallbacks, SDKBridge { private val _state = MutableStateFlow<SDKState>(SDkState.NotInitialized) override val state = _state.asStateFlow()
override fun onInitialize() { _state.update { SDKState.InitializeStarted } }
override fun onInitializeSuccess() { _state.update { SDKState.Initialized } }}We can then pass this class as a singleton to the activity, connect to the interface using delegation and bind the activity implementation to the SDKBridge interface when we need to access the current state.
class OurActivity( val sdkBridge: SDKBridge = SDKBridgeImpl() // Assume we made it singleton): SDkCallbacks by sdkBridge { // Callback implementation is decoupled}Delegation using the by keyword is like a magic in Kotlin. There are a lot of use-cases of delegation but at its core, it lets us delegate the implementation of an interface to a Concrete class of we are choosing.
This is a valid alternative solution to the problem at hand, we convert the callback based SDK API to a set of states we can freely use, we can inject the SDkBridge any domain use-case we need to act on it, and the implementation is to an extent, truly decoupled.
The Problem
Let’s say that the SDK we use had a major refactor. They decided that the interface with 30 functions is a bit hard to maintain. And they separated the functionalities to core and 5 main features. Let’s call them FeatureA to FeatureE.
There is one catch to this separation though. Each feature requires a smaller Core SDK functionality to work, so they also extend Core Callbacks. Like so 👇🏻
interface CoreCallbacks { fun onInitialize() fun onInitializeSuccess() fun onInitializeFail()}
interface FeatureA: CoreCallbacks { fun startFeatureA() // ... more methods}
interface FeatureB: CoreCallbacks { fun startFeatureB() // ... more methods}To decouple this set of interfaces, we could follow a Facade Pattern, where we have a Facade class that would contain all of the Callback implementation classes as delegates, and we would just expose the state we use in our application like before. The true problem is that they share a set of functions. So when we implement the solution by delegating all of the callbacks, we get this squiggly line in our IDE:
This happens because each interface has also wants us to implement the core functionality. If we decide to implement these in a delegate class like before, we will see this line in Activity too. The point of this decoupling is to offload the SDK implementation away from the Activity. So we want to fix this override conflict in our Facade.
But now Let’s pause here for a bit and implement our Facade to see how we can solve this problem.
Delegation by Facade
Here is the new set of interfaces we must implement in our app:
interface CoreCallbacks { fun onInitialize() fun onInitializeSuccess() fun onInitializeFail() // ... more methods}
interface FeatureA: CoreCallbacks { fun startFeatureA() // ... more methods}
interface FeatureB: CoreCallbacks { fun startFeatureB() // ... more methods}
interface FeatureC: CoreCallbacks { fun startFeatureC() // ... more methods}
interface FeatureD: CoreCallbacks { fun startFeatureD() // ... more methods}
interface FeatureE: CoreCallbacks { fun startFeatureE() // ... more methods}To use delegation in the Feature Callbacks, and Core callbacks, we must also separate the mechanism for passing state. Let’s define a new interface to Update SDK states:
interface SDKBridgeUpdater { fun updateState(latest: SDKState)}And implement this class along with SDKBridge
class SDKBridgeUpdaterImpl: SDKBridgeUpdater, SDKBridge { private val _state = MutableStateFlow<SDKState>(SDkState.NotInitialized) override val state = _state.asStateFlow()
override fun updateState(latest: SDKState) { _state.update { latest } }}If we inject this class as a singleton dependency, than we can use it alongside with every FeatureCallback type.
Let’s call our concrete implementations CallbackHandler. First we will implement the CoreCallbacks:
class CoreCallbacksHandler( private val sdkUpdater: SDKBridgeUpdater): CoreCallbacks {
override fun onInitialize() { sdkUpdater.updateState(SDKState.InitializeStarted) }
override fun onInitializeSuccess() { sdkUpdated.updateState(SDKState.Initialized) }}Once the CoreCallbacks are implemented this way, then we can implement the Feature types by delegating the core functionality.
class FeatureAHandler( private val coreCallbackHandler: CoreCallbackHandler, private val sdkUpdater: SDKBridgeUpdater): FeatureA, CoreCallbacks by coreCallbackHandler { override fun startFeatureA() { sdkUpdater.updateState(SDKState.StartFeatureA) } // ... other featureA methods}
// ... Other Feature Callback handler implementationsSeparating these allow us to move this class implementation to their respective state. It is a bit more code to write but you get decoupling benefits for a modularized project.
Once everything is implemented, we can finally implement our Facade:
class SDKFacade( private val featureAHandler: FeatureAHandler, private val featureBHandler: FeatureBHandler, private val featureCHandler: FeatureCHandler, private val featureDHandler: FeatureDHandler, private val featureEHandler: FeatureEHandler,): FeatureA by featureAHandler, FeatureB by featureBHandler, FeatureC by featureCHandler, FeatureD by featureDHandler, FeatureE by featureEHandler {
}Now we reached to the point of the error we talked at the problem section. At this point, we could argue that each interface should keep a separate set of functionalities and they should be composed at the SDK level. But again, we are focusing on the implementation of the SDK. And we can still make things work with a little boilerplate.
Since all of these Features are extended from CoreCallbacks, compiler can’t decide which implementation this class should going to use at runtime.
So we should override all of the CoreCallbacks methods in the Facade class. That is the first workaround.
Luckily, since we also implemented the CoreCallbacksHandler we can manually delegate the functionality to there by calling the related methods directly:
class SDKFacade( private val coreCallbackHandler: CoreCallbackHandler, private val featureAHandler: FeatureAHandler, private val featureBHandler: FeatureBHandler, private val featureCHandler: FeatureCHandler, private val featureDHandler: FeatureDHandler, private val featureEHandler: FeatureEHandler,): FeatureA by featureAHandler, FeatureB by featureBHandler, FeatureC by featureCHandler, FeatureD by featureDHandler, FeatureE by featureEHandler {
// Implementing core callbacks but delegating them manually to our implementation override fun onInitialize() { coreCallbackHandler.onInitialize() }
override fun onInitializeSuccess() { coreCallbackHandler.onInitializeSuccess() }}This solves our interface implementation problem. But there is one issue remains: How do we use this class and delegate the feature interface implementations for our activity ?
Injecting the facade and delegating each feature to the facade should work right ?
class OurActivity(val sdkFacade: SDKFacade = SDKFacade() // Again, Assume we made it singleton): FeatureA by sdkFacade,FeatureB by sdkFacade,FeatureC by sdkFacade,FeatureD by sdkFacade,FeatureE by sdkFacade { // Callback implementation is decoupled}Turns out, it does not work. Since we are extending our activity class with these Feature interfaces, even though we implemented the resolution for multiple override issue of CoreCallbacks, compiler can’t understand that we solved the issue in Facade class. We still need to add overrides for the CoreCallbacks method inside Activity. Or do we ?
Delegation by an… Interface?
Turns out, we can fix this issue with a simple addition, another interface!
The core problem is separated interfaces with shared function set, if we define a new interface that gathers the functionality, and implement this interface through our Facade, we can inject the Facade to the activity and offload the whole SDK implementation away from the Activity.
In a way we are reverting the refactor, but it lets us extend the Activity without any issue.
// - Part 1
// Define the virtual interfaceinterface SDKOwner : FeatureA, FeatureB, FeatureC, FeatureD, FeatureE
// - Part 2
class SDKFacade( private val coreCallbackHandler: CoreCallbackHandler, private val featureAHandler: FeatureAHandler, private val featureBHandler: FeatureBHandler, private val featureCHandler: FeatureCHandler, private val featureDHandler: FeatureDHandler, private val featureEHandler: FeatureEHandler,): SDKOwner, // <-- We can just add this because its implementation is already delegated and ready. FeatureA by featureAHandler, FeatureB by featureBHandler, FeatureC by featureCHandler, FeatureD by featureDHandler, FeatureE by featureEHandler {
// ... CoreCallback overrides}
// - Part 3class OurActivity(val sdkFacade: SDKFacade = SDKFacade() // Again, Assume we made it singleton): SDKOwner by sdkFacade { // Callback implementation is decoupled}Now, if we need to give activity context to the SDK to initialize it, it will correctly acknowledge that it has the necessary callback implementations because SDKOwner conforms all of the Feature Callbacks + CoreCallback resolution through our Facade!
Here is a small implementation sample that shows that we correctly implemented all features to our ConsumerActivity ✅
And our Facade uses the delegation and resolution of manual override
Conclusion
Decoupling things has a trade off like most things. Here we added an additional layer to our implementation but gained the flexibility of moving these feature handlers into their respective modules if we want to. I am sure there are more elegant ways to tackle this implementation issue as well. But, for our use-case, it will be easy to maintain and test for the future. (Fingers crossed)
