While we all talk about different architectures, there are other important and simple things that can be done to keep code clean and manageable in the long run. In this article, I will share how adding abstraction for using external libraries can benefit in the long run and make your code more resilient to changes when decisions to remove or replace external libraries are taken.
We don’t use libraries because of the features they provide we use libraries because we need certain features to be fulfilled.
So there are things which as a product/developer you want to have control on it can be enabling and disabling features on the fly, enabling for certain cities, user segment, showing different messages for different users, changing messages, etc. so there are multiple ways of doing it and Firebase Remote Config does that for you. One can check how to set up Firebase remote config and other use-cases here.
As a developer, we need something which helps us auto-sync our changes on the fly, and data can be primitive or of a specific type while auto-sync is something that the library provides and maintains for us we are focused only consuming.
Once you decide you want to use any particular library you can go ahead add it to your project and start using it but it is always better to think how easy it will be for me/someone on the project to change/replace the library without impacting the code. In the case of Firebase Remote Config, it is as easy as below
remoteConfig.getBoolean(key) remoteConfig.getString(key) . . other available options
The above might look tempting but slowly you will see Firebase Remote Config getting into your code everywhere which is not good in the long run.
So we create simple rules for consumption that guard our code for the future.
abstract class IConfigProvider { | |
abstract fun getString(key: String): String | |
abstract fun getBoolean(key: String): Boolean | |
abstract fun getDouble(key: String): Double | |
abstract fun getLong(key: String): Long | |
abstract fun getInt(key: String): Int | |
inline fun <reified T> dataFromJson(data: String?): T? { | |
var configData: T? = null | |
try { | |
configData = Gson().fromJson( | |
data, | |
object : TypeToken<T?>() {}.type | |
) | |
} catch (e: JsonParseException) { | |
e.printStackTrace() | |
} | |
return configData | |
} | |
} |
Here Gson is used and other such or your own impl can be used for converting to specific types
Adding Implementation for this abstraction:
class ConfigProviderImpl(private val remoteConfig: FirebaseRemoteConfig) : | |
IConfigProvider() { | |
override fun getString(key: String): String { | |
return remoteConfig.getString(key) | |
} | |
override fun getBoolean(key: String): Boolean { | |
return remoteConfig.getBoolean(key) | |
} | |
override fun getDouble(key: String): Double { | |
return remoteConfig.getDouble(key) | |
} | |
//and other implementations | |
} |
Config Provider which wraps the required options and usages for its consumer
Job Offers
Start using it now with simple data providers one can create different data providers based on different feature modules and use them. The consumers of this implementation are least concerned about the underlying implementation of the library itself and we won’t even need to add Firebase Remote Config dependencies in Gradle for other modules.
/** | |
* common remote config data source for configs used in more than 1 module | |
*/ | |
class CommonRemoteConfigData(private val config: IConfigProvider) { | |
fun getListOfEnabledCities(): List<String>? { | |
return config.dataFromJson( | |
config.getString( | |
"SOMEKEY" | |
) | |
) | |
} | |
fun isServiceEnabled(): Boolean { | |
return config.getBoolean("SOMEKEY") | |
} | |
// and other such implementations. | |
} |
Simple usage without even knowing what is being used for this feature
In the same way, other modules can create a config data source and use the feature which will lead to further separation.
Benefits of such implementation:
- Library-related code is not scattered in the code.
- The library becomes plug and play
- Developers can move out of Firebase Remote Config and add new implementation and it will not require code changes given we adhere to rules.
- We hide features that we don’t want of the library as the user can only use the available options.
We can write simple wrapper around the remote config provider as well and use instead of this implementation
Another use case where such abstraction can be implemented is image libraries. As these features deal with specific functionality with image libraries we want to load the image in a specific view and have some caching mechanism and it should be able to load images from drawable, network, files, etc so creating simple abstraction with this thought process can help in the long run and we can keep changing libraries without impacting our code e.g. initially we think Picasso is good and later Glide, Coil or any other library works better in some scenarios so changing it will be way easier.
That’s it for now 🙂 If this helped you please share it with others as well and do give some claps. Also, do share your feedback as well.
This article was originally published on proandroiddev.com on March 24, 2022