Skip to content

Commit

Permalink
Add directories initialiser and tests (#132)
Browse files Browse the repository at this point in the history
* Add directories initialiser and tests

* Update documentation

* Update api
  • Loading branch information
xxfast authored Oct 23, 2024
1 parent 8bd4e49 commit 9a439c3
Show file tree
Hide file tree
Showing 23 changed files with 565 additions and 26 deletions.
101 changes: 79 additions & 22 deletions docs/topics/using-platform-paths.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,112 @@
# Using Platform Specific Paths

Getting a path to a file is different for each platform, and you will need to define how this works for each platform
## Using Default Directory Provider

> This is experimental API and may be removed in future releases
> {style="note"}
`DefaultDirectories` provides a path to directories where you can store your files for each platform.

```kotlin
// For files directory
val files: KStore<Pet> = storeOf(file = Path("${DefaultDirectories.files}/my_cats.json"))

// For caches directory
val caches: KStore<Pet> = storeOf(file = Path("${DefaultDirectories.caches}/my_cats.json"))
```

This will resolve to the appropriate directory for each platform
// Generate a table of 3 columns, 6 rows
| Platform | Files directory | Caches directory |
|----------|-----------------|------------------|
| Android | `context.filesDir` | `context.cacheDir` |
| iOS | `NSDocumentDirectory` | `NSCachesDirectory` |
| Desktop (Mac)* | `/Users/<Account>/Library/Application Support/io.github.xxfast.kstore` | `/Users/<Account>/Library/Caches/io.github.xxfast.kstore` |
| Desktop (Windows)* | `C:\<Account>\ave\AppData\Local\xxfast\io.github.xxfast.kstore` | `C:\Users\ave\AppData\Local\xxfast\myapp\Cache\io.github.xxfast.kstore` |
| Desktop (Linux)* | `home/<Account>/.local/share/io.github.xxfast.kstore` | `/home/<Account>/.cache/io.github.xxfast.kstore` |

> * - via [harawata/appdirs](https://github.com/harawata/appdirs)
## Defining Your Own Directory Provider

If you want to put your files elsewhere, define your own directory provider for each platform.

```kotlin
var storageDir: String
var directories: DirectoryProvider
val files: KStore<Pet> = storeOf(file = Path("${directories.files}/my_cats.json"))
val caches: KStore<Pet> = storeOf(file = Path("${directories.cache}/my_cats.json"))
```
> For this example, we are keeping this as a top level variable.
> For this example, we are keeping this as a top level variable. But do use your favorite DI framework instead
> { style="note" }
## On Android
### On Android
Getting a path on android involves invoking from `filesDir`/`cacheDir` from a `Context`.
```kotlin
import kotlin.io.path.Path

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// for documents directory
storageDir = filesDir.path

// or caches directory
storageDir = cacheDir.path
directories = DirectoryProvider(
files = Path(context.filesDir),
cache = Path(context.cacheDir),
)
}
}
```

## On Desktop (JVM)
### On Desktop (JVM)

This depends on where you want to save your files, but generally you should save your files in a user data directory.
Recommending to use [harawata's appdirs](https://github.com/harawata/appdirs) to get the platform specific app dir

```kotlin
storageDir = AppDirsFactory.getInstance()
.getUserDataDir(PACKAGE_NAME, VERSION, ORGANISATION)
const val PACKAGE_NAME = "io.github.xxfast.kstore"
const val VERSION = "1.0"
const val ORGANISATION = "xxfast"

directories = DirectoryProvider(
files = AppDirsFactory.getInstance().getUserDataDir(PACKAGE_NAME, VERSION, ORGANISATION),
cache = AppDirsFactory.getInstance().getUserCacheDir(PACKAGE_NAME, VERSION, ORGANISATION),
)
```

## On iOS & other Apple platforms
> Make sure to create those directories if they don't already exist. The store won't create them for you
> { style="note" }
### On iOS & other Apple platforms
This depends on where you want to place your files. For most common use-cases, you will want either `NSDocumentDirectory` or `NSCachesDirectory`

KStore provides you a convenience extensions to resolve these for you

```kotlin
// for documents directory
storageDir = NSFileManager.defaultManager.DocumentDirectory?.relativePath

// or caches directory
storageDir = NSFileManager.defaultManager.CachesDirectory?.relativePath
val fileManager:NSFileManager = NSFileManager.defaultManager
val documentsUrl: NSURL? = fileManager.URLForDirectory(
directory = NSDocumentDirectory,
appropriateForURL = null,
create = false,
inDomain = NSUserDomainMask,
error = null
)

val cachesUrl:NSURL? = fileManager.URLForDirectory(
directory = NSCachesDirectory,
appropriateForURL = null,
create = false,
inDomain = NSUserDomainMask,
error = null
)

val files = requireNotNull(documentsUrl?.path)
val caches = requireNotNull(cachesUrl?.path)

directories = DirectoryProvider(
files = Path(files),
caches = Path(caches)
)
```

> This is experimental API and may be removed in future releases
> {style="note"}
> `NSHomeDirectory()` _(though it works on the simulator)_ is **not** suitable for physical devices as the security policies on physical devices does not permit read/writes to this directory
> {style="warning"}
Expand All @@ -58,4 +115,4 @@ storageDir = NSFileManager.defaultManager.CachesDirectory?.relativePath
<category ref="external">
<a href="https://tanaschita.com/20221010-quick-guide-on-the-ios-file-system/">Learn how to work with files and directories when developing iOS applications</a>
</category>
</seealso>
</seealso>
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
[versions]
agp = "8.0.2"
androidx-test-junit = "1.2.1"
androidx-startup = "1.1.1"
harawata-appdirs = "1.2.1"
junit = "4.13.2"
junit-jupiter="5.10.1"
kotlin = "2.0.10"
kotlinx-coroutines = "1.9.0-RC"
kotlinx-serialization = "1.7.1"
kotlinx-io = "0.5.0"
mockk = "1.13.13"
turbine = "1.1.0"

[libraries]
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" }
harawata-appdirs = { module = "net.harawata:appdirs", version.ref = "harawata-appdirs" }
junit = { module = "junit:junit", version.ref = "junit" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" }
Expand All @@ -22,4 +27,7 @@ kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", v
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-serialization-json-io = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-io", version.ref = "kotlinx-serialization" }
kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockk" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
20 changes: 20 additions & 0 deletions kstore-file/api/android/kstore-file.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
public final class io/github/xxfast/kstore/file/AndroidDirectoryProviderKt {
public static final fun getDefaultDirectories ()Lio/github/xxfast/kstore/file/DirectoryProvider;
}

public final class io/github/xxfast/kstore/file/DirectoriesInitializer : androidx/startup/Initializer {
public fun <init> ()V
public fun create (Landroid/content/Context;)Lio/github/xxfast/kstore/file/DirectoryProvider;
public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object;
public fun dependencies ()Ljava/util/List;
}

public abstract interface class io/github/xxfast/kstore/file/DirectoryProvider {
public abstract fun getCaches ()Lkotlinx/io/files/Path;
public abstract fun getFiles ()Lkotlinx/io/files/Path;
}

public final class io/github/xxfast/kstore/file/DirectoryProviderKt {
public static final fun DirectoryProvider (Lkotlinx/io/files/Path;Lkotlinx/io/files/Path;)Lio/github/xxfast/kstore/file/DirectoryProvider;
}

public final class io/github/xxfast/kstore/file/FileCodec : io/github/xxfast/kstore/Codec {
public fun <init> (Lkotlinx/io/files/Path;Lkotlinx/io/files/Path;Lkotlinx/serialization/json/Json;Lkotlinx/serialization/KSerializer;)V
public fun decode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
13 changes: 13 additions & 0 deletions kstore-file/api/desktop/kstore-file.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
public final class io/github/xxfast/kstore/file/DesktopDirectoryProviderKt {
public static final fun getDefaultDirectories ()Lio/github/xxfast/kstore/file/DirectoryProvider;
}

public abstract interface class io/github/xxfast/kstore/file/DirectoryProvider {
public abstract fun getCaches ()Lkotlinx/io/files/Path;
public abstract fun getFiles ()Lkotlinx/io/files/Path;
}

public final class io/github/xxfast/kstore/file/DirectoryProviderKt {
public static final fun DirectoryProvider (Lkotlinx/io/files/Path;Lkotlinx/io/files/Path;)Lio/github/xxfast/kstore/file/DirectoryProvider;
}

public final class io/github/xxfast/kstore/file/FileCodec : io/github/xxfast/kstore/Codec {
public fun <init> (Lkotlinx/io/files/Path;Lkotlinx/io/files/Path;Lkotlinx/serialization/json/Json;Lkotlinx/serialization/KSerializer;)V
public fun decode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
24 changes: 22 additions & 2 deletions kstore-file/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ android {
abortOnError = false
}

testOptions {
unitTests {
isReturnDefaultValues = true
}
}

namespace = "io.github.xxfast.kstore.file"
}

Expand Down Expand Up @@ -102,13 +108,27 @@ kotlin {
}
}

val androidMain by getting
val androidUnitTest by getting
val androidMain by getting {
dependencies {
implementation(libs.androidx.startup)
}
}

val androidUnitTest by getting {
dependencies {
implementation(libs.androidx.startup)
implementation(libs.mockk)
implementation(libs.mockk.android)
implementation(libs.mockk.agent)
}
}

val desktopMain by getting {
dependencies {
implementation(libs.harawata.appdirs)
}
}

val desktopTest by getting

val jsMain by getting
Expand Down
15 changes: 15 additions & 0 deletions kstore-file/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="io.github.xxfast.kstore.file.DirectoriesInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.xxfast.kstore.file

import android.content.Context
import io.github.xxfast.kstore.utils.ExperimentalKStoreApi
import kotlinx.io.files.Path

/**
* When no context is available, this will point to root directory.
*/
@ExperimentalKStoreApi
internal var _directories: DirectoryProvider = RootDirectoryProvider

@ExperimentalKStoreApi
public actual val DefaultDirectories: DirectoryProvider get() = _directories

@ExperimentalKStoreApi
internal class AndroidDirectoryProvider(context: Context) : DirectoryProvider {
override val files: Path = Path(context.filesDir.path)
override val caches: Path = Path(context.cacheDir.path)
}

@ExperimentalKStoreApi
internal object RootDirectoryProvider: DirectoryProvider {
override val files: Path = Path("/")
override val caches: Path = Path("/")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.xxfast.kstore.file

import android.content.Context
import androidx.startup.Initializer
import io.github.xxfast.kstore.utils.ExperimentalKStoreApi

@ExperimentalKStoreApi
public class DirectoriesInitializer: Initializer<DirectoryProvider> {
override fun create(context: Context): DirectoryProvider = AndroidDirectoryProvider(context)
.also { _directories = it }

override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.xxfast.kstore.file

import android.content.Context
import androidx.startup.AppInitializer
import io.github.xxfast.kstore.utils.ExperimentalKStoreApi
import io.mockk.every
import io.mockk.mockk
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.Test

@OptIn(ExperimentalKStoreApi::class)
class DirectoryProviders {

@AfterTest
fun cleanUp() {
_directories = RootDirectoryProvider
}

@Test
fun testRootDirectoryWhenNotInitialised() {
val provider: DirectoryProvider = DefaultDirectories
val files: String = provider.files.toString()
val caches: String = provider.caches.toString()
assert(files == "/") { "$files doesn't match expected" }
assert(caches == "/") { "$caches doesn't match expected" }
}

@Test
fun testRootDirectoryWhenInitialised() {
val context: Context = mockk()

every { context.applicationContext } returns context
every { context.filesDir } returns File("files")
every { context.cacheDir } returns File("caches")

AppInitializer.getInstance(context)
.initializeComponent(DirectoriesInitializer::class.java)

val provider: DirectoryProvider = DefaultDirectories
val files: String = provider.files.toString()
val caches: String = provider.caches.toString()
assert(files == "files") { "$files doesn't match expected" }
assert(caches == "caches") { "$caches doesn't match expected" }
}
}
Loading

0 comments on commit 9a439c3

Please sign in to comment.