Icarion is a lightweight, extensible migration library designed to handle version-based update migrations for your application. It supports both rollback and recovery mechanisms for fine-grained control over migrations, making it ideal for settings and configuration changes, file migrations, even database updates and more.
Written for Kotlin Multiplatform you can run it on Android, iOS and any JVM based system: Ktor, Spring, Desktop, you name it...Android based projects were the main culprit behind Icarion idea as many times devs would just perform SharedPreferences or FirebaseConfig data updates in the Application onCreate() based on current BuildConfig version value without any long term organization of migrations.
This library is here to help alleviate some of the pain of rolling out your own system of migrations, no matter where you run it.
Inspired by Icarus myth, which is often interpreted as a cautionary tale about ego, self-sabotage, and the consequences of ignoring wise counsel. Icarus, in Greek mythology, son of the inventor Daedalus who perished by flying too near the Sun with waxen wings.
- Version-Based Migrations: Define migrations targeting specific versions.
- Flexible Rollbacks: Support for rollback strategies in case of migration failures.
- Observer Support: Monitor migration progress with callbacks.
- Concurrent Execution Prevention: Ensures no overlapping migrations are executed.
- Custom Recovery Strategies: Handle failures by skipping, aborting, or rolling back migrations.
- Configuration or settings updates
- File system changes
- Data transformations during app updates
- Any stateful application upgrade processes
- Database schema migrations
Add Icarion to your project as a dependency. If you use Gradle, add the following:
repositories {
mavenCentral()
}
dependencies {
implementation("xyz.amplituhedron:icarion:1.1.0")
}
You can use any versioning system that implements Comparable
. Icarion comes with two versioning schemes out of the box for easier integration:
data class IntVersion(val value: Int)
and data class SemanticVersion(val major: Int, val minor: Int, val patch: Int)
val v1 = IntVersion(1)
val v3 = IntVersion(3)
val v1_1_1 = SemanticVersion(1, 1, 1)
val v2_3_0 = SemanticVersion.fromVersion("2.3.0")
Enums can be used as they implement comparable by default via their natural ordering:
enum class YourAppNamedVersion {
ACACIA, // Lowest
BIRCH,
CEDAR,
DOUGLAS_FIR,
OAK,
PINE,
SEQUOIA; // Highest
}
val v1 = YourAppNamedVersion.ACACIA
val v3 = YourAppNamedVersion.CEDAR
To define a migration, you need to implement the AppUpdateMigration
interface. This interface requires you to define:
targetVersion
: The version this migration updates to.migrate
: The logic to apply the migration.rollback
: The logic to revert the migration in case of failure.
class SampleMigration : AppUpdateMigration<IntVersion> {
override val targetVersion = IntVersion(2)
override suspend fun migrate() {
println("Migrating to version $targetVersion")
// Add your migration logic here
}
override suspend fun rollback() {
println("Rolling back version $targetVersion")
// Add your rollback logic here
}
}
class FeatureUpgradeMigrationV110 : AppUpdateMigration<SemanticVersion> {
override val targetVersion = SemanticVersion(1, 1, 0)
override suspend fun migrate() {
println("Upgrading feature to version $targetVersion")
// Add feature-specific migration logic here
// Intentionally failed migrations should throw an exception here, for ex. throw RuntimeException("Can not migrate all data to external storage...")
}
override suspend fun rollback() {
println("Reverting feature upgrade for version $targetVersion")
// Add feature-specific rollback logic here
}
}
Define as many migrations as needed for your application. Each migration should handle only the changes required for its specific version.
Once you've implemented your migrations, register them with an instance of IcarionMigrator
. This ensures the migrator knows which migrations are available for execution.
// Register multiple migrations at once
val migrator = IcarionMigrator<IntVersion>().apply {
registerMigration(FeatureUpgradeMigrationV1())
registerMigration(FeatureUpgradeMigrationV2())
registerMigration(FeatureUpgradeMigrationV3())
registerMigration(FeatureUpgradeMigrationV4())
registerMigration(FeatureUpgradeMigrationV5())
}
// Register individual migrations
migrator.registerMigration(SampleMigration())
• You cannot register migrations while a migration process is running. An IllegalStateException will be thrown if you attempt to do so.
• Each migration must target a unique version. If you register two migrations with the same targetVersion, an IllegalArgumentException will be thrown.
Registering migrations correctly ensures that the migrator can execute the necessary upgrades in the right order.
To execute migrations, invoke the migrateTo method, specifying current and the target version. The migrator will ensure that all migrations between the current and target versions are executed sequentially.
val currentVersion = IntVersion("1")
val targetVersion = IntVersion("5")
val result = migrator.migrateTo(
from = currentVersion,
to = targetVersion
)
In this example he migrator runs all migrations from version 1 up to and including 5.
Migration Result
The IcarionMigrationsResult class encapsulates the outcome of executed migrations, providing a detailed report of the migration process.
Result Types
- Success - Indicates that all migrations have been successfully completed or skipped.
- Fields:
- completedMigrations: A list of successfully completed migrations.
- skippedMigrations: A list of migrations that were skipped (they failed, but you returned Skip from migration observer).
- Fields:
- Failure - Represents a migration failure and provides information about rollback operations.
- Fields:
- completedNotRolledBackMigrations: Migrations that completed but were not rolled back due to fallback hint or rollback failure
- skippedMigrations: Migrations which failed but were "recovered" via
IcarionFailureRecoveryHint.Skip
- rolledBackMigrations: Migrations which failed but were rolled back due to
IcarionFailureRecoveryHint.Rollback
- failedMigration: Migration [VERSION] which caused the Failure
- eligibleMigrations: All migrations which were selected for migration between (currentVersion, targetVersion]
- Fields:
- AlreadyRunning - Indicates that another migration process is already in progress
To provide insights into the migration process, IcarionMigrator supports an observer mechanism. By implementing the IcarionMigrationObserver interface, you can monitor the progress of each migration, handle failures, and decide the recovery strategy.
The IcarionMigrationObserver interface includes the following methods:
onMigrationStart(version: VERSION)
: Invoked when a migration targeting the specified version begins.onMigrationSuccess(version: VERSION)
: Invoked when a migration targeting the specified version completes successfully.onMigrationFailure(version: VERSION, exception: Exception)
: Invoked when a migration targeting the specified version fails. You can return an appropriateIcarionFailureRecoveryHint
to determine the recovery strategy:Skip
,Rollback
, orAbort
.
Observer can be set via migrator.migrationObserver
The migrator supports three strategies to handle migration failures, configurable via IcarionFailureRecoveryHint
:
- Skip: Continues execution by skipping the failed migration.
- Rollback: Tries to revert previously successful migrations in reverse order.
- Abort: Stops the migration process immediately without rolling back or continuing.
These strategies can be set as a default via migrator.defaultFailureRecoveryHint
or they can be determined on individual migration level.
The default value is IcarionFailureRecoveryHint.Abort
If no Migration Observer is set, the defaultFailureRecoveryHint
is used.
With the Migration Observer, each migration must return how its failure should be addressed.
When using the Rollback
strategy, the migrator will:
- Halt further migrations upon encountering a failure.
- Invoke the
rollback
function for all successfully executed migrations, in reverse order.
Note: If rollback fails for any migration in the chain, the process is stopped and IcarionMigrationsResult.Failure
is returned with info on which migrations have been completed and which have been rolled backed.
With this Result information you can then decide how to handle the failed rollback process.
The migrator provides detailed logging at every step of the process. You can integrate your preferred logging framework (e.g., SLF4J, Android Logcat, etc...) to monitor progress, failures, and skipped migrations.
Logging is done via a small and simple Logger Facade to not force any dependencies via Icarion.
IcarionLoggerAdapter.init(createLoggerFacade())
private fun createLoggerFacade() = object : IcarionLogger {
private val logger = LoggerFactory.getLogger("IcarionLogger")
override fun d(message: String) {
logger.debug(message)
}
override fun i(message: String) {
logger.info(message)
}
override fun e(t: Throwable, message: String) {
logger.error(message, t)
}
override fun e(t: Throwable) {
logger.error(t)
}
}
Take a look at working samples in the following folders ktor-sample, android-sample, desktop-sample (TODO).
The IcarionMigrator simplifies version migrations by handling:
- Version ordering: Ensures migrations are executed in the correct sequence.
- Failure recovery: Allows flexible behavior when migrations fail.
- Logging: Provides visibility into migration progress and issues.
You’re now ready to run migrations in your app!
- Support other KMP architectures?
Contributions are welcome! Please fork this repository and submit a pull request.