Kernl is a Kotlin library built to simplify the majority of data management scenarios. Kernl provides a flexible and declarative approach to defining your data management strategies. Kernl gives you granular control over all aspects of caching, from in-memory network caching to offline backups in poor network conditions to database persistence to Key-Value storage.
- Declarative API: Define the rules and Kernl generates the code to execute it.
- Performance: Kernl uses KSP to generate code rather than relying on reflection to build functionality.
- Flexible Caching: Use Kernl-provided policies or build your own custom policies to define your needs.
- Flexible Integration: Integrates easily with all major dependency injection frameworks.
- Kotlin-First: Kernl prioritizes Kotlin native functionality like Coroutines and Serialization.
- Real-Time Data Sync: Ensure your entire application stays in sync with Kernl's managed streams.
You'll notice there are 3 distinct libraries. This is to keep your final jar/aar files as compact as possible by
only
including the minimum runtime dependencies. Things like annotations are only needed at compile time.
dependencies {
ksp("org.mattshoe.shoebox:Kernl.Processor:0.0.1-beta4")
compileOnly("org.mattshoe.shoebox:Kernl.Annotations:0.0.1-beta4")
implementation("org.mattshoe.shoebox:Kernl.Runtime:0.0.1-beta4")
}With Kernl, you can define the lifespan of your memory-cached data. Once that lifespan is ended, all memory caches will be immediately invalidated, ensuring that no data can be used outside the defined scope. Disk-cached data will not be invalidated upon scope termination, only in-memory caches.
This is useful for situations such as invalidating data when a user logs out, ensuring that no sensitive data is leaked to the next session and all memory is disposed appropriately.
If you opt for no defined scope, then the Kernl framework will not automatically invalidate any data unless you
invoke kernl { globalInvalidate() }.
// Scope your data to a single login session, invalidating all data on logout
class MyUserSessionManager : SessionManager {
override fun onLoggedIn() {
kernl { startSession() }
}
override fun onLoggedOut() {
kernl { stopSession() }
}
}Annotate any method and give it a name to indicate that a Kernl should be generated for that method. This annotation
will typically be on a Retrofit service method.
The suffix "Kernl" will be appended to whatever name you choose, and a Kernl will be generated to automate access. The
example below will generate a UserDataKernl.
See Annotations for a list of available annotations.
interface UserDataService {
@Kernl.SingleCache.InMemory("UserData") // Note that "Kernl" will be appended to the end of the name automatically
suspend fun getUserData(id: String, someParam: Int, otherParam: Boolean): UserData
}Kernl provides you with a simple Factory for each generated Kernl. This allows you to use Kernl with any
dependency injection framework. Examples included below.
// Option 1: Pass function pointer to the factory
val useDataKernl = UserDataKernl.Factory(serviceImpl::getUserData)
// Option 2: Pass lambda to the factory
val useDataKernl = UserDataKernl.Factory { id, someParam, otherParam ->
service.getUserData(id, someParam, otherParam)
}Refer to KernlPolicy for further customization options
// Just copy the defaults and provide your own values! Each parameter is optional to allow you to only override what you need.
val myKernlPolicy = KernlPolicyDefaults.copy(
retryStrategy = ExponentialBackoff.copy(maxAttempts = 4),
//cacheStrategy = CacheStrategy.DiskFirst,
//invalidationStrategy = InvalidationStrategy.LazyRefresh(timeToLive = 25.minutes)
)// Option 1: Pass function pointer to the factory
val useDataKernl = UserDataKernl.Factory(myKernlPolicy, serviceImpl::getUserData)
// Option 2: Pass lambda to the factory
val useDataKernl = UserDataKernl.Factory(myKernlPolicy) { id, someParam, otherParam ->
service.getUserData(id, someParam, otherParam)
}@Module
interface UserDataModule {
companion object {
@Provides
fun provideUserDataKernl(
service: UserDataService
): UserDataKernl {
return UserDataKernl.Factory(service::getUserData)
}
}
}@Module
interface UserDataModule {
companion object {
@Provides
fun provideUserDataKernl(
service: UserDataService
): UserDataKernl {
return UserDataKernl.Factory(myKernlPolicy, service::getUserData)
}
}
}Other Popular Dependency Injection Examples
@Module
@InstallIn(SingletonComponent::class)
object UserDateModule {
@Singleton
@Provides
fun provideUserDataKernl(
service: UserDataService
): UserDataKernl {
return UserDataKernl.Factory(service::getUserData)
}
}val userDataKernl = module {
single<UserDataKernl> {
UserDataKernl.Factory(get<UserDataService>()::getUserData)
}
}@Configuration
class UserDataConfiguration {
@Bean
fun userDataKernl(service: UserDataService): UserDataKernl {
return UserDataKernl.Factory(service::getUserData)
}
}Now you can just inject your Kernl and use it wherever you see fit. This will typically be injected into your repository layer to interface with the rest of your app, but every architecture is unique, so use it where it suits your architecture best.
class UserRepository(
private val userDataKernl: UserDataKernl
) {
private val _userData = MutableSharedFlow<UserData>()
private val _errors = MutableSharedFlow<Error>()
val userData: Flow<UserData> = _userData
val errors: Flow<Error> = _errors
init {
userDataKernl.data
.onSuccess {
_userData.emit(it)
}.onInvalidation {
cleanup()
}.onError {
_errors.emit(it.error)
}.launchIn(viewModelScope)
}
suspend fun loadUser(id: String, someParam: Int, otherParam: Boolean) {
userDataKernl.fetch(id, someParam, otherParam)
}
suspend fun cleanup() {
// logic to remove any invalidated data (if necessary)
}
}NoCacheKernlSingleCacheKernlAssociativeCacheKernlDataResultValidDataResultErrorDataResultKernlDefaultKernlPolicyKernlPolicyRetryStrategyCacheStrategyInvalidationStrategyKernlEventExponentialBackoff
valueOrNull()unwrap()unwrap { ... }orElse { ... }asDataResult()onSuccess { ... }onError { ... }onInvalidation { ... }cacheDataResult { ... }
Contributors are absolutely welcome! This is a relatively ambitious project, so the help is always greatly appreciated. Bonus points if you have domain expertise in caching in general, or serialization methods such as KotlinX, ProtoBuf, Gson, Moshi, etc, etc. Expertise is not a requirement by any means though!
See our Contributing documentation for how to get started!