Skip to content

Commit

Permalink
More STAR kmp work (#1119)
Browse files Browse the repository at this point in the history
See individual commits!

Main thing is this adds caching to desktop, so it's not just in-memory
anymore.
  • Loading branch information
ZacSweers committed Jan 15, 2024
1 parent 89a42c1 commit 3187f88
Show file tree
Hide file tree
Showing 19 changed files with 158 additions and 60 deletions.
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ androidx-test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01"
anvil-annotations = { module = "com.squareup.anvil:annotations", version.ref = "anvil" }
anvil-annotations-optional = { module = "com.squareup.anvil:annotations-optional", version.ref = "anvil" }

# Utilities for local storage dirs on Desktop
# https://github.com/harawata/appdirs
appDirs = "net.harawata:appdirs:1.2.2"

atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }

autoService-annotations = { module = "com.google.auto.service:auto-service-annotations", version = "1.1.1" }
Expand Down Expand Up @@ -267,6 +271,7 @@ sqldelight-primitiveAdapters = { module = "app.cash.sqldelight:primitive-adapter

telephoto-zoomableImageCoil = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }

testing-assertk = "com.willowtreeapps.assertk:assertk:0.28.0"
testing-espresso-core = "androidx.test.espresso:espresso-core:3.5.1"
testing-testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version.ref = "testParameterInjector" }
truth = "com.google.truth:truth:1.2.0"
Expand Down
2 changes: 2 additions & 0 deletions samples/star/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ Desktop
-------

Run `./gradlew :samples:star:run -Pcircuit.buildDesktop`. This property must be set to build the desktop app due to https://youtrack.jetbrains.com/issue/KT-30878.

_Note that you cannot run the project from the `main()` function in `Main.kt`, as this does not create a fat jar bundle with all necessary dependencies._
34 changes: 21 additions & 13 deletions samples/star/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ kotlin {
}
jvmToolchain(libs.versions.jdk.get().toInt())

applyDefaultHierarchyTemplate()
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("jvmCommon") {
withAndroidTarget()
withJvm()
}
}
}

