Resistance is futile - your components will be assimilated in perfect order.
π About Borg is a high-performance Android initialization orchestrator that brings order to your app's startup chaos. It automatically manages complex dependency graphs, parallelizes independent initializations, and provides bulletproof thread safety - all with the elegance of Kotlin coroutines.
Check out the article on Medium
- Why Borg?
- Key Features
- Installation
- Quick Start
- Advanced Usage
- Best Practices
- Error Handling
- Testing
- Comparison with Alternatives
- Contributing
- License
Modern Android apps face complex initialization challenges:
- β Race conditions in component initialization
- β Unclear dependency ordering
- β Blocking main thread during setup
- β Hard to test initialization logic
- β Difficult error recovery
- β Poor performance from sequential initialization
- β Thread-safe, deterministic initialization
- β Automatic dependency resolution
- β Non-blocking coroutine-based setup
- β Easy to test with constructor injection
- β Structured error handling
- β Maximum parallel initialization
- Type-Safe Dependencies: Compile-time verification of dependency graph
- Parallel Initialization: Automatic parallelization of independent components
- Coroutine Support: Native suspend function support for async operations
- Thread Safety: Bulletproof concurrency handling with deadlock prevention
- Error Handling: Rich exception hierarchy with detailed context
- Testing Support: Easy mocking and testing through constructor injection
- Performance: Optimal initialization order with parallel execution
- Flexibility: Generic context type for any initialization needs
allprojects {
repositories {
mavenCentral()
}
}
dependencies {
implementation 'net.kibotu:Borg:{latest-version}'
}
- Add JitPack repository:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
}
}
- Add the dependency:
// build.gradle.kts
dependencies {
implementation("com.github.kibotu:Borg:latest-version")
}
Create a drone for each component that needs initialization:
// 1. Simple configuration
class ConfigDrone : BorgDrone<AppConfig, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>) =
AppConfig.load(context.assets.open("config.json"))
}
// 2. Database with config dependency
class DatabaseDrone : BorgDrone<AppDatabase, Context> {
override fun requiredDrones() = listOf(ConfigDrone::class.java)
override suspend fun assimilate(context: Context, borg: Borg<Context>): AppDatabase {
val config = borg.requireAssimilated(ConfigDrone::class.java)
return Room.databaseBuilder(context, AppDatabase::class.java, config.dbName)
.build()
}
}
// 3. Repository combining multiple dependencies
class RepositoryDrone : BorgDrone<Repository, Context> {
override fun requiredDrones() = listOf(
DatabaseDrone::class.java,
ApiDrone::class.java
)
override suspend fun assimilate(context: Context, borg: Borg<Context>) = Repository(
database = borg.requireAssimilated(DatabaseDrone::class.java),
api = borg.requireAssimilated(ApiDrone::class.java)
)
}
class App : Application() {
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun onCreate() {
super.onCreate()
lifecycleScope.launch {
try {
// Create and initialize the collective
val borg = Borg(setOf(
ConfigDrone(),
DatabaseDrone(),
ApiDrone(),
RepositoryDrone()
))
// Assimilate all components
borg.assimilate(applicationContext)
// Store initialized components
appContainer.repository = borg.requireAssimilated(RepositoryDrone::class.java)
} catch (e: BorgException) {
handleInitializationError(e)
}
}
}
}
Borg automatically parallelizes initialization of independent components:
val drones = setOf(
AnalyticsDrone(), // No dependencies - Parallel
ConfigDrone(), // No dependencies - Parallel
DatabaseDrone(), // Depends on Config - Waits for Config
ApiDrone() // Depends on Config - Waits for Config
)
// Visualization of parallel execution:
// Time β
// Analytics βββββββ
// Config ββββ
// Database ββββ (starts after Config)
// Api ββββ (starts after Config)
Use getAssimilated()
for optional dependencies:
class AnalyticsDrone : BorgDrone<Analytics, Context> {
override fun requiredDrones() = listOf(UserDrone::class.java)
override suspend fun assimilate(context: Context, borg: Borg<Context>): Analytics {
val analytics = FirebaseAnalytics.getInstance(context)
// Optional user identification
borg.getAssimilated(UserDrone::class.java)?.let { user ->
analytics.setUserId(user.id)
}
return analytics
}
}
Implement graceful degradation:
class RemoteConfigDrone : BorgDrone<Config, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>): Config {
return try {
// Try remote config first
FirebaseRemoteConfig.getInstance()
.fetchAndActivate()
.await()
.let { RemoteConfig() }
} catch (e: Exception) {
// Fall back to local config
LocalConfig.fromAssets(context)
}
}
}
To enable logging in Borg, you can use the enableLogging
parameter in the Borg constructor. This will automatically set up logging for all Borg operations.
val borg = Borg(
drones = setOf(
ConfigDrone(),
DatabaseDrone(),
ApiDrone(),
RepositoryDrone()
),
enableLogging = true
)
By setting enableLogging
to true
, Borg will log important lifecycle events and errors, helping you to monitor and debug the initialization process effectively.
You can still configure the logging level and customize the logger as needed, but the enableLogging
parameter provides a quick and easy way to get started with logging in Borg.
// β Bad: Too many responsibilities
class MonolithicDrone : BorgDrone<AppServices, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>): AppServices {
val db = Room.databaseBuilder(/*...*/).build()
val api = Retrofit.Builder().build()
val analytics = FirebaseAnalytics.getInstance(context)
return AppServices(db, api, analytics)
}
}
// β
Good: Single responsibility
class DatabaseDrone : BorgDrone<AppDatabase, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>) =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
}
class ApiDrone : BorgDrone<ApiClient, Context> {
override fun requiredDrones() = listOf(ConfigDrone::class.java)
override suspend fun assimilate(context: Context, borg: Borg<Context>): ApiClient {
try {
val config = borg.requireAssimilated(ConfigDrone::class.java)
// Validate configuration
require(config.apiUrl.isNotBlank()) { "API URL is required" }
return ApiClient(config.apiUrl)
.also { client ->
// Verify connectivity
client.ping()
.await()
.also { response ->
require(response.isSuccessful) {
"API not reachable: ${response.code()}"
}
}
}
} catch (e: Exception) {
throw BorgException.AssimilationException(
drone = this::class.java,
cause = e
)
}
}
}
class RepositoryDrone : BorgDrone<Repository, Context> {
override fun requiredDrones() = listOf(
DatabaseDrone::class.java,
ApiDrone::class.java,
CacheDrone::class.java
)
override suspend fun assimilate(context: Context, borg: Borg<Context>): Repository {
val db = borg.requireAssimilated(DatabaseDrone::class.java)
val api = borg.requireAssimilated(ApiDrone::class.java)
val cache = borg.getAssimilated(CacheDrone::class.java)
return Repository(db, api, cache)
}
}
Borg provides structured error handling:
try {
borg.assimilate(context)
} catch (e: BorgException) {
when (e) {
is BorgException.CircularDependencyException -> {
// Circular dependency detected
Log.e("Borg", "Dependency cycle: ${e.cycle.joinToString(" -> ")}")
}
is BorgException.DroneNotFoundException -> {
// Missing required drone
Log.e("Borg", "${e.drone} requires ${e.requiredDrone}")
}
is BorgException.AssimilationException -> {
// Initialization failed
Log.e("Borg", "Failed to initialize ${e.drone}", e.cause)
}
is BorgException.DroneNotAssimilatedException -> {
// Accessed uninitialized drone
Log.e("Borg", "Drone not ready: ${e.drone}")
}
}
}
Borg is designed for testability:
class RepositoryTest {
@Test
fun `test repository initialization`() = runTest {
// Given
val mockDb = mockk<AppDatabase>()
val mockApi = mockk<ApiClient>()
val dbDrone = object : BorgDrone<AppDatabase, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>) = mockDb
}
val apiDrone = object : BorgDrone<ApiClient, Context> {
override suspend fun assimilate(context: Context, borg: Borg<Context>) = mockApi
}
val repositoryDrone = RepositoryDrone(dbDrone, apiDrone)
// When
val borg = Borg(setOf(dbDrone, apiDrone, repositoryDrone))
borg.assimilate(mockk())
// Then
val repository = repositoryDrone.assimilate(mockk(), borg)
assertNotNull(repository)
}
}
Feature | Borg | androidx.startup |
---|---|---|
Dependency Resolution | β Automatic, type-safe with compile-time validation | β Automatic via dependencies() method |
Parallel Initialization | β Automatic parallel execution of independent components | β Sequential execution only |
Coroutine Support | β Native suspend function support | β Blocking calls only |
Error Handling | β Structured exception hierarchy with dependency context | β Basic exceptions without dependency context |
Thread Safety | β Full thread safety with deadlock prevention | β Basic thread safety |
Initialization Caching | β Thread-safe result caching | β Component-level caching |
Circular Dependency Detection | β Compile-time detection with clear error messages | β Runtime detection |
Testing Support | β Easy to mock with direct instantiation | β Requires ContentProvider mocking |
Lazy Initialization | β On-demand initialization with dependency tracking | β Manual lazy initialization |
Configuration | β Runtime configuration with constructor params | β Manifest metadata only |
Auto-initialization | β Manual Application.onCreate() call | β Automatic via ContentProvider |
Library Size | β Larger due to coroutine support | β Very small footprint |
Choose Borg when you need:
- Type-safe dependency management with compile-time validation
- Maximum performance through parallel initialization
- Async operations with coroutine support
- Rich error context for debugging dependency issues
- Runtime configuration flexibility
- Direct component testing without Android dependencies
- Complex dependency graphs with clear visualization
Choose androidx.startup when:
- You want automatic initialization without Application class changes
- Your initialization chain is relatively simple
- You prefer configuration through AndroidManifest.xml
- Minimal library footprint is critical
- You're strictly following Android component lifecycle
- You don't need async initialization support
Feature | Borg | Dagger/Hilt |
---|---|---|
Focus | Initialization order | Dependency injection |
Learning Curve | π Medium | π Steep |
Compile-time Safety | β Yes | β Yes |
Initialization Control | β Explicit | β Implicit |
Parallel Init | β Automatic | β Manual |
Android Integration | β Native context | β Full framework |
We welcome contributions!
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.
- Inspired by Star Trek's Borg Collective & androidx.startup
- Built with Kotlin Coroutines
- Made with β€οΈ by kibotu