Unlocking the Power of Firebase Remote Config in Android Kotlin with Clean Architecture
Hello, wonderful devs! I am your app Sensei, and today, we're going on an exciting journey to explore Firebase Remote Config in the context of Android development using Kotlin and the principles of Clean Architecture. Whether you're building your first app or looking to refine your development practices, let’s start by seeing how we can make our apps smarter and more dynamic without the hassle!
What is Firebase Remote Config?
Okay, devs! Now imagine being able to change your app's look or how it works without needing to release a new update to the app store. Sounds like magic, right? Well, that's exactly what Firebase Remote Config (FRC) offers! 🎩✨
Firebase Remote Config allows developers to change the app’s behavior and appearance on the fly by fetching values from the Firebase server. Whether it’s updating the app’s theme, enabling/disabling features, or modifying content, FRC provides a seamless way to keep your app fresh and responsive to your users' needs without requiring them to download updates constantly.
Why Clean Architecture?
Whenever we start a project, having a solid architecture is essential to keep things organized. Clean Architecture is perfect for larger projects. Think of it as the strong foundation of a house—it makes sure everything built on top is solid, easy to maintain, and can grow with your needs. In Android development, following Clean Architecture principles means dividing your code into separate layers, each with its own job. This makes your code easier to read, test, and maintain as your app expands.
By using Firebase Remote Config within a Clean Architecture setup, we keep our configuration logic tidy, separate from the UI, and easy to handle. Let's dive into how we can set this up!
Project Setup
Step 1: Setting Up Firebase in Your Android Project
Create a Firebase Project: First, let's open the Firebase Console on the web. Once you're there, we'll create a new project and follow the setup wizard to get everything started.
Add Firebase to Your Android App:
Register Your App: Now that we've created the project in the Firebase console, it's time to register our Android app by entering our app's package name. You can find your package name in the manifest file.
Download
google-services.json
: After registration, download thegoogle-services.json
file and place it in theapp/
directory of our Android project.Add Firebase Dependencies: After finishing all those steps, let's update our
build.gradle
files to include the Firebase dependencies.
// Project-level build.gradle
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
// App-level build.gradle
apply plugin: 'com.google.gms.google-services'
dependencies {
implementation 'com.google.firebase:firebase-config-ktx:21.4.1'
implementation 'com.google.firebase:firebase-analytics-ktx:21.4.1'
}
Initialize Firebase in Your App: Typically done in the
Application
class.class MyApplication : Application() { override fun onCreate() { super.onCreate() FirebaseApp.initializeApp(this) } }
Step 2: Structuring with Clean Architecture
To maintain a clean and organized codebase, we'll divide our project into three main layers:
Data Layer: Handles data operations, including interactions with Firebase Remote Config.
Domain Layer: Contains business logic and use cases.
Presentation Layer: Manages UI and user interactions.
Implementing Firebase Remote Config
1. Data Layer
This layer is responsible for fetching and managing configuration parameters from Firebase.
RemoteConfigService.kt: This service interacts directly with Firebase Remote Config.
interface RemoteConfigService { suspend fun fetchRemoteConfigs() fun getBoolean(key: String): Boolean fun getString(key: String): String // Add more getters as needed } class RemoteConfigServiceImpl( private val firebaseRemoteConfig: FirebaseRemoteConfig ) : RemoteConfigService { override suspend fun fetchRemoteConfigs() { try { firebaseRemoteConfig.fetchAndActivate().await() Log.d("RemoteConfigService", "Configs fetched and activated") } catch (e: Exception) { Log.e("RemoteConfigService", "Error fetching remote configs", e) } } override fun getBoolean(key: String): Boolean { return firebaseRemoteConfig.getBoolean(key) } override fun getString(key: String): String { return firebaseRemoteConfig.getString(key) } }
RemoteConfigRepository.kt: This class Acts as an intermediary between the service and the domain layer.
interface RemoteConfigRepository { suspend fun updateConfigs() fun isFeatureEnabled(featureKey: String): Boolean fun getWelcomeMessage(): String // Add more methods as per your app's needs } class RemoteConfigRepositoryImpl( private val remoteConfigService: RemoteConfigService ) : RemoteConfigRepository { override suspend fun updateConfigs() { remoteConfigService.fetchRemoteConfigs() } override fun isFeatureEnabled(featureKey: String): Boolean { return remoteConfigService.getBoolean(featureKey) } override fun getWelcomeMessage(): String { return remoteConfigService.getString("welcome_message") } }
2. Domain Layer
This layer includes the business logic and use cases that define how the app behaves.
UpdateRemoteConfigsUseCase.kt: A use case to fetch and update remote configurations.
class UpdateRemoteConfigsUseCase( private val remoteConfigRepository: RemoteConfigRepository ) { suspend operator fun invoke() { remoteConfigRepository.updateConfigs() } }
GetFeatureStatusUseCase.kt: Determines if a particular feature should be enabled based on remote config.
class GetFeatureStatusUseCase( private val remoteConfigRepository: RemoteConfigRepository ) { fun execute(featureKey: String): Boolean { return remoteConfigRepository.isFeatureEnabled(featureKey) } }
GetWelcomeMessageUseCase.kt: Retrieves a welcome message from remote config.
class GetWelcomeMessageUseCase( private val remoteConfigRepository: RemoteConfigRepository ) { fun execute(): String { return remoteConfigRepository.getWelcomeMessage() } }
3. Presentation Layer
This is where the UI components interact with the ViewModel to reflect changes based on remote configurations.
MainViewModel.kt: The ViewModel that manages UI-related data.
class MainViewModel( private val updateRemoteConfigsUseCase: UpdateRemoteConfigsUseCase, private val getFeatureStatusUseCase: GetFeatureStatusUseCase, private val getWelcomeMessageUseCase: GetWelcomeMessageUseCase ) : ViewModel() { private val _welcomeMessage = MutableLiveData<String>() val welcomeMessage: LiveData<String> get() = _welcomeMessage private val _isNewFeatureEnabled = MutableLiveData<Boolean>() val isNewFeatureEnabled: LiveData<Boolean> get() = _isNewFeatureEnabled init { fetchConfigs() } private fun fetchConfigs() { viewModelScope.launch { updateRemoteConfigsUseCase() _welcomeMessage.value = getWelcomeMessageUseCase.execute() _isNewFeatureEnabled.value = getFeatureStatusUseCase.execute("new_feature_enabled") } } }
MainActivity.kt: Observe the ViewModel and update the UI accordingly.
class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) val welcomeTextView: TextView = findViewById(R.id.welcomeTextView) val newFeatureButton: Button = findViewById(R.id.newFeatureButton) viewModel.welcomeMessage.observe(this, { message -> welcomeTextView.text = message }) viewModel.isNewFeatureEnabled.observe(this, { isEnabled -> newFeatureButton.visibility = if (isEnabled) View.VISIBLE else View.GONE }) } }
Dependency Injection with Koin
To manage dependencies efficiently and keep our code clean, we'll use Koin for dependency injection.
AppModule.kt:
val appModule = module { // Provide FirebaseRemoteConfig instance single { FirebaseRemoteConfig.getInstance().apply { setConfigSettingsAsync( FirebaseRemoteConfigSettings.Builder() .setMinimumFetchIntervalInSeconds(3600) // 1 hour .build() ) setDefaultsAsync(R.xml.remote_config_defaults) } } // Remote Config Service single<RemoteConfigService> { RemoteConfigServiceImpl(get()) } // Remote Config Repository single<RemoteConfigRepository> { RemoteConfigRepositoryImpl(get()) } // Use Cases single { UpdateRemoteConfigsUseCase(get()) } single { GetFeatureStatusUseCase(get()) } single { GetWelcomeMessageUseCase(get()) } // ViewModel viewModel { MainViewModel(get(), get(), get()) } }
MyApplication.kt:
class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } }
remote_config_defaults.xml: Default values for Remote Config parameters.
<?xml version="1.0" encoding="utf-8"?> <defaultsMap> <entry> <key>welcome_message</key> <value>Welcome to our App!</value> </entry> <entry> <key>new_feature_enabled</key> <value>false</value> </entry> </defaultsMap>
Testing Your Implementation
Did you know devs One of the beauties of Clean Architecture is how it simplifies testing. Since our business logic is decoupled from the Android framework, we can easily write unit tests for our use cases and repositories.
RemoteConfigRepositoryTest.kt:
@RunWith(MockitoJUnitRunner::class) class RemoteConfigRepositoryTest { @Mock private lateinit var remoteConfigService: RemoteConfigService private lateinit var repository: RemoteConfigRepository @Before fun setUp() { repository = RemoteConfigRepositoryImpl(remoteConfigService) } @Test fun `updateConfigs should call fetchRemoteConfigs`() = runBlocking { repository.updateConfigs() verify(remoteConfigService).fetchRemoteConfigs() } @Test fun `isFeatureEnabled should return correct value`() { `when`(remoteConfigService.getBoolean("test_feature")).thenReturn(true) val result = repository.isFeatureEnabled("test_feature") assertTrue(result) } @Test fun `getWelcomeMessage should return correct string`() { `when`(remoteConfigService.getString("welcome_message")).thenReturn("Hello World!") val result = repository.getWelcomeMessage() assertEquals("Hello World!", result) } }
MainViewModelTest.kt:
@RunWith(MockitoJUnitRunner::class) class MainViewModelTest { @Mock private lateinit var updateRemoteConfigsUseCase: UpdateRemoteConfigsUseCase @Mock private lateinit var getFeatureStatusUseCase: GetFeatureStatusUseCase @Mock private lateinit var getWelcomeMessageUseCase: GetWelcomeMessageUseCase private lateinit var viewModel: MainViewModel @Before fun setUp() { viewModel = MainViewModel( updateRemoteConfigsUseCase, getFeatureStatusUseCase, getWelcomeMessageUseCase ) } @Test fun `fetchConfigs should update welcomeMessage and isNewFeatureEnabled`() = runBlocking { `when`(getWelcomeMessageUseCase.execute()).thenReturn("Hello Test!") `when`(getFeatureStatusUseCase.execute("new_feature_enabled")).thenReturn(true) viewModel.fetchConfigs() assertEquals("Hello Test!", viewModel.welcomeMessage.getOrAwaitValue()) assertTrue(viewModel.isNewFeatureEnabled.getOrAwaitValue()) } }
Note: getOrAwaitValue
is a utility function to get LiveData values in tests.
Bringing It All Together
By integrating Firebase Remote Config with Clean Architecture, we've created a strong system that allows you dynamic updates and easy maintenance. Here's a quick recap of what we've achieved:
Dynamic Configurations: Easily update app behavior and appearance without requiring user updates.
Clean Codebase: Separation of concerns ensures that each layer has a distinct responsibility.
Scalability: As your app grows, adding new features or modifying existing ones becomes straightforward.
Testability: Decoupled components mean that writing unit tests is hassle-free, ensuring your app remains reliable.
Using Firebase Remote Config with a Clean Architecture setup makes your app super flexible and keeps your codebase strong against the challenges of growth and change. It's like giving your app a smart brain that can adapt and grow, all while ensuring its foundation stays solid and reliable.
I hope this article has been helpful and has given you the confidence to use Firebase Remote Config in your own Android projects with Kotlin. Remember, learning is so much more fun when we're part of a supportive community. So, don't hesitate to share your experiences, ask questions, or leave feedback in the comments below. Let's keep building amazing apps together! See you on the next blog.
Connect with Me:
Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. 📲👨💻 Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟
Subscribe to my newsletter
Read articles from Mayursinh Parmar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mayursinh Parmar
Mayursinh Parmar
📱Mobile App Developer | Android & Flutter 🌟💡 Passionate about creating intuitive and engaging apps 💭✨ Let’s shape the future of mobile technology!