sourceSets {
commonMain {
Expand Down Expand Up @@ -89,10 +97,12 @@ kotlin {
implementation(libs.kotlin.test)
implementation(libs.molecule.runtime)
implementation(libs.turbine)
implementation(libs.okio.fakefilesystem)
implementation(libs.testing.assertk)
implementation(projects.circuitTest)
}
}
val commonJvm by creating {
maybeCreate("jvmCommonMain").apply {
dependsOn(commonMain.get())
dependencies {
api(libs.anvil.annotations)
Expand All @@ -110,7 +120,7 @@ kotlin {
kapt.dependencies.addLater(libs.dagger.compiler)
}
}
val commonJvmTest by creating {
maybeCreate("jvmCommonTest").apply {
dependsOn(commonTest.get())
dependencies {
implementation(dependencies.testFixtures(libs.eithernet))
Expand All @@ -120,7 +130,6 @@ kotlin {
}
if (!buildDesktop) {
androidMain {
dependsOn(commonJvm)
dependencies {
implementation(libs.androidx.appCompat)
implementation(libs.androidx.browser)
Expand All @@ -138,7 +147,6 @@ kotlin {
}
}
val androidUnitTest by getting {
dependsOn(commonJvmTest)
dependencies {
implementation(libs.androidx.compose.ui.testing.junit)
implementation(libs.androidx.compose.ui.testing.manifest)
Expand Down Expand Up @@ -170,16 +178,13 @@ kotlin {
}
if (!disableJvmTarget) {
jvmMain {
dependsOn(commonJvm)
dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.coroutines.swing)
// Used for an in-memory datastore
implementation(libs.okio.fakefilesystem)
implementation(libs.sqldelight.driver.jdbc)
implementation(libs.appDirs)
}
}
jvmTest { dependsOn(commonJvmTest) }
}

configureEach {
Expand All @@ -191,7 +196,7 @@ kotlin {
"coil3.annotation.ExperimentalCoilApi",
"kotlinx.coroutines.ExperimentalCoroutinesApi",
)
freeCompilerArgs.addAll("-Xexpect-actual-classes")
freeCompilerArgs.add("-Xexpect-actual-classes")

if (project.hasProperty("circuit.enableComposeCompilerReports")) {
val metricsDir =
Expand All @@ -218,9 +223,12 @@ if (!buildDesktop) {

// Hack to get these resources visible to other source sets
// https://kotlinlang.slack.com/archives/C3PQML5NU/p1696283778314299?thread_ts=1696283403.197389&cid=C3PQML5NU
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
sourceSets["test"].resources.srcDirs("src/commonTest/resources")
sourceSets["androidTest"].resources.srcDirs("src/commonTest/resources")
// Disabled during sync because it breaks source sets
if (!System.getProperty("idea.sync.active", "false").toBoolean()) {
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
sourceSets["test"].resources.srcDirs("src/commonTest/resources")
sourceSets["androidTest"].resources.srcDirs("src/commonTest/resources")
}

defaultConfig {
minSdk = 28
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.star.data

import android.content.Context
import com.slack.circuit.star.di.AppScope
import com.slack.circuit.star.di.ApplicationContext
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.optional.SingleIn
import javax.inject.Inject
import okio.FileSystem
import okio.Path
import okio.Path.Companion.toOkioPath

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class ContextStarAppDirs
@Inject
constructor(
@ApplicationContext private val context: Context,
override val fs: FileSystem,
) : StarAppDirs {

override val userConfig: Path by lazy {
(context.filesDir.toOkioPath() / "config").also(fs::createDirectories)
}

override val userData: Path by lazy {
(context.filesDir.toOkioPath() / "data").also(fs::createDirectories)
}

override val userCache: Path by lazy { context.cacheDir.toOkioPath() }
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.star.data

import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import okio.fakefilesystem.FakeFileSystem
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

// TODO eventually move to commonTest
@RunWith(RobolectricTestRunner::class)
class TokenStorageTest {
@Test
fun basicStore() = runTest {
val tokenStorage =
TokenStorageImpl(
TokenStorageModule.provideDatastoreStorage(ApplicationProvider.getApplicationContext())
TokenStorageModule.provideDatastoreStorage(FakeStarAppDirs(FakeFileSystem()))
)
assertThat(tokenStorage.getAuthData()).isNull()
val inputData =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.star.data

import okio.FileSystem
import okio.Path
import okio.Path.Companion.toPath

/** Common interface for access to different directories on the filesystem. */
interface StarAppDirs {
val fs: FileSystem
val userConfig: Path
val userData: Path
val userCache: Path
}

class FakeStarAppDirs(override val fs: FileSystem) : StarAppDirs {
override val userConfig: Path = "/userConfig".toPath().also(fs::createDirectories)
override val userData: Path = "/userData".toPath().also(fs::createDirectories)
override val userCache: Path = "/userCache".toPath().also(fs::createDirectories)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import io.ktor.client.engine.okhttp.OkHttpConfig
import io.ktor.client.engine.okhttp.OkHttpEngine
import io.ktor.client.plugins.HttpRequestRetry
import javax.inject.Qualifier
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okio.FileSystem
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create

private const val MAX_CACHE_SIZE = 1024L * 1024L * 25L // 25 MB

@ContributesTo(AppScope::class)
@Module
object DataModule {
Expand All @@ -35,8 +39,15 @@ object DataModule {

@Provides
@SingleIn(AppScope::class)
fun provideOkHttpClient(): OkHttpClient {
fun provideHttpCache(appDirs: StarAppDirs): Cache {
return Cache(appDirs.userCache / "http_cache", MAX_CACHE_SIZE, appDirs.fs)
}

@Provides
@SingleIn(AppScope::class)
fun provideOkHttpClient(cache: Cache): OkHttpClient {
return OkHttpClient.Builder()
.cache(cache)
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
Expand Down Expand Up @@ -113,4 +124,6 @@ object DataModule {
.build()
.create<PetfinderApi>()
}

@Provides @SingleIn(AppScope::class) fun provideFileSystem(): FileSystem = FileSystem.SYSTEM
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.optional.SingleIn
import dagger.Module
import dagger.Provides
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem

// TODO better reconcile this with the android version
@ContributesTo(AppScope::class)
@Module
object TokenStorageModule {
private const val TOKEN_STORAGE_FILE_NAME = "TokenManager"

@SingleIn(AppScope::class)
@Provides
fun provideDatastoreStorage(): Storage<Preferences> {
// Use a FakeFileSystem to just keep it in-memory.
return createStorage(FakeFileSystem()) {
val dir = "/tokenstorage".toPath()
fun provideDatastoreStorage(appDirs: StarAppDirs): Storage<Preferences> {
return createStorage(appDirs.fs) {
val dir = appDirs.userConfig / "token_storage"
createDirectory(dir)
dir.resolve("$TOKEN_STORAGE_FILE_NAME.preferences_pb")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,35 @@ package com.slack.circuit.star.di

import coil3.ImageLoader
import coil3.PlatformContext
import coil3.disk.DiskCache
import coil3.network.NetworkFetcher
import com.slack.circuit.star.data.StarAppDirs
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.optional.SingleIn
import dagger.Module
import dagger.Provides
import io.ktor.client.HttpClient

private const val MAX_CACHE_SIZE = 1024L * 1024L * 100L // 100 MB

@ContributesTo(AppScope::class)
@Module
object CoilModule {
@SingleIn(AppScope::class)
@Provides
fun provideImageLoader(
@ApplicationContext platformContext: PlatformContext,
httpClient: dagger.Lazy<HttpClient>
httpClient: dagger.Lazy<HttpClient>,
starAppDirs: StarAppDirs,
): ImageLoader =
ImageLoader.Builder(platformContext)
.diskCache {
DiskCache.Builder()
.directory(starAppDirs.userCache / "image_cache")
.fileSystem(starAppDirs.fs)
.maxSizeBytes(MAX_CACHE_SIZE)
.build()
}
.components { add(NetworkFetcher.Factory(lazy { httpClient.get() })) }
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.star.data

import com.slack.circuit.star.di.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.optional.SingleIn
import javax.inject.Inject
import net.harawata.appdirs.AppDirsFactory
import okio.FileSystem
import okio.Path
import okio.Path.Companion.toPath

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DesktopStarAppDirs @Inject constructor(override val fs: FileSystem) : StarAppDirs {

private val appDirs = AppDirsFactory.getInstance()

override val userConfig: Path by lazy {
appDirs.getUserConfigDir(APP_NAME, APP_VERSION, APP_AUTHOR).toPath().also(fs::createDirectories)
}

override val userData: Path by lazy {
appDirs.getUserDataDir(APP_NAME, APP_VERSION, APP_AUTHOR).toPath().also(fs::createDirectories)
}

override val userCache: Path by lazy {
appDirs.getUserCacheDir(APP_NAME, APP_VERSION, APP_AUTHOR).toPath().also(fs::createDirectories)
}

private companion object {
const val APP_NAME = "STAR"
const val APP_VERSION = "1.0.0"
const val APP_AUTHOR = "slackhq"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import app.cash.sqldelight.db.QueryResult.Value
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import com.slack.circuit.star.data.StarAppDirs
import javax.inject.Inject
import kotlin.io.path.absolutePathString

actual class SqlDriverFactory @Inject constructor() {
actual class SqlDriverFactory @Inject constructor(private val appDirs: StarAppDirs) {
actual fun create(schema: SqlSchema<Value<Unit>>, name: String): SqlDriver {
// TODO what's the right way to do this on Desktop?
// val dbPath = createTempDirectory(name)
// return JdbcSqliteDriver(url = "jdbc:sqlite:${dbPath.absolutePathString()}")
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
val dbPath = appDirs.userConfig.resolve("$name.db")
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${dbPath.toNioPath().absolutePathString()}")
// val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
schema.create(driver)
return driver
}
Expand Down

0 comments on commit 3187f88

Please sign in to comment.