Skip to content

Implies flexibility in configuration management, ideal for A/B testing scenarios.

License

Notifications You must be signed in to change notification settings

maxim-petlyuk/proteus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

116 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

proteus

License API Build Status Documentation Profile
Maven Central: proteus-core Maven Central: proteus-firebase Maven Central: proteus-ui Maven Central: proteus-bom

Proteus is an Android library for managing remote configurations with runtime override capabilities. It provides a type-safe abstraction layer over remote config providers like Firebase, enabling teams to test feature flags and A/B variants instantly without server deployments. The built-in Material Design 3 UI allows you to modify configurations directly on your device during development and testing. With multi-module architecture and BOM support, Proteus seamlessly integrates into any Android application.

Why Proteus?

The Problem: Testing remote configurations requires server deployments, slowing down your development cycle and making A/B testing tedious.

The Solution: Proteus provides a runtime override UI that lets you instantly test any configuration scenario without waiting for backend changes.

Unique Value: Test feature flags, A/B variants, and remote configs in real-time directly on your device - no server round-trips required.

How it looks like?

Enable mock config Disable mock config

Key Features

  • Runtime Configuration Override - Modify remote configs instantly through a polished local UI
  • Multi-Provider Support - Seamlessly integrate with Firebase, CleverTap, or custom providers
  • Material Design 3 - Beautiful beige-themed UI that follows the latest design guidelines
  • Multi-Module Architecture - Independent versioning with BOM support for simplified dependency management
  • Production-Ready - Comprehensive error handling, async/await support with coroutines, and thread-safe operations

Packages

Package Description
proteus-core Core abstraction layer for remote configuration providers
proteus-firebase Firebase Remote Config provider implementation
proteus-ui Material Design 3 UI for runtime configuration overrides
proteus-bom Bill of Materials for consistent version management

Installation

Using BOM (Recommended)

Maven Central: proteus-bom

The Bill of Materials (BOM) ensures all Proteus modules use compatible versions:

[versions]
proteus = "$version"

[libraries]
proteus-bom = { module = "io.github.maxim-petlyuk:proteus-bom", version.ref = "proteus" }
proteus-core = { module = "io.github.maxim-petlyuk:proteus-core" }
proteus-firebase = { module = "io.github.maxim-petlyuk:proteus-firebase" }
proteus-ui = { module = "io.github.maxim-petlyuk:proteus-ui" }

[bundles]
proteus = ["proteus-core", "proteus-firebase", "proteus-ui"]

Then in your build.gradle.kts:

dependencies {
    implementation(platform(libs.proteus.bom))
    implementation(libs.bundles.proteus)
}

Requirements

  • Minimum Android SDK: 23
  • Kotlin Coroutines: Required for async operations
  • Jetpack Compose: Required for proteus-ui module only

Quick Start

Get Proteus up and running in minutes with this minimal example:

1. Initialized first:

class MainApp : Application() {

    override fun onCreate() {
        super.onCreate()

        /* IMPORTANT: Initialize Firebase Remote Config BEFORE using FirebaseOnlyProviderFactory */

        // Initialize Proteus with your feature definitions
        Proteus.Builder(this)
            .registerConfigProviderFactory(FirebaseOnlyProviderFactory())
            .registerFeatureBookDataSource(
                // Option A: Load from assets/features.json
                AssetsFeatureBookDataSource(this, "features.json")
                // Option B: Use runtime code
                // StaticFeatureBookDataSource()
            )
            .build()
    }
}

2. Build Proteus config provider

val provider = Proteus.getInstance().buildConfigProvider()

3. Access configuration values anywhere in your app

Async Usage (Recommended):

// In Activity/Fragment
lifecycleScope.launch {
    val darkModeEnabled = provider.getBoolean("dark_mode_enabled")
    val maxItems = provider.getLong("max_items_per_page")
    // Use configuration values...
}

Synchronous Usage (Legacy Compatibility):

// For non-coroutine contexts
val syncProvider = Proteus.getInstance().buildSynchronousConfigProvider()
val darkModeEnabled = syncProvider.getBoolean("dark_mode_enabled")
val maxItems = syncProvider.getLong("max_items_per_page")

4. Show Proteus library UI for overriding remote config

startActivity(Intent(this, FeatureBookActivity::class.java))

Data Source Options

Option A: JSON file in assets folder

Create assets/features.json:

[
  {
    "feature_key": "dark_mode_enabled",
    "default_value": "false",
    "value_type": "boolean"
  },
  {
    "feature_key": "max_items_per_page",
    "default_value": "20",
    "value_type": "long"
  }
]

Then register it during initialization:

Proteus.Builder(this)
    .registerConfigProviderFactory(FirebaseOnlyProviderFactory())
    .registerFeatureBookDataSource(AssetsFeatureBookDataSource(context = this, jsonFilePath = "features.json"))
    .build()

Option B: Runtime code

Create a custom data source with your feature definitions:

class SampleFeatureBookDataSource : FeatureBookDataSource {

    override suspend fun getFeatureBook(): Result<List<FeatureContext<*>>> {
        return Result.success(
            listOf(
                Feature(
                    key = "dark_mode_enabled",
                    defaultValue = false,
                    valueClass = Boolean::class
                ),
                Feature(
                    key = "max_items_per_page",
                    defaultValue = 20L,
                    valueClass = Long::class
                ),
                Feature(
                    key = "api_timeout_seconds",
                    defaultValue = 30L,
                    valueClass = Long::class
                ),
                Feature(
                    key = "animation_duration_multiplier",
                    defaultValue = 1.0,
                    valueClass = Double::class
                ),
                Feature(
                    key = "primary_server_url",
                    defaultValue = "https://api.production.com",
                    valueClass = String::class
                )
            )
        )
    }
}

Then register it during initialization:

Proteus.Builder(this)
    .registerConfigProviderFactory(FirebaseOnlyProviderFactory())
    .registerFeatureBookDataSource(SampleFeatureBookDataSource())
    .build()

Firebase Remote Config Migration

Before: Direct Firebase Remote Config Usage

class FeatureManager(
    private val remoteConfig : FirebaseRemoteConfig
) {

    fun isDarkModeEnabled(): Boolean {
        return remoteConfig.getBoolean("dark_mode_enabled")
    }

    fun getApiUrl(): String {
        return remoteConfig.getString("primary_server_url")
    }
}

After: Using Proteus

class FeatureManager(
    private val configProvider: FeatureConfigProvider
) {

    // Async approach (recommended)
    suspend fun isDarkModeEnabled(): Boolean {
        return configProvider.getBoolean("dark_mode_enabled")
    }

    // Or use synchronous wrapper for legacy code
    fun isDarkModeEnabledSync(): Boolean {
        val syncProvider = Proteus.getInstance().buildSynchronousConfigProvider()
        return syncProvider.getBoolean("dark_mode_enabled")
    }

    suspend fun getApiUrl(): String {
        return configProvider.getString("primary_server_url")
    }
}

Key Benefits

Aspect Before (Firebase only) After (Proteus)
Testing Speed Minutes (Firebase Console changes) Instant (runtime UI)
Override Capability None Full runtime override
A/B Testing Requires Firebase Console setup Test locally first
Debug Features Switch between Firebase Console & app Visual UI directly in app
Development Workflow Firebase Console dependent Independent local testing

Core Concepts

Architecture Overview

Proteus uses a layered architecture with runtime override capabilities:

graph TB
    App[Your App]
    UI[Runtime Override UI]
    Core[Proteus Core<br/>FeatureConfigProvider]
    Mock[MockConfigProvider]
    Storage[(SharedPreferences<br/>Local Overrides)]
    Firebase[Firebase Remote Config]
    CleverTap[CleverTap]
    Custom[Custom Provider]

    App --> Core
    UI --> Core
    Core --> Mock
    Mock --> Storage
    Core -.fallback.-> Firebase
    Core -.fallback.-> CleverTap
    Core -.fallback.-> Custom

    style App fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style UI fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style Core fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    style Mock fill:#fce4ec,stroke:#880e4f,stroke-width:2px
    style Storage fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
    style Firebase fill:#fff8e1,stroke:#f57f17,stroke-width:2px
    style CleverTap fill:#fff8e1,stroke:#f57f17,stroke-width:2px
    style Custom fill:#fff8e1,stroke:#f57f17,stroke-width:2px
Loading

Key Components

  • Proteus - Central singleton that coordinates the entire system
  • FeatureConfigProvider - Type-safe interface for configuration access
  • FeatureBookDataSource - Defines all available features and their metadata
  • MockConfigProvider - Enables runtime overrides via local storage
  • ConfigValue - Type-safe wrapper ensuring correct value types

Provider Abstraction

All configuration providers implement a simple, type-safe interface:

interface FeatureConfigProvider {
    suspend fun getBoolean(featureKey: String): Boolean
    suspend fun getString(featureKey: String): String
    suspend fun getLong(featureKey: String): Long
    suspend fun getDouble(featureKey: String): Double
}

Runtime Override Mechanism

The override mechanism follows a simple priority system:

  1. Check Local Override: First checks MockConfigProvider for user-overridden values
  2. Fallback to Remote: If no override exists, fetches from remote provider (Firebase, etc.)
  3. Type Safety: ConfigValue sealed class ensures type-safe value handling
  4. Persistence: Overrides are persisted in SharedPreferences across app sessions
// How FeatureConfigProviderImpl resolves values
suspend fun getBoolean(featureKey: String): Boolean {
    return try {
        mockConfigProvider.getBoolean(featureKey)  // Check override first
    } catch (e: MockConfigUnavailableException) {
        remoteProvider.getBoolean(featureKey)      // Fallback to remote
    }
}

Feature Definition

Features are defined with strong typing and default values:

data class Feature<DataType : Any>(
    val key: String,                    // Unique identifier
    val defaultValue: DataType,         // Fallback value
    val valueClass: KClass<DataType>    // Type information
)

// Example usage
Feature(
    key = "dark_mode_enabled",
    defaultValue = false,
    valueClass = Boolean::class
)

Configuration Lifecycle

  1. Initialization: App registers providers and data sources with Proteus.Builder
  2. Feature Discovery: FeatureBookDataSource provides all feature definitions
  3. Value Resolution: Provider checks overrides, then remote sources
  4. Runtime Override: UI allows instant value changes during development
  5. Persistence: Changes are saved and restored on next app launch

Coroutines Usage Patterns

Proteus supports both asynchronous (suspend functions) and synchronous APIs:

In Activities/Fragments

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            val provider = Proteus.getInstance().buildConfigProvider()
            val isEnabled = provider.getBoolean("feature_enabled")
            // Use configuration...
        }
    }
}

In ViewModels

class FeatureViewModel : ViewModel() {
    fun loadConfiguration() {
        viewModelScope.launch {
            val provider = Proteus.getInstance().buildConfigProvider()
            val config = AppConfig(
                isEnabled = provider.getBoolean("feature_enabled"),
                timeout = provider.getLong("timeout_ms")
            )
            // Update UI state...
        }
    }
}

In Compose

@Composable
fun FeatureScreen() {
    var isEnabled by remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        val provider = Proteus.getInstance().buildConfigProvider()
        isEnabled = provider.getBoolean("feature_enabled")
    }

    if (isEnabled) {
        NewFeatureContent()
    }
}

Synchronous API (Legacy Compatibility)

// For non-coroutine contexts or legacy code
val syncProvider = Proteus.getInstance().buildSynchronousConfigProvider()
val isEnabled = syncProvider.getBoolean("feature_enabled")

Advanced Features

Custom Provider Implementation

Create your own configuration provider for any backend system:

1. Implement the FeatureConfigProvider Interface

class CustomConfigProvider(
    private val apiClient: YourApiClient
) : FeatureConfigProvider {

    override suspend fun getBoolean(featureKey: String): Boolean = withContext(Dispatchers.IO) {
        apiClient.getConfig(featureKey)?.toBoolean() ?: false
    }

    override suspend fun getString(featureKey: String): String = withContext(Dispatchers.IO) {
        apiClient.getConfig(featureKey) ?: ""
    }

    override suspend fun getLong(featureKey: String): Long = withContext(Dispatchers.IO) {
        apiClient.getConfig(featureKey)?.toLongOrNull() ?: 0L
    }

    override suspend fun getDouble(featureKey: String): Double = withContext(Dispatchers.IO) {
        apiClient.getConfig(featureKey)?.toDoubleOrNull() ?: 0.0
    }
}

2. Create a Provider Factory

class CustomProviderFactory(
    private val apiClient: YourApiClient
) : FeatureConfigProviderFactory {

    private val provider = CustomConfigProvider(apiClient)

    override fun getProvider(featureKey: String): FeatureConfigProvider {
        return provider
    }

    override fun getProviderTag(featureKey: String): String {
        return "custom"
    }
}

3. Register with Proteus

Proteus.Builder(context)
    .registerConfigProviderFactory(CustomProviderFactory(apiClient))
    .registerFeatureBookDataSource(dataSource)
    .build()

Dependency Injection Setup

Using Hilt/Dagger for dependency injection:

@Module
@InstallIn(SingletonComponent::class)
object ProteusModule {

    @Provides
    @Singleton
    fun provideProteus(@ApplicationContext context: Context): Proteus {
        return Proteus.Builder(context)
            .registerConfigProviderFactory(FirebaseOnlyProviderFactory())
            .registerFeatureBookDataSource(
                AssetsFeatureBookDataSource(context, "features.json")
            )
            .build()
    }

    @Provides
    @Singleton
    fun provideFeatureConfigProvider(proteus: Proteus): FeatureConfigProvider {
        return proteus.buildConfigProvider()
    }
}

ProGuard/R8 Configuration

Proteus modules include consumer ProGuard rules automatically. For additional safety, you can add:

# Proteus
-keep class io.proteus.** { *; }
-keep class io.proteus.core.domain.** { *; }
-keepattributes *Annotation*

# Keep your custom providers
-keep class com.yourpackage.CustomConfigProvider { *; }
-keep class com.yourpackage.CustomProviderFactory { *; }

Performance Considerations

  • Caching: Provider implementations should cache values to avoid repeated network/disk operations
  • Lazy Loading: Initialize Proteus in Application.onCreate() for immediate availability
  • Thread Safety: All Proteus providers are thread-safe by design
  • Memory Usage: Override values in SharedPreferences have minimal memory impact
  • Network Optimization: Batch fetch configurations when using custom API providers

Community

Contributing

We welcome contributions! Please see our Contributing Guide for details on:

  • Code style and guidelines
  • Pull request process
  • Issue reporting
  • Development environment setup

Issues and Support

Connect

Find this library useful? ⭐

Support it by joining stargazers for this repository. ⭐ And follow me for my next creations! 🤩

License

Copyright 2025 Maxim Petlyuk

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.