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.
- 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
| 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 |
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)
}- Minimum Android SDK: 23
- Kotlin Coroutines: Required for async operations
- Jetpack Compose: Required for proteus-ui module only
Get Proteus up and running in minutes with this minimal example:
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()
}
}val provider = Proteus.getInstance().buildConfigProvider()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")startActivity(Intent(this, FeatureBookActivity::class.java))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()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()class FeatureManager(
private val remoteConfig : FirebaseRemoteConfig
) {
fun isDarkModeEnabled(): Boolean {
return remoteConfig.getBoolean("dark_mode_enabled")
}
fun getApiUrl(): String {
return remoteConfig.getString("primary_server_url")
}
}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")
}
}| 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 |
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
- 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
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
}The override mechanism follows a simple priority system:
- Check Local Override: First checks
MockConfigProviderfor user-overridden values - Fallback to Remote: If no override exists, fetches from remote provider (Firebase, etc.)
- Type Safety:
ConfigValuesealed class ensures type-safe value handling - 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
}
}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
)- Initialization: App registers providers and data sources with
Proteus.Builder - Feature Discovery:
FeatureBookDataSourceprovides all feature definitions - Value Resolution: Provider checks overrides, then remote sources
- Runtime Override: UI allows instant value changes during development
- Persistence: Changes are saved and restored on next app launch
Proteus supports both asynchronous (suspend functions) and synchronous APIs:
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...
}
}
}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...
}
}
}@Composable
fun FeatureScreen() {
var isEnabled by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
val provider = Proteus.getInstance().buildConfigProvider()
isEnabled = provider.getBoolean("feature_enabled")
}
if (isEnabled) {
NewFeatureContent()
}
}// For non-coroutine contexts or legacy code
val syncProvider = Proteus.getInstance().buildSynchronousConfigProvider()
val isEnabled = syncProvider.getBoolean("feature_enabled")Create your own configuration provider for any backend system:
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
}
}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"
}
}Proteus.Builder(context)
.registerConfigProviderFactory(CustomProviderFactory(apiClient))
.registerFeatureBookDataSource(dataSource)
.build()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()
}
}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 { *; }- 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
We welcome contributions! Please see our Contributing Guide for details on:
- Code style and guidelines
- Pull request process
- Issue reporting
- Development environment setup
- Bug Reports: Create an issue
- Feature Requests: Request a feature
- Questions: Ask in Discussions
- GitHub: @maxim-petlyuk
- Issues: proteus/issues
Support it by joining stargazers for this repository. ⭐ And follow me for my next creations! 🤩
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.


