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

  1. 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.

  2. 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 the google-services.json file and place it in the app/ 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'
    }
  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:

  1. Data Layer: Handles data operations, including interactions with Firebase Remote Config.

  2. Domain Layer: Contains business logic and use cases.

  3. 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:

  1. Dynamic Configurations: Easily update app behavior and appearance without requiring user updates.

  2. Clean Codebase: Separation of concerns ensures that each layer has a distinct responsibility.

  3. Scalability: As your app grows, adding new features or modifying existing ones becomes straightforward.

  4. 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! 🌟

Check out my Instagram page

Check out my LinkedIn

0
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!