Preferences

Kstoreopen in new window is a tiny Kotlin multiplatform library that assists in saving and restoring objects to and from disk using kotlinx.coroutines, kotlinx.serialization and kotlinx.io. Inspired by RxStore

More settings options

if you want alternate library to store simple key-value data, you can use Multiplatform-Settingsopen in new window or DataStore multiplatform. Be carefull, not all target web platform

Add kstore dependency to your project for each target platform

build.gradle.kts (composeMain)
  commonMain.dependencies {
            ...
            implementation(libs.kstore)
        }
        androidMain.dependencies {
            ...
            implementation(libs.kstore.file)
        }
        desktopMain.dependencies {
            ...
            implementation(libs.kstore.file)
        }
        iosMain.dependencies {
            implementation(libs.kstore.file)
        }
        wasmJsMain.dependencies {
            implementation(libs.kstore.storage)
        }
    ...

Define the native call to get the kstore instance

platform.kt (commonMain)
    expect fun getKStore(): KStore<Quiz>?

Define each platform call to get the kstore instance for Android, iOS, Web, Desktop

platform.kt (androidMain)
    actual fun getKStore(): KStore<Quiz>? {
        return storeOf(QuizApp.context().dataDir.path.plus("/quiz.json").toPath())
    }

Also Android needs context to instanciate the kstore. Without injection library, you can use an App context singleton.

QuizApp.kt (androidMain)
class QuizApp : Application() {
    init {
        app = this
    }

    companion object {
        private lateinit var app: QuizApp
        fun context(): Context = app.applicationContext
    }
} 

Add the QuizApp to the AndroidManifest.xml

AndroidManifest.xml (androidMain)
...
    <application
        android:name=".QuizApp"
...
platform.kt (iosMain)
    @OptIn(ExperimentalKStoreApi::class)
    actual fun getKStore(): KStore<Quiz>? {
        return NSFileManager.defaultManager.DocumentDirectory?.relativePath?.plus("/quiz.json")?.toPath()?.let {
            storeOf(
            file= it
        )
        }
    }
platform.kt (wasmJsMain)
    actual fun getKStore(): KStore<Quiz>? {
        return storeOf(key = "kstore_quiz")
     }

platform.kt (desktopMain)
    actual fun getKStore(): KStore<Quiz>? {
        return storeOf("quiz.json".toPath())
    }

Upgrade the Quiz object with an update timestamp

Quiz.kt (commonMain)
@Serializable
data class Quiz(var questions: List<Question>,  val updateTime:Long=0L)

Create a QuizKStoreDataSource class to store the kstore data

QuizKStoreDataSource.kts (commonMain)
class QuizKStoreDataSource {
   private val kStoreQuiz: KStore<Quiz>? = getKStore()
   suspend fun getUpdateTimeStamp(): Long = kStoreQuiz?.get()?.updateTime ?: 0L

   suspend fun setUpdateTimeStamp(timeStamp: Long) {
       kStoreQuiz?.update { quiz: Quiz? ->
           quiz?.copy(updateTime = timeStamp)
       }
   }

   suspend fun getAllQuestions(): List<Question> {
       return kStoreQuiz?.get()?.questions ?: emptyList()
   }

   suspend fun insertQuestions(newQuestions: List<Question>) {
       kStoreQuiz?.update { quiz: Quiz? ->
           quiz?.copy(questions = newQuestions)
       }
   }

   suspend fun resetQuizKstore() {
       kStoreQuiz?.delete()
       kStoreQuiz?.set(Quiz(emptyList(), 0L))
   }
}

Update the QuizRepository class to use the kstore

QuizRepository.kts (commonMain)
class QuizRepository {
    private val mockDataSource = MockDataSource()
    private val quizApiDatasource = QuizApiDatasource()
    private var quizKStoreDataSource = QuizKStoreDataSource()

    private suspend fun fetchQuiz(): List<Question> = quizApiDatasource.getAllQuestions().questions

    private suspend fun fetchAndStoreQuiz(): List<Question> {
        quizKStoreDataSource.resetQuizKstore()
        val questions = fetchQuiz()
        quizKStoreDataSource.insertQuestions(questions)
        quizKStoreDataSource.setUpdateTimeStamp(Clock.System.now().epochSeconds)
        return questions
    }

    suspend fun updateQuiz(): List<Question> {
        try {
            val lastRequest = quizKStoreDataSource.getUpdateTimeStamp()
            return if (lastRequest == 0L || lastRequest - Clock.System.now().epochSeconds > 300000) {
                fetchAndStoreQuiz()
            } else {
                quizKStoreDataSource.getAllQuestions()
            }
        } catch (e: NullPointerException) {
            return fetchAndStoreQuiz()
        } catch (e: Exception) {
            e.printStackTrace()
            return mockDataSource.generateDummyQuestionsList()
        }
    }

}

Sources

The full sources can be retrieved hereopen in new window

🎬 Summary video of the course

Last Updated:
Contributors: Ibrahim Gharbi