├── .editorconfig ├── .github ├── renovate.json5 └── workflows │ ├── check.yml │ └── deploy.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── inspectionProfiles │ └── Project_Default.xml ├── LICENSE ├── README-ja.md ├── README.md ├── build.gradle.kts ├── composeApp ├── .gitignore ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── kazakago │ │ │ └── swr │ │ │ └── compose │ │ │ └── example │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ ├── commonMain │ └── kotlin │ │ ├── App.kt │ │ ├── MainScreen.kt │ │ ├── basic │ │ ├── ArgumentsScreen.kt │ │ ├── AutoRevalidationScreen.kt │ │ ├── ConditionalFetchingScreen.kt │ │ ├── DataFetchingScreen.kt │ │ ├── ErrorHandlingScreen.kt │ │ ├── GlobalConfigurationScreen.kt │ │ ├── InfinitePaginationScreen.kt │ │ ├── MutationScreen.kt │ │ ├── PaginationScreen.kt │ │ └── PrefetchingScreen.kt │ │ ├── todolist │ │ ├── ToDoCreationDialog.kt │ │ ├── ToDoEditingDialog.kt │ │ ├── ToDoListScreen.kt │ │ └── server │ │ │ ├── MockServer.kt │ │ │ ├── MockServerAllFailed.kt │ │ │ ├── MockServerLoadingSlow.kt │ │ │ ├── MockServerMutationFailed.kt │ │ │ ├── MockServerRandomFailed.kt │ │ │ └── MockServerSucceed.kt │ │ └── ui │ │ ├── ErrorContent.kt │ │ ├── LoadingContent.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ ├── desktopMain │ └── kotlin │ │ └── main.kt │ └── iosMain │ └── kotlin │ └── MainViewController.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── .gitignore ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── settings.gradle.kts └── swr ├── .gitignore ├── api ├── android │ └── swr.api ├── jvm │ └── swr.api └── swr.klib.api ├── build.gradle.kts └── src ├── androidUnitTest └── kotlin │ └── com │ └── kazakago │ └── swr │ └── compose │ ├── DummyException.kt │ ├── NetworkRule.kt │ ├── config │ ├── GlobalConfigutationsTest.kt │ └── UseSWRConfigTest.kt │ ├── immutable │ └── UseSWRImmutableTest.kt │ ├── infinite │ ├── InitialSizeOptionTest.kt │ ├── PersistSizeOptionTest.kt │ ├── RevalidateAllOptionTest.kt │ ├── RevalidateFirstPageOptionTest.kt │ └── UseSWRInfiniteTest.kt │ ├── mutation │ ├── OptimisticOptionTest.kt │ ├── PopulateCacheOptionTest.kt │ ├── RevalidateOptionTest.kt │ ├── RollbackOnErrorOptionTest.kt │ ├── SWRMutateTest.kt │ └── ThrowOnErrorOptionTest.kt │ ├── prefetch │ └── UsePreloadTest.kt │ ├── trigger │ ├── OptimisticOptionTest.kt │ ├── PopulateCacheOptionTest.kt │ ├── RevalidateOptionTest.kt │ ├── RollbackOnErrorOptionTest.kt │ ├── ThrowOnErrorOptionTest.kt │ └── UseSWRMutationTest.kt │ └── validation │ ├── DedupingIntervalOptionTest.kt │ ├── ErrorRetryCountOptionTest.kt │ ├── ErrorRetryIntervalOptionTest.kt │ ├── FallbackDataOptionTest.kt │ ├── FallbackOptionTest.kt │ ├── FetcherOptionTest.kt │ ├── FocusThrottleIntervalOptionTest.kt │ ├── IsPausedOptionTest.kt │ ├── KeepPreviousDataOptionTest.kt │ ├── LoadingTimeoutOptionTest.kt │ ├── OnErrorOptionTest.kt │ ├── OnErrorRetryOptionTest.kt │ ├── OnLoadingSlowOptionTest.kt │ ├── OnSuccessOptionTest.kt │ ├── RefreshIntervalOptionTest.kt │ ├── RefreshWhenHiddenOptionTest.kt │ ├── RefreshWhenOfflineOptionTest.kt │ ├── RevalidateIfStaleOptionTest.kt │ ├── RevalidateOnFocusOptionTest.kt │ ├── RevalidateOnMountOptionTest.kt │ ├── RevalidateOnReconnectOptionTest.kt │ ├── ShouldRetryOnErrorOptionTest.kt │ └── UseSWRTest.kt └── commonMain └── kotlin └── com └── kazakago └── swr └── compose ├── UseSWR.kt ├── UseSWRConfig.kt ├── UseSWRImmutable.kt ├── UseSWRInfinite.kt ├── UseSWRMutation.kt ├── UseSWRPreload.kt ├── cache ├── SWRCache.kt └── SWRSystemCache.kt ├── config ├── SWRConfig.kt ├── SWRInfiniteConfig.kt ├── SWRMutateConfig.kt └── SWRTriggerConfig.kt ├── internal ├── DataHolder.kt ├── GlobalKonnection.kt ├── SWRGlobalScope.kt ├── UseSWRInternal.kt └── UseSWROptions.kt ├── mutate └── SWRMutate.kt ├── preload └── SWRPreload.kt ├── retry └── SWRRetryDefault.kt ├── state ├── SWRConfigState.kt ├── SWRInfiniteState.kt ├── SWRMutationState.kt └── SWRState.kt ├── trigger ├── SWRReset.kt └── SWRTrigger.kt └── validate ├── SWRValidate.kt └── SWRValidateOptions.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [ main, production ] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | check: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - run: ./gradlew check 17 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: [ "*.*.*" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 16 | env: 17 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 18 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 19 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 22 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/* 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | !.idea/codeStyles/ 12 | !.idea/inspectionProfiles/ 13 | !.idea/runConfigurations/ 14 | .kotlin/ 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) apply false 3 | alias(libs.plugins.kotlin.android) apply false 4 | alias(libs.plugins.compose) apply false 5 | alias(libs.plugins.compose.compiler) apply false 6 | alias(libs.plugins.dokka) apply false 7 | alias(libs.plugins.android.application) apply false 8 | alias(libs.plugins.android.library) apply false 9 | alias(libs.plugins.binary.compatibility.validator) apply false 10 | alias(libs.plugins.nexus.publish) 11 | } 12 | 13 | nexusPublishing { 14 | repositories { 15 | sonatype { 16 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 17 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 18 | username.set(System.getenv("SONATYPE_USERNAME") ?: findProperty("sonatype.username").toString()) 19 | password.set(System.getenv("SONATYPE_PASSWORD") ?: findProperty("sonatype.password").toString()) 20 | stagingProfileId.set(System.getenv("SONATYPE_STAGING_PROFILE_ID") ?: findProperty("sonatype.stagingProfileId").toString()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /composeApp/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | alias(libs.plugins.kotlin.multiplatform) 7 | alias(libs.plugins.android.application) 8 | alias(libs.plugins.compose) 9 | alias(libs.plugins.compose.compiler) 10 | } 11 | 12 | kotlin { 13 | androidTarget { 14 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 15 | compilerOptions { 16 | jvmTarget.set(JvmTarget.JVM_11) 17 | } 18 | } 19 | 20 | jvm("desktop") 21 | 22 | listOf( 23 | iosX64(), 24 | iosArm64(), 25 | iosSimulatorArm64() 26 | ).forEach { iosTarget -> 27 | iosTarget.binaries.framework { 28 | baseName = "ComposeApp" 29 | isStatic = true 30 | } 31 | } 32 | 33 | sourceSets { 34 | commonMain.dependencies { 35 | implementation(compose.material3) 36 | implementation(compose.components.uiToolingPreview) 37 | implementation(libs.kotlinx.datetime) 38 | implementation(libs.androidx.navigation.compose) 39 | implementation(projects.swr) 40 | } 41 | androidMain.dependencies { 42 | implementation(compose.preview) 43 | implementation(libs.androidx.activity.compose) 44 | } 45 | val desktopMain by getting 46 | desktopMain.dependencies { 47 | implementation(compose.desktop.currentOs) 48 | } 49 | } 50 | } 51 | 52 | android { 53 | namespace = "com.kazakago.swr.compose.example" 54 | compileSdk = libs.versions.compileSdk.get().toInt() 55 | 56 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 57 | sourceSets["main"].res.srcDirs("src/androidMain/res") 58 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 59 | 60 | defaultConfig { 61 | applicationId = "com.kazakago.swr.compose" 62 | minSdk = libs.versions.minSdk.get().toInt() 63 | targetSdk = libs.versions.targetSdk.get().toInt() 64 | versionCode = 1 65 | versionName = "1.0" 66 | } 67 | packaging { 68 | resources { 69 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 70 | } 71 | } 72 | buildTypes { 73 | getByName("release") { 74 | isMinifyEnabled = false 75 | } 76 | } 77 | compileOptions { 78 | sourceCompatibility = JavaVersion.VERSION_11 79 | targetCompatibility = JavaVersion.VERSION_11 80 | } 81 | buildFeatures { 82 | compose = true 83 | } 84 | dependencies { 85 | debugImplementation(compose.uiTooling) 86 | } 87 | } 88 | 89 | compose.desktop { 90 | application { 91 | mainClass = "MainKt" 92 | 93 | nativeDistributions { 94 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 95 | packageName = "com.kazakago.swr.compose.example" 96 | packageVersion = "1.0.0" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/kazakago/swr/compose/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.example 2 | 3 | import App 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.tooling.preview.Preview 9 | 10 | class MainActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContent { 14 | App() 15 | } 16 | } 17 | } 18 | 19 | @Preview 20 | @Composable 21 | fun AppAndroidPreview() { 22 | App() 23 | } 24 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SWR Compose 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import androidx.compose.runtime.CompositionLocalProvider 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.compose.rememberNavController 9 | import basic.ArgumentsScreen 10 | import basic.AutoRevalidationScreen 11 | import basic.ConditionalFetchingScreen 12 | import basic.DataFetchingScreen 13 | import basic.ErrorHandlingScreen 14 | import basic.GlobalConfigurationScreen 15 | import basic.InfinitePaginationScreen 16 | import basic.MutationScreen 17 | import basic.PaginationScreen 18 | import basic.PrefetchingNextScreen 19 | import basic.PrefetchingScreen 20 | import org.jetbrains.compose.ui.tooling.preview.Preview 21 | import todolist.ToDoListScreen 22 | import todolist.server.LocalMockServer 23 | import todolist.server.MockServer 24 | import todolist.server.MockServerSucceed 25 | import ui.theme.AppTheme 26 | 27 | @Composable 28 | @Preview 29 | fun App() { 30 | AppTheme { 31 | val navController = rememberNavController() 32 | val scope = rememberCoroutineScope() 33 | val mockServer = remember { mutableStateOf(MockServerSucceed) } 34 | val isClearCache = remember { mutableStateOf(true) } 35 | CompositionLocalProvider(LocalMockServer provides mockServer.value) { 36 | NavHost(navController = navController, startDestination = "main") { 37 | composable("main") { MainScreen(navController, mockServer, isClearCache) } 38 | composable("data_fetching") { DataFetchingScreen(navController) } 39 | composable("global_configuration") { GlobalConfigurationScreen(navController) } 40 | composable("error_handling") { ErrorHandlingScreen(navController) } 41 | composable("auto_revalidation") { AutoRevalidationScreen(navController) } 42 | composable("conditional_fetching") { ConditionalFetchingScreen(navController) } 43 | composable("arguments") { ArgumentsScreen(navController) } 44 | composable("mutation") { MutationScreen(navController) } 45 | composable("todolist") { ToDoListScreen(navController) } 46 | composable("pagination") { PaginationScreen(navController) } 47 | composable("infinite_pagination") { InfinitePaginationScreen(navController) } 48 | composable("prefetching") { PrefetchingScreen(navController, scope) } 49 | composable("prefetching_next") { PrefetchingNextScreen(navController, scope) } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/ArgumentsScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.navigation.NavController 20 | import ui.LoadingContent 21 | import com.kazakago.swr.compose.useSWR 22 | import kotlinx.coroutines.delay 23 | 24 | private val fetcher: suspend (key: String) -> String = { key -> 25 | delay(1000) 26 | "Argument is '$key'" 27 | } 28 | 29 | @Composable 30 | @OptIn(ExperimentalMaterial3Api::class) 31 | fun ArgumentsScreen(navController: NavController) { 32 | val (data1) = useSWR(key = "/arguments/google", fetcher = fetcher) 33 | val (data2) = useSWR(key = "/arguments/apple", fetcher = fetcher) 34 | Scaffold( 35 | topBar = { 36 | TopAppBar( 37 | title = { Text("Arguments") }, 38 | navigationIcon = { 39 | IconButton(onClick = navController::popBackStack) { 40 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 41 | } 42 | }, 43 | ) 44 | }, 45 | ) { 46 | Box( 47 | modifier = Modifier 48 | .fillMaxSize() 49 | .padding(it), 50 | contentAlignment = Alignment.Center, 51 | ) { 52 | if (data1 == null || data2 == null) { 53 | LoadingContent() 54 | } else { 55 | Column( 56 | modifier = Modifier.fillMaxSize(), 57 | verticalArrangement = Arrangement.Center, 58 | horizontalAlignment = Alignment.CenterHorizontally, 59 | ) { 60 | Text(data1) 61 | Text(data2) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/AutoRevalidationScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.LinearProgressIndicator 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.navigation.NavController 20 | import com.kazakago.swr.compose.useSWR 21 | import kotlinx.coroutines.delay 22 | import kotlinx.datetime.Clock 23 | import kotlinx.datetime.format 24 | import kotlinx.datetime.format.DateTimeComponents 25 | import ui.LoadingContent 26 | import kotlin.time.Duration.Companion.seconds 27 | 28 | private val fetcher: suspend (key: String) -> String = { 29 | delay(1000) 30 | Clock.System.now().format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) 31 | } 32 | 33 | @Composable 34 | @OptIn(ExperimentalMaterial3Api::class) 35 | fun AutoRevalidationScreen(navController: NavController) { 36 | val (data, _, _, isValidating) = useSWR(key = "/auto_revalidation", fetcher = fetcher) { 37 | revalidateOnMount = true // default is null 38 | revalidateIfStale = true // default is true 39 | revalidateOnFocus = true // default is true 40 | revalidateOnReconnect = true // default is true 41 | refreshInterval = 10.seconds // default is 0.seconds (=disable) 42 | refreshWhenHidden = false // default is false 43 | refreshWhenOffline = false // default is false 44 | } 45 | Scaffold( 46 | topBar = { 47 | TopAppBar( 48 | title = { Text("Auto Revalidation") }, 49 | navigationIcon = { 50 | IconButton(onClick = navController::popBackStack) { 51 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 52 | } 53 | }, 54 | ) 55 | }, 56 | ) { 57 | Box( 58 | modifier = Modifier 59 | .fillMaxSize() 60 | .padding(it), 61 | contentAlignment = Alignment.TopCenter, 62 | ) { 63 | if (data == null) { 64 | LoadingContent() 65 | } else { 66 | if (isValidating) { 67 | LinearProgressIndicator(Modifier.fillMaxWidth()) 68 | } 69 | Box( 70 | modifier = Modifier.fillMaxSize(), 71 | contentAlignment = Alignment.Center 72 | ) { 73 | Text(data) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/ConditionalFetchingScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 10 | import androidx.compose.material3.CircularProgressIndicator 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.IconButton 14 | import androidx.compose.material3.Scaffold 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TopAppBar 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.navigation.NavController 21 | import ui.LoadingContent 22 | import com.kazakago.swr.compose.useSWR 23 | import kotlinx.coroutines.delay 24 | 25 | private val userFetcher: suspend (key: String) -> String = { 26 | delay(1000) 27 | "User1" 28 | } 29 | 30 | private val projectsFetcher: suspend (key: String) -> List = { 31 | delay(1000) 32 | listOf("Project1", "Project2", "Project3") 33 | } 34 | 35 | @Composable 36 | @OptIn(ExperimentalMaterial3Api::class) 37 | fun ConditionalFetchingScreen(navController: NavController) { 38 | val (user, _, _) = useSWR(key = "/confidential_fetching/user", fetcher = userFetcher) 39 | val (projects, _, _) = useSWR(key = if (user != null) "/confidential_fetching/$user/projects" else null, fetcher = projectsFetcher) 40 | Scaffold( 41 | topBar = { 42 | TopAppBar( 43 | title = { Text("Conditional Fetching") }, 44 | navigationIcon = { 45 | IconButton(onClick = navController::popBackStack) { 46 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 47 | } 48 | }, 49 | ) 50 | }, 51 | ) { 52 | Box( 53 | modifier = Modifier 54 | .fillMaxSize() 55 | .padding(it), 56 | contentAlignment = Alignment.Center, 57 | ) { 58 | if (user == null) { 59 | LoadingContent() 60 | } else { 61 | Column( 62 | modifier = Modifier.fillMaxSize(), 63 | verticalArrangement = Arrangement.Center, 64 | horizontalAlignment = Alignment.CenterHorizontally, 65 | ) { 66 | Text(user) 67 | if (projects == null) { 68 | CircularProgressIndicator() 69 | } else { 70 | Text(projects.joinToString(", ")) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/DataFetchingScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBar 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.navigation.NavController 18 | import ui.ErrorContent 19 | import ui.LoadingContent 20 | import com.kazakago.swr.compose.useSWR 21 | import kotlinx.coroutines.delay 22 | 23 | private val fetcher: suspend (key: String) -> String = { 24 | delay(1000) 25 | "Hello world!" 26 | } 27 | 28 | @Composable 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | fun DataFetchingScreen(navController: NavController) { 31 | val (data, error) = useSWR(key = "/data_fetching", fetcher = fetcher) 32 | Scaffold( 33 | topBar = { 34 | TopAppBar( 35 | title = { Text("Data Fetching") }, 36 | navigationIcon = { 37 | IconButton(onClick = navController::popBackStack) { 38 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 39 | } 40 | }, 41 | ) 42 | }, 43 | ) { 44 | Box( 45 | modifier = Modifier 46 | .fillMaxSize() 47 | .padding(it), 48 | contentAlignment = Alignment.Center, 49 | ) { 50 | if (error != null) { 51 | ErrorContent() 52 | } else if (data == null) { 53 | LoadingContent() 54 | } else { 55 | Text(data) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/ErrorHandlingScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.material3.SnackbarHost 13 | import androidx.compose.material3.SnackbarHostState 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.rememberCoroutineScope 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.navigation.NavController 22 | import com.kazakago.swr.compose.useSWR 23 | import kotlinx.coroutines.delay 24 | import kotlinx.coroutines.launch 25 | import ui.ErrorContent 26 | import ui.LoadingContent 27 | import kotlin.time.Duration.Companion.seconds 28 | 29 | private val fetcher: suspend (key: String) -> String = { 30 | delay(1000) 31 | throw NullPointerException() 32 | } 33 | 34 | @Composable 35 | @OptIn(ExperimentalMaterial3Api::class) 36 | fun ErrorHandlingScreen(navController: NavController) { 37 | val scope = rememberCoroutineScope() 38 | val snackbarHostState = remember { SnackbarHostState() } 39 | val (data, error, _, _, mutate) = useSWR(key = "/error_handling", fetcher = fetcher) { 40 | shouldRetryOnError = true // default is true 41 | errorRetryCount = 3 // default is null 42 | errorRetryInterval = 5.seconds // default is 5.seconds 43 | onError = { error, key, config -> // default is null 44 | scope.launch { 45 | snackbarHostState.showSnackbar(error.toString()) 46 | } 47 | } 48 | // onErrorRetry = { _, _, _, _, _ -> // default is `com.kazakago.compose.swr.retry.SWRRetryDefault.kt` 49 | // } 50 | } 51 | Scaffold( 52 | topBar = { 53 | TopAppBar( 54 | title = { Text("Error Handling") }, 55 | navigationIcon = { 56 | IconButton(onClick = navController::popBackStack) { 57 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 58 | } 59 | }, 60 | ) 61 | }, 62 | snackbarHost = { 63 | SnackbarHost(snackbarHostState) 64 | }, 65 | ) { 66 | Box( 67 | modifier = Modifier 68 | .fillMaxSize() 69 | .padding(it), 70 | contentAlignment = Alignment.Center, 71 | ) { 72 | if (error != null) { 73 | ErrorContent { scope.launch { mutate() } } 74 | } else if (data == null) { 75 | LoadingContent() 76 | } else { 77 | Text(data) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/GlobalConfigurationScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.Scaffold 13 | import androidx.compose.material3.SnackbarHost 14 | import androidx.compose.material3.SnackbarHostState 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TopAppBar 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.navigation.NavController 23 | import com.kazakago.swr.compose.config.SWRConfig 24 | import com.kazakago.swr.compose.useSWR 25 | import kotlinx.coroutines.delay 26 | import kotlinx.coroutines.launch 27 | import ui.LoadingContent 28 | import kotlin.time.Duration.Companion.seconds 29 | 30 | @Composable 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | fun GlobalConfigurationScreen(navController: NavController) { 33 | val snackbarHostState = remember { SnackbarHostState() } 34 | val scope = rememberCoroutineScope() 35 | SWRConfig(options = { 36 | refreshInterval = 3.seconds 37 | fetcher = { key -> 38 | delay(1000) 39 | "Response of $key" 40 | } 41 | onError = { error, _, _ -> 42 | scope.launch { 43 | snackbarHostState.showSnackbar(error.toString()) 44 | } 45 | } 46 | }) { 47 | val (events) = useSWR(key = "/global_configuration/events") 48 | val (projects) = useSWR(key = "/global_configuration/projects") 49 | Scaffold( 50 | topBar = { 51 | TopAppBar( 52 | title = { Text("Global Configuration") }, 53 | navigationIcon = { 54 | IconButton(onClick = navController::popBackStack) { 55 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 56 | } 57 | }, 58 | ) 59 | }, 60 | snackbarHost = { 61 | SnackbarHost(snackbarHostState) 62 | }, 63 | ) { 64 | Box( 65 | modifier = Modifier 66 | .fillMaxSize() 67 | .padding(it), 68 | contentAlignment = Alignment.Center, 69 | ) { 70 | if (events == null || projects == null) { 71 | LoadingContent() 72 | } else { 73 | Column { 74 | Text(events) 75 | Text(projects) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/MutationScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.IconButton 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.material3.Text 17 | import androidx.compose.material3.TopAppBar 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.navigation.NavController 23 | import ui.LoadingContent 24 | import com.kazakago.swr.compose.useSWR 25 | import kotlinx.coroutines.delay 26 | import kotlinx.coroutines.launch 27 | 28 | private val fetcher: suspend (key: String) -> String = { 29 | delay(1000) 30 | "Fetched Data" 31 | } 32 | 33 | private val mutator: suspend () -> String = { 34 | delay(1000) 35 | "Mutated Data" 36 | } 37 | 38 | @Composable 39 | @OptIn(ExperimentalMaterial3Api::class) 40 | fun MutationScreen(navController: NavController) { 41 | val scope = rememberCoroutineScope() 42 | val (data, _, _, _, mutate) = useSWR(key = "/mutation", fetcher = fetcher) 43 | Scaffold( 44 | topBar = { 45 | TopAppBar( 46 | title = { Text("Mutation") }, 47 | navigationIcon = { 48 | IconButton(onClick = navController::popBackStack) { 49 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 50 | } 51 | }, 52 | ) 53 | }, 54 | ) { 55 | Box( 56 | modifier = Modifier 57 | .fillMaxSize() 58 | .padding(it), 59 | contentAlignment = Alignment.Center, 60 | ) { 61 | if (data == null) { 62 | LoadingContent() 63 | } else { 64 | Column( 65 | modifier = Modifier.fillMaxSize(), 66 | verticalArrangement = Arrangement.Center, 67 | horizontalAlignment = Alignment.CenterHorizontally, 68 | ) { 69 | Text(data) 70 | Row { 71 | Button(onClick = { 72 | scope.launch { 73 | mutate(data = mutator) { 74 | optimisticData = "Optimistic Data" // default is null 75 | revalidate = false // default is true 76 | populateCache = true // default is true 77 | rollbackOnError = true // default is true 78 | } 79 | } 80 | }) { 81 | Text("mutate") 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/PaginationScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 11 | import androidx.compose.material3.CircularProgressIndicator 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.IconButton 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.OutlinedButton 17 | import androidx.compose.material3.Scaffold 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TopAppBar 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableIntStateOf 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.navigation.NavController 28 | import com.kazakago.swr.compose.useSWR 29 | import kotlinx.coroutines.delay 30 | 31 | private val fetcher: suspend (key: String) -> List = { key -> 32 | delay(1000) 33 | List(10) { "$key - $it" } 34 | } 35 | 36 | @Composable 37 | @OptIn(ExperimentalMaterial3Api::class) 38 | fun PaginationScreen(navController: NavController) { 39 | var pageCount by rememberSaveable { mutableIntStateOf(1) } 40 | Scaffold( 41 | topBar = { 42 | TopAppBar( 43 | title = { Text("Pagination") }, 44 | navigationIcon = { 45 | IconButton(onClick = navController::popBackStack) { 46 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 47 | } 48 | }, 49 | ) 50 | }, 51 | ) { 52 | Box( 53 | modifier = Modifier 54 | .fillMaxSize() 55 | .padding(it), 56 | contentAlignment = Alignment.Center, 57 | ) { 58 | LazyColumn(Modifier.fillMaxSize()) { 59 | items(pageCount) { page -> 60 | PaginationRow(page = page) 61 | } 62 | item { 63 | Box( 64 | modifier = Modifier.fillMaxWidth(), 65 | contentAlignment = Alignment.Center, 66 | ) { 67 | OutlinedButton(onClick = { pageCount += 1 }) { 68 | Text("Load More") 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | @Composable 78 | private fun PaginationRow(page: Int) { 79 | val (list) = useSWR(key = "/pagination/$page", fetcher = fetcher) 80 | Column( 81 | modifier = Modifier.fillMaxWidth(), 82 | horizontalAlignment = Alignment.CenterHorizontally, 83 | ) { 84 | if (list == null) { 85 | CircularProgressIndicator() 86 | } else { 87 | list.forEach { content -> 88 | Text(content, style = MaterialTheme.typography.titleLarge) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/basic/PrefetchingScreen.kt: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.IconButton 16 | import androidx.compose.material3.Scaffold 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TopAppBar 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import androidx.navigation.NavController 24 | import ui.LoadingContent 25 | import com.kazakago.swr.compose.useSWR 26 | import com.kazakago.swr.compose.useSWRPreload 27 | import kotlinx.coroutines.CoroutineScope 28 | import kotlinx.coroutines.delay 29 | import kotlinx.coroutines.launch 30 | 31 | private val fetcher: suspend (key: String) -> String = { 32 | delay(3000) 33 | "Hello world!" 34 | } 35 | 36 | @Composable 37 | @OptIn(ExperimentalMaterial3Api::class) 38 | fun PrefetchingScreen(navController: NavController, scope: CoroutineScope) { 39 | val preload = useSWRPreload(key = "/prefetching", fetcher = fetcher) 40 | Scaffold( 41 | topBar = { 42 | TopAppBar( 43 | title = { Text("Prefetching") }, 44 | navigationIcon = { 45 | IconButton(onClick = navController::popBackStack) { 46 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 47 | } 48 | }, 49 | ) 50 | }, 51 | ) { 52 | Column( 53 | modifier = Modifier 54 | .fillMaxSize() 55 | .padding(it), 56 | verticalArrangement = Arrangement.Center, 57 | horizontalAlignment = Alignment.CenterHorizontally, 58 | ) { 59 | Button(onClick = { scope.launch { preload() } }) { 60 | Text("Start prefetch (3s)") 61 | } 62 | Spacer(Modifier.size(16.dp)) 63 | Button(onClick = { navController.navigate("prefetching_next") }) { 64 | Text("Move to next Screen") 65 | } 66 | } 67 | } 68 | } 69 | 70 | @Composable 71 | @OptIn(ExperimentalMaterial3Api::class) 72 | fun PrefetchingNextScreen(navController: NavController, scope: CoroutineScope) { 73 | val (data) = useSWR(key = "/prefetching", fetcher = fetcher, scope = scope) 74 | Scaffold( 75 | topBar = { 76 | TopAppBar( 77 | title = { Text("Prefetching Next") }, 78 | navigationIcon = { 79 | IconButton(onClick = navController::popBackStack) { 80 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) 81 | } 82 | }, 83 | ) 84 | }, 85 | ) { 86 | Box( 87 | modifier = Modifier 88 | .fillMaxSize() 89 | .padding(it), 90 | contentAlignment = Alignment.Center, 91 | ) { 92 | if (data == null) { 93 | LoadingContent() 94 | } else { 95 | Text(data) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/ToDoCreationDialog.kt: -------------------------------------------------------------------------------- 1 | package todolist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Card 10 | import androidx.compose.material3.OutlinedButton 11 | import androidx.compose.material3.OutlinedTextField 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.window.Dialog 22 | import org.jetbrains.compose.ui.tooling.preview.Preview 23 | import ui.theme.AppTheme 24 | 25 | @Composable 26 | fun ToDoCreationDialog( 27 | onSubmit: (text: String) -> Unit, 28 | onCancel: () -> Unit, 29 | ) { 30 | var text by remember { mutableStateOf("") } 31 | Dialog(onDismissRequest = {}) { 32 | Card { 33 | Column(modifier = Modifier.padding(16.dp)) { 34 | OutlinedTextField( 35 | modifier = Modifier.fillMaxWidth(), 36 | value = text, 37 | singleLine = true, 38 | onValueChange = { text = it }, 39 | ) 40 | Spacer(modifier = Modifier.size(16.dp)) 41 | Button( 42 | modifier = Modifier.fillMaxWidth(), 43 | onClick = { onSubmit(text) }, 44 | ) { 45 | Text(text = "OK") 46 | } 47 | OutlinedButton( 48 | modifier = Modifier.fillMaxWidth(), 49 | onClick = onCancel, 50 | ) { 51 | Text(text = "Cancel") 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | @Preview 59 | @Composable 60 | fun PreviewToDoCreationDialog() { 61 | AppTheme { 62 | Surface { 63 | ToDoCreationDialog( 64 | onSubmit = {}, 65 | onCancel = {}, 66 | ) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/ToDoEditingDialog.kt: -------------------------------------------------------------------------------- 1 | package todolist 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.ButtonDefaults 10 | import androidx.compose.material3.Card 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.window.Dialog 24 | import org.jetbrains.compose.ui.tooling.preview.Preview 25 | import ui.theme.AppTheme 26 | 27 | @Composable 28 | fun ToDoEditingDialog( 29 | initialText: String, 30 | onSubmit: (text: String) -> Unit, 31 | onDelete: () -> Unit, 32 | onCancel: () -> Unit, 33 | ) { 34 | var text by remember { mutableStateOf(initialText) } 35 | Dialog(onDismissRequest = {}) { 36 | Card { 37 | Column(modifier = Modifier.padding(16.dp)) { 38 | OutlinedTextField( 39 | modifier = Modifier.fillMaxWidth(), 40 | value = text, 41 | singleLine = true, 42 | onValueChange = { text = it }, 43 | ) 44 | Spacer(modifier = Modifier.size(16.dp)) 45 | Button( 46 | modifier = Modifier.fillMaxWidth(), 47 | onClick = { onSubmit(text) }, 48 | ) { 49 | Text(text = "OK") 50 | } 51 | OutlinedButton( 52 | modifier = Modifier.fillMaxWidth(), 53 | onClick = onCancel, 54 | ) { 55 | Text(text = "Cancel") 56 | } 57 | Spacer(modifier = Modifier.size(16.dp)) 58 | Button( 59 | modifier = Modifier.fillMaxWidth(), 60 | colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), 61 | onClick = { onDelete() }, 62 | ) { 63 | Text(text = "Delete") 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | @Preview 71 | @Composable 72 | fun PreviewToDoEditingDialog() { 73 | AppTheme { 74 | Surface { 75 | ToDoEditingDialog( 76 | initialText = "hoge", 77 | onSubmit = {}, 78 | onCancel = {}, 79 | onDelete = {}, 80 | ) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServer.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalMockServer = compositionLocalOf { 6 | MockServerSucceed 7 | } 8 | 9 | interface MockServer { 10 | 11 | suspend fun getToDoList(): List 12 | 13 | suspend fun addToDo(value: String): List 14 | 15 | suspend fun editToDo(index: Int, value: String): List 16 | 17 | suspend fun removeToDo(index: Int): List 18 | } 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServerAllFailed.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import kotlinx.coroutines.delay 4 | import todolist.server.MockServer 5 | 6 | object MockServerAllFailed : MockServer { 7 | 8 | override suspend fun getToDoList(): List { 9 | delay(1000) 10 | throw NullPointerException() 11 | } 12 | 13 | override suspend fun addToDo(value: String): List { 14 | delay(1000) 15 | throw NullPointerException() 16 | } 17 | 18 | override suspend fun editToDo(index: Int, value: String): List { 19 | delay(1000) 20 | throw NullPointerException() 21 | } 22 | 23 | override suspend fun removeToDo(index: Int): List { 24 | delay(1000) 25 | throw NullPointerException() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServerLoadingSlow.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import kotlinx.coroutines.delay 4 | 5 | object MockServerLoadingSlow : MockServer { 6 | 7 | private var todoList: List = listOf( 8 | "Remember the milk", 9 | "Call bob at 5pm.", 10 | ) 11 | 12 | override suspend fun getToDoList(): List { 13 | delay(5000) 14 | return todoList 15 | } 16 | 17 | override suspend fun addToDo(value: String): List { 18 | delay(5000) 19 | todoList = todoList.toList() + value 20 | return todoList 21 | } 22 | 23 | override suspend fun editToDo(index: Int, value: String): List { 24 | delay(5000) 25 | todoList = todoList.toMutableList().apply { 26 | this[index] = value 27 | } 28 | return todoList 29 | } 30 | 31 | override suspend fun removeToDo(index: Int): List { 32 | delay(5000) 33 | todoList = todoList.toMutableList().apply { 34 | removeAt(index) 35 | } 36 | return todoList 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServerMutationFailed.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import kotlinx.coroutines.delay 4 | 5 | object MockServerMutationFailed : MockServer { 6 | 7 | private var todoList: List = listOf( 8 | "Remember the milk", 9 | "Call bob at 5pm.", 10 | ) 11 | 12 | override suspend fun getToDoList(): List { 13 | delay(1000) 14 | return todoList 15 | } 16 | 17 | override suspend fun addToDo(value: String): List { 18 | delay(1000) 19 | throw NullPointerException() 20 | } 21 | 22 | override suspend fun editToDo(index: Int, value: String): List { 23 | delay(1000) 24 | throw NullPointerException() 25 | } 26 | 27 | override suspend fun removeToDo(index: Int): List { 28 | delay(1000) 29 | throw NullPointerException() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServerRandomFailed.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlin.random.Random 5 | 6 | object MockServerRandomFailed : MockServer { 7 | 8 | private var todoList: List = listOf( 9 | "Remember the milk", 10 | "Call bob at 5pm.", 11 | ) 12 | 13 | override suspend fun getToDoList(): List { 14 | delay(1000) 15 | if (Random.nextBoolean()) throw NullPointerException() 16 | return todoList 17 | } 18 | 19 | override suspend fun addToDo(value: String): List { 20 | delay(1000) 21 | if (Random.nextBoolean()) throw NullPointerException() 22 | todoList = todoList.toList() + value 23 | return todoList 24 | } 25 | 26 | override suspend fun editToDo(index: Int, value: String): List { 27 | delay(1000) 28 | if (Random.nextBoolean()) throw NullPointerException() 29 | todoList = todoList.toMutableList().apply { 30 | this[index] = value 31 | } 32 | return todoList 33 | } 34 | 35 | override suspend fun removeToDo(index: Int): List { 36 | delay(1000) 37 | if (Random.nextBoolean()) throw NullPointerException() 38 | todoList = todoList.toMutableList().apply { 39 | removeAt(index) 40 | } 41 | return todoList 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/todolist/server/MockServerSucceed.kt: -------------------------------------------------------------------------------- 1 | package todolist.server 2 | 3 | import kotlinx.coroutines.delay 4 | 5 | object MockServerSucceed : MockServer { 6 | 7 | private var todoList: List = listOf( 8 | "Remember the milk", 9 | "Call bob at 5pm.", 10 | ) 11 | 12 | override suspend fun getToDoList(): List { 13 | delay(1000) 14 | return todoList 15 | } 16 | 17 | override suspend fun addToDo(value: String): List { 18 | delay(1000) 19 | todoList = todoList.toList() + value 20 | return todoList 21 | } 22 | 23 | override suspend fun editToDo(index: Int, value: String): List { 24 | delay(1000) 25 | todoList = todoList.toMutableList().apply { 26 | this[index] = value 27 | } 28 | return todoList 29 | } 30 | 31 | override suspend fun removeToDo(index: Int): List { 32 | delay(1000) 33 | todoList = todoList.toMutableList().apply { 34 | removeAt(index) 35 | } 36 | return todoList 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/ErrorContent.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.unit.dp 18 | import ui.theme.AppTheme 19 | import org.jetbrains.compose.ui.tooling.preview.Preview 20 | 21 | @Composable 22 | fun ErrorContent( 23 | onRetry: (() -> Unit)? = null, 24 | ) { 25 | Column( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(16.dp), 29 | verticalArrangement = Arrangement.Center, 30 | horizontalAlignment = Alignment.CenterHorizontally, 31 | ) { 32 | Text( 33 | text = "Validation failed.", 34 | textAlign = TextAlign.Center, 35 | style = MaterialTheme.typography.titleSmall, 36 | color = MaterialTheme.colorScheme.error, 37 | ) 38 | Spacer(modifier = Modifier.size(4.dp)) 39 | if (onRetry != null) { 40 | Button(onClick = onRetry) { 41 | Text("Retry") 42 | } 43 | } 44 | } 45 | } 46 | 47 | @Preview 48 | @Composable 49 | fun PreviewErrorContent() { 50 | AppTheme { 51 | Surface { 52 | ErrorContent {} 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/LoadingContent.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.CircularProgressIndicator 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import ui.theme.AppTheme 14 | import org.jetbrains.compose.ui.tooling.preview.Preview 15 | 16 | @Composable 17 | fun LoadingContent() { 18 | Column( 19 | modifier = Modifier 20 | .fillMaxSize() 21 | .padding(16.dp), 22 | verticalArrangement = Arrangement.Center, 23 | horizontalAlignment = Alignment.CenterHorizontally, 24 | ) { 25 | CircularProgressIndicator() 26 | } 27 | } 28 | 29 | @Preview 30 | @Composable 31 | fun PreviewLoadingContent() { 32 | AppTheme { 33 | Surface { 34 | LoadingContent() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple40 = Color(0xFF6650a4) 6 | val PurpleGrey40 = Color(0xFF625b71) 7 | val Pink40 = Color(0xFF7D5260) 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package ui.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | private val LightColorScheme = lightColorScheme( 8 | primary = Purple40, 9 | secondary = PurpleGrey40, 10 | tertiary = Pink40, 11 | ) 12 | 13 | @Composable 14 | fun AppTheme( 15 | content: @Composable () -> Unit 16 | ) { 17 | MaterialTheme( 18 | colorScheme = LightColorScheme, 19 | typography = Typography, 20 | content = content 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) 35 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import androidx.compose.ui.window.application 3 | 4 | fun main() = application { 5 | Window( 6 | onCloseRequest = ::exitApplication, 7 | title = "SWR Compose", 8 | ) { 9 | App() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | 3 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | version = "0.7.1" 3 | 4 | compileSdk = "34" 5 | minSdk = "21" 6 | targetSdk = "34" 7 | 8 | kotlin = "2.0.20" 9 | coroutines = "1.9.0" 10 | compose-ui = "1.7.2" 11 | android-plugin = "8.6.1" 12 | 13 | [libraries] 14 | # Kotlinx Coroutines 15 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } 16 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } 17 | # Kotlinx DateTime 18 | kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.6.1" 19 | # AndroidX Compose UI Test 20 | androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-ui" } 21 | androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose-ui" } 22 | # AndroidX Activity Compose 23 | androidx-activity-compose = "androidx.activity:activity-compose:1.9.2" 24 | # AndroidX Navigation Compose 25 | androidx-navigation-compose = "org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10" 26 | # AndroidX Lifecycle Runtime 27 | androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime:2.8.6" 28 | # Konnection 29 | konnection = "dev.tmapps:konnection:1.4.2" 30 | # MockK 31 | mockk = "io.mockk:mockk:1.13.12" 32 | # Robolectric 33 | robolectric = "org.robolectric:robolectric:4.13" 34 | 35 | [plugins] 36 | # Kotlin 37 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 38 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } # https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility 39 | # Compose 40 | compose = { id = "org.jetbrains.compose", version = "1.6.11" } 41 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 42 | # Dokka 43 | dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } 44 | # Android 45 | android-application = { id = "com.android.application", version.ref = "android-plugin" } 46 | android-library = { id = "com.android.library", version.ref = "android-plugin" } 47 | # Nexus Publish 48 | nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } 49 | # Binary Compatibility Validator 50 | binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.16.3" } 51 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /iosApp/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode 3 | 4 | ### Xcode ### 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Xcode 8 and earlier 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ### Xcode Patch ### 13 | *.xcodeproj/* 14 | !*.xcodeproj/project.pbxproj 15 | !*.xcodeproj/xcshareddata/ 16 | !*.xcodeproj/project.xcworkspace/ 17 | !*.xcworkspace/contents.xcworkspacedata 18 | /*.gcno 19 | **/xcshareddata/WorkspaceSettings.xcsettings 20 | 21 | # End of https://www.toptal.com/developers/gitignore/api/xcode 22 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.kazakago.swr.compose.example 3 | APP_NAME=SWR Compose 4 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazakago/swr-compose/53ef41c3fcabc4d4a8a650406860a01b24a1d361/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "swr_compose" 17 | include(":composeApp") 18 | include(":swr") 19 | 20 | // https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:type-safe-project-accessors 21 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 22 | -------------------------------------------------------------------------------- /swr/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/DummyException.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | object DummyException1 : Exception() 4 | object DummyException2 : Exception() 5 | object DummyException3 : Exception() 6 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/NetworkRule.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import com.kazakago.swr.compose.internal.GlobalKonnection 4 | import dev.tmapps.konnection.Konnection 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import kotlinx.coroutines.channels.Channel 8 | import kotlinx.coroutines.flow.receiveAsFlow 9 | import org.junit.rules.TestRule 10 | import org.junit.runner.Description 11 | import org.junit.runners.model.Statement 12 | 13 | class NetworkRule : TestRule { 14 | 15 | private val konnection = mockk() 16 | private val connectionObserver = Channel(Channel.CONFLATED) 17 | 18 | override fun apply(base: Statement, description: Description): Statement { 19 | return object : Statement() { 20 | override fun evaluate() { 21 | try { 22 | setup() 23 | base.evaluate() 24 | } finally { 25 | teardown() 26 | } 27 | } 28 | } 29 | } 30 | 31 | private fun setup() { 32 | every { konnection.isConnected() } returns true 33 | every { konnection.observeHasConnection() } returns connectionObserver.receiveAsFlow() 34 | GlobalKonnection = konnection 35 | } 36 | 37 | private fun teardown() { 38 | } 39 | 40 | fun changeNetwork(isConnected: Boolean) { 41 | every { konnection.isConnected() } returns isConnected 42 | connectionObserver.trySend(isConnected) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/config/GlobalConfigutationsTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.config 2 | 3 | import androidx.compose.ui.test.junit4.createComposeRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.kazakago.swr.compose.NetworkRule 6 | import com.kazakago.swr.compose.useSWR 7 | import com.kazakago.swr.compose.useSWRConfig 8 | import org.junit.Rule 9 | import org.junit.runner.RunWith 10 | import kotlin.random.Random 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.time.Duration.Companion.seconds 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class GlobalConfigutationsTest { 17 | 18 | @get:Rule 19 | val networkRule = NetworkRule() 20 | 21 | @get:Rule 22 | val composeTestRule = createComposeRule().apply { 23 | mainClock.autoAdvance = false 24 | } 25 | 26 | @Test 27 | fun defaultOptions() { 28 | val key = Random.nextInt().toString() 29 | val globalFetcher: suspend (Any) -> Any = { "${it}_fetched" } 30 | var data: String? = null 31 | composeTestRule.setContent { 32 | val (_, config) = useSWRConfig() 33 | assertEquals(null, config.fetcher) 34 | assertEquals(2.seconds, config.dedupingInterval) 35 | SWRConfig(options = { 36 | fetcher = globalFetcher 37 | dedupingInterval = 10.seconds 38 | }) { 39 | val state = useSWR(key = key) { 40 | @Suppress("UNCHECKED_CAST") 41 | assertEquals(globalFetcher, fetcher as suspend (Any) -> Any) 42 | assertEquals(10.seconds, dedupingInterval) 43 | } 44 | data = state.data 45 | } 46 | } 47 | 48 | composeTestRule.mainClock.advanceTimeBy(1) 49 | assertEquals("${key}_fetched", data) 50 | } 51 | 52 | @Test 53 | fun nestedDefaultOptions() { 54 | val globalFetcher1: suspend (Any) -> Any = { "${it}_fetched_1" } 55 | val globalFetcher2: suspend (Any) -> Any = { "${it}_fetched_2" } 56 | val key1 = "${object {}.javaClass.enclosingMethod?.name}_1" 57 | val key2 = "${object {}.javaClass.enclosingMethod?.name}_2" 58 | val key3 = "${object {}.javaClass.enclosingMethod?.name}_3" 59 | var data1: String? = null 60 | var data2: String? = null 61 | var data3: String? = null 62 | composeTestRule.setContent { 63 | val (_, config) = useSWRConfig() 64 | assertEquals(null, config.fetcher) 65 | assertEquals(2.seconds, config.dedupingInterval) 66 | SWRConfig(options = { 67 | fetcher = globalFetcher1 68 | dedupingInterval = 10.seconds 69 | }) { 70 | val state1 = useSWR(key = key1) { 71 | @Suppress("UNCHECKED_CAST") 72 | assertEquals(globalFetcher1, fetcher as suspend (Any) -> Any) 73 | assertEquals(10.seconds, dedupingInterval) 74 | } 75 | data1 = state1.data 76 | 77 | SWRConfig(options = { 78 | fetcher = globalFetcher2 79 | dedupingInterval = 20.seconds 80 | }) { 81 | val state2 = useSWR(key = key2) { 82 | @Suppress("UNCHECKED_CAST") 83 | assertEquals(globalFetcher2, fetcher as suspend (Any) -> Any) 84 | assertEquals(20.seconds, dedupingInterval) 85 | } 86 | data2 = state2.data 87 | } 88 | 89 | val state3 = useSWR(key = key3) { 90 | @Suppress("UNCHECKED_CAST") 91 | assertEquals(globalFetcher1, fetcher as suspend (Any) -> Any) 92 | assertEquals(10.seconds, dedupingInterval) 93 | } 94 | data3 = state3.data 95 | } 96 | } 97 | 98 | composeTestRule.mainClock.advanceTimeBy(1) 99 | assertEquals("${key1}_fetched_1", data1) 100 | assertEquals("${key2}_fetched_2", data2) 101 | assertEquals("${key3}_fetched_1", data3) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/config/UseSWRConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.config 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import com.kazakago.swr.compose.useSWRConfig 12 | import kotlinx.coroutines.delay 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class UseSWRConfigTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun mutation() { 32 | val key = Random.nextInt().toString() 33 | val stateList = mutableListOf>() 34 | composeTestRule.setContent { 35 | SWRGlobalScope = rememberCoroutineScope() 36 | stateList += useSWR(key = key, fetcher = { 37 | delay(100) 38 | "fetched" 39 | }) { 40 | revalidateOnMount = false 41 | } 42 | val (mutate, _, _) = useSWRConfig() 43 | LaunchedEffect(Unit) { 44 | mutate(key = key) 45 | } 46 | } 47 | 48 | composeTestRule.mainClock.advanceTimeBy(1000) 49 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 50 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 51 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 52 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/immutable/UseSWRImmutableTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.immutable 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import com.kazakago.swr.compose.NetworkRule 9 | import com.kazakago.swr.compose.cache.LocalSWRCache 10 | import com.kazakago.swr.compose.internal.SWRGlobalScope 11 | import com.kazakago.swr.compose.state.SWRState 12 | import com.kazakago.swr.compose.useSWRImmutable 13 | import kotlinx.coroutines.delay 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import kotlin.random.Random 17 | import kotlin.test.Test 18 | import kotlin.test.assertEquals 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class UseSWRImmutableTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createAndroidComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun immutable() { 33 | val key = Random.nextInt().toString() 34 | val stateList = mutableListOf>() 35 | composeTestRule.setContent { 36 | SWRGlobalScope = rememberCoroutineScope() 37 | LocalSWRCache.current.state(key = key).let { 38 | if (it.value == null) it.value = "cached" 39 | } 40 | stateList += useSWRImmutable(key = key, fetcher = { 41 | delay(100) 42 | "fetched" 43 | }) 44 | } 45 | 46 | composeTestRule.mainClock.advanceTimeBy(5000) 47 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED) 48 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) 49 | composeTestRule.mainClock.advanceTimeBy(5000) 50 | 51 | assertEquals(listOf("cached"), stateList.map { it.data }) 52 | assertEquals(listOf(null), stateList.map { it.error }) 53 | assertEquals(listOf(false), stateList.map { it.isLoading }) 54 | assertEquals(listOf(false), stateList.map { it.isValidating }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/infinite/InitialSizeOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.infinite 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRInfiniteState 9 | import com.kazakago.swr.compose.useSWRInfinite 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class InitialSizeOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun initialSize1() { 30 | val key = Random.nextInt().toString() 31 | val stateList = mutableListOf>() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | stateList += useSWRInfinite(getKey = { pageIndex, _ -> "${key}_${pageIndex}" }, fetcher = { 35 | delay(100) 36 | "fetched" 37 | }) { 38 | initialSize = 1 39 | } 40 | } 41 | 42 | composeTestRule.mainClock.advanceTimeBy(2500) 43 | assertEquals(listOf(null, null, listOf("fetched")), stateList.map { it.data }) 44 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 45 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 46 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 47 | } 48 | 49 | @Test 50 | fun initialSize3() { 51 | val key = Random.nextInt().toString() 52 | val stateList = mutableListOf>() 53 | composeTestRule.setContent { 54 | SWRGlobalScope = rememberCoroutineScope() 55 | stateList += useSWRInfinite(getKey = { pageIndex, _ -> "${key}_${pageIndex}" }, fetcher = { 56 | delay(100) 57 | "fetched" 58 | }) { 59 | initialSize = 3 60 | } 61 | } 62 | 63 | composeTestRule.mainClock.advanceTimeBy(2500) 64 | assertEquals(listOf(null, null, listOf("fetched", "fetched", "fetched")), stateList.map { it.data }) 65 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 66 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 67 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/infinite/UseSWRInfiniteTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.infinite 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRInfiniteState 9 | import com.kazakago.swr.compose.useSWRInfinite 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class UseSWRInfiniteTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun validate() { 32 | val key = Random.nextInt().toString() 33 | val stateList = mutableListOf>() 34 | composeTestRule.setContent { 35 | SWRGlobalScope = rememberCoroutineScope() 36 | stateList += useSWRInfinite(getKey = { pageIndex, _ -> "${key}_${pageIndex}" }, fetcher = { 37 | delay(100) 38 | "fetched" 39 | }) 40 | } 41 | 42 | composeTestRule.mainClock.advanceTimeBy(2500) 43 | assertEquals(listOf(null, null, listOf("fetched")), stateList.map { it.data }) 44 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 45 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 46 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 47 | } 48 | 49 | @Test 50 | fun incrementSetSize() { 51 | val key = Random.nextInt().toString() 52 | val stateList = mutableListOf>() 53 | lateinit var scope: CoroutineScope 54 | composeTestRule.setContent { 55 | scope = rememberCoroutineScope() 56 | SWRGlobalScope = rememberCoroutineScope() 57 | stateList += useSWRInfinite(getKey = { pageIndex, _ -> "${key}_${pageIndex}" }, fetcher = { 58 | delay(100) 59 | "fetched_$it" 60 | }) 61 | } 62 | 63 | composeTestRule.mainClock.advanceTimeBy(2500) 64 | val (_, _, _, _, _, size, setSize) = stateList.last() 65 | scope.launch { setSize(size + 1) } 66 | 67 | composeTestRule.mainClock.advanceTimeBy(2500) 68 | assertEquals(listOf(null, null, listOf("fetched_${key}_1"), listOf("fetched_${key}_1", null), listOf("fetched_${key}_1", null), listOf("fetched_${key}_1", "fetched_${key}_2")), stateList.map { it.data }) 69 | assertEquals(listOf(null, null, null, null, null, null), stateList.map { it.error }) 70 | assertEquals(listOf(false, true, false, false, false, false), stateList.map { it.isLoading }) 71 | assertEquals(listOf(false, true, false, false, true, false), stateList.map { it.isValidating }) 72 | assertEquals(listOf(1, 1, 1, 2, 2, 2), stateList.map { it.size }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/mutation/OptimisticOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class OptimisticOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun withOptimisticData() { 32 | val key = Random.nextInt().toString() 33 | var result: () -> String = { "fetched_1" } 34 | val stateList = mutableListOf>() 35 | lateinit var scope: CoroutineScope 36 | composeTestRule.setContent { 37 | scope = rememberCoroutineScope() 38 | SWRGlobalScope = rememberCoroutineScope() 39 | stateList += useSWR(key = key, fetcher = { 40 | delay(100) 41 | result() 42 | }) 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(2500) 46 | result = { "fetched_2" } 47 | scope.launch { 48 | stateList.last().mutate(data = { 49 | delay(100) 50 | "mutated" 51 | }) { 52 | optimisticData = "optimisticData" 53 | } 54 | } 55 | 56 | composeTestRule.mainClock.advanceTimeBy(2500) 57 | assertEquals(listOf(null, null, "fetched_1", "optimisticData", "mutated", "fetched_2"), stateList.map { it.data }) 58 | assertEquals(listOf(null, null, null, null, null, null), stateList.map { it.error }) 59 | assertEquals(listOf(false, true, false, false, false, false), stateList.map { it.isLoading }) 60 | assertEquals(listOf(false, true, false, false, true, false), stateList.map { it.isValidating }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/mutation/PopulateCacheOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class PopulateCacheOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun noPopulateCache() { 32 | val key = Random.nextInt().toString() 33 | var result: () -> String = { "fetched_1" } 34 | val stateList = mutableListOf>() 35 | lateinit var scope: CoroutineScope 36 | composeTestRule.setContent { 37 | scope = rememberCoroutineScope() 38 | SWRGlobalScope = rememberCoroutineScope() 39 | stateList += useSWR(key = key, fetcher = { 40 | delay(100) 41 | result() 42 | }) 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(2500) 46 | result = { "fetched_2" } 47 | scope.launch { 48 | stateList.last().mutate(data = { 49 | delay(100) 50 | "mutated" 51 | }) { 52 | populateCache = false 53 | } 54 | } 55 | 56 | composeTestRule.mainClock.advanceTimeBy(2500) 57 | assertEquals(listOf(null, null, "fetched_1", "fetched_1", "fetched_2"), stateList.map { it.data }) 58 | assertEquals(listOf(null, null, null, null, null), stateList.map { it.error }) 59 | assertEquals(listOf(false, true, false, false, false), stateList.map { it.isLoading }) 60 | assertEquals(listOf(false, true, false, true, false), stateList.map { it.isValidating }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/mutation/RevalidateOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class RevalidateOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun noRevalidate() { 32 | val key = Random.nextInt().toString() 33 | var result: () -> String = { "fetched_1" } 34 | val stateList = mutableListOf>() 35 | lateinit var scope: CoroutineScope 36 | composeTestRule.setContent { 37 | scope = rememberCoroutineScope() 38 | SWRGlobalScope = rememberCoroutineScope() 39 | stateList += useSWR(key = key, fetcher = { 40 | delay(100) 41 | result() 42 | }) 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(2500) 46 | result = { "fetched_2" } 47 | scope.launch { 48 | stateList.last().mutate(data = { 49 | delay(100) 50 | "mutated" 51 | }) { 52 | revalidate = false 53 | } 54 | } 55 | 56 | composeTestRule.mainClock.advanceTimeBy(2500) 57 | assertEquals(listOf(null, null, "fetched_1", "mutated"), stateList.map { it.data }) 58 | assertEquals(listOf(null, null, null, null), stateList.map { it.error }) 59 | assertEquals(listOf(false, true, false, false), stateList.map { it.isLoading }) 60 | assertEquals(listOf(false, true, false, false), stateList.map { it.isValidating }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/mutation/RollbackOnErrorOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import kotlin.random.Random 17 | import kotlin.test.Test 18 | import kotlin.test.assertEquals 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class RollbackOnErrorOptionTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun noRollbackOnError() { 33 | val key = Random.nextInt().toString() 34 | var result: () -> String = { "fetched_1" } 35 | val stateList = mutableListOf>() 36 | lateinit var scope: CoroutineScope 37 | composeTestRule.setContent { 38 | scope = rememberCoroutineScope() 39 | SWRGlobalScope = rememberCoroutineScope() 40 | stateList += useSWR(key = key, fetcher = { 41 | delay(100) 42 | result() 43 | }) 44 | } 45 | 46 | composeTestRule.mainClock.advanceTimeBy(2500) 47 | result = { "fetched_2" } 48 | var mutationError: Throwable? = null 49 | scope.launch { 50 | runCatching { 51 | stateList.last().mutate(data = { 52 | delay(100) 53 | throw DummyException1 54 | }) { 55 | optimisticData = "optimisticData" 56 | rollbackOnError = false 57 | } 58 | }.onFailure { 59 | mutationError = it 60 | } 61 | } 62 | 63 | composeTestRule.mainClock.advanceTimeBy(2500) 64 | assertEquals(listOf(null, null, "fetched_1", "optimisticData"), stateList.map { it.data }) 65 | assertEquals(listOf(null, null, null, null), stateList.map { it.error }) 66 | assertEquals(listOf(false, true, false, false), stateList.map { it.isLoading }) 67 | assertEquals(listOf(false, true, false, false), stateList.map { it.isValidating }) 68 | assertEquals(DummyException1, mutationError) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/mutation/ThrowOnErrorOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import kotlin.random.Random 17 | import kotlin.test.Test 18 | import kotlin.test.assertEquals 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class ThrowOnErrorOptionTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun withThrowOnError() { 33 | val key = Random.nextInt().toString() 34 | val stateList = mutableListOf>() 35 | lateinit var scope: CoroutineScope 36 | composeTestRule.setContent { 37 | scope = rememberCoroutineScope() 38 | SWRGlobalScope = rememberCoroutineScope() 39 | stateList += useSWR(key = key, fetcher = { 40 | delay(100) 41 | "fetched" 42 | }) 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(2500) 46 | var mutationError: Throwable? = null 47 | scope.launch { 48 | runCatching { 49 | stateList.last().mutate(data = { 50 | delay(100) 51 | throw DummyException1 52 | }) { 53 | throwOnError = true 54 | } 55 | }.onFailure { 56 | mutationError = it 57 | } 58 | } 59 | 60 | composeTestRule.mainClock.advanceTimeBy(2500) 61 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 62 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 63 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 64 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 65 | assertEquals(DummyException1, mutationError) 66 | } 67 | 68 | @Test 69 | fun noThrowOnError() { 70 | val key = Random.nextInt().toString() 71 | val stateList = mutableListOf>() 72 | lateinit var scope: CoroutineScope 73 | composeTestRule.setContent { 74 | scope = rememberCoroutineScope() 75 | SWRGlobalScope = rememberCoroutineScope() 76 | stateList += useSWR(key = key, fetcher = { 77 | delay(100) 78 | "fetched" 79 | }) 80 | } 81 | 82 | composeTestRule.mainClock.advanceTimeBy(2500) 83 | var mutationError: Throwable? = null 84 | scope.launch { 85 | runCatching { 86 | stateList.last().mutate(data = { 87 | delay(100) 88 | throw DummyException1 89 | }) { 90 | throwOnError = false 91 | } 92 | }.onFailure { 93 | mutationError = it 94 | } 95 | } 96 | 97 | composeTestRule.mainClock.advanceTimeBy(2500) 98 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 99 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 100 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 101 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 102 | assertEquals(null, mutationError) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/prefetch/UsePreloadTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.prefetch 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import com.kazakago.swr.compose.DummyException1 8 | import com.kazakago.swr.compose.NetworkRule 9 | import com.kazakago.swr.compose.internal.SWRGlobalScope 10 | import com.kazakago.swr.compose.state.SWRState 11 | import com.kazakago.swr.compose.useSWR 12 | import com.kazakago.swr.compose.useSWRPreload 13 | import kotlinx.coroutines.delay 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import kotlin.random.Random 17 | import kotlin.test.Test 18 | import kotlin.test.assertEquals 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class UsePreloadTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun prefetch() { 33 | val key = Random.nextInt().toString() 34 | val stateList = mutableListOf>() 35 | composeTestRule.setContent { 36 | SWRGlobalScope = rememberCoroutineScope() 37 | stateList += useSWR(key = key) { 38 | revalidateOnMount = false 39 | } 40 | val preload = useSWRPreload(key = key, fetcher = { 41 | delay(100) 42 | "fetched" 43 | }) 44 | LaunchedEffect(Unit) { 45 | preload() 46 | } 47 | } 48 | 49 | composeTestRule.mainClock.advanceTimeBy(1000) 50 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 51 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 52 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 53 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 54 | } 55 | 56 | @Test 57 | fun prefetchFailed() { 58 | val key = Random.nextInt().toString() 59 | val stateList = mutableListOf>() 60 | composeTestRule.setContent { 61 | SWRGlobalScope = rememberCoroutineScope() 62 | stateList += useSWR(key = key) { 63 | revalidateOnMount = false 64 | } 65 | val preload = useSWRPreload(key = key, fetcher = { 66 | delay(100) 67 | throw DummyException1 68 | }) 69 | LaunchedEffect(Unit) { 70 | preload() 71 | } 72 | } 73 | 74 | composeTestRule.mainClock.advanceTimeBy(1000) 75 | assertEquals(listOf(null, null, null), stateList.map { it.data }) 76 | assertEquals(listOf(null, null, DummyException1), stateList.map { it.error }) 77 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 78 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/trigger/OptimisticOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.trigger 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRMutationState 9 | import com.kazakago.swr.compose.useSWRMutation 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class OptimisticOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun trigger_withOptimisticData() { 32 | val key = Random.nextInt().toString() 33 | val stateList = mutableListOf>() 34 | lateinit var scope: CoroutineScope 35 | composeTestRule.setContent { 36 | scope = rememberCoroutineScope() 37 | SWRGlobalScope = rememberCoroutineScope() 38 | stateList += useSWRMutation(key = key, fetcher = { _, arg -> 39 | delay(100) 40 | arg 41 | }) { 42 | optimisticData = "optimisticData" 43 | } 44 | } 45 | 46 | composeTestRule.mainClock.advanceTimeBy(2500) 47 | assertEquals(listOf(null), stateList.map { it.data }) 48 | assertEquals(listOf(null), stateList.map { it.error }) 49 | assertEquals(listOf(false), stateList.map { it.isMutating }) 50 | var triggerResult: String? = null 51 | var mutationError: Throwable? = null 52 | scope.launch { 53 | runCatching { 54 | stateList.last().trigger("trigger") 55 | }.onSuccess { 56 | triggerResult = it 57 | }.onFailure { 58 | mutationError = it 59 | } 60 | } 61 | 62 | composeTestRule.mainClock.advanceTimeBy(2500) 63 | assertEquals(listOf(null, "optimisticData", "trigger"), stateList.map { it.data }) 64 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 65 | assertEquals(listOf(false, true, false), stateList.map { it.isMutating }) 66 | assertEquals("trigger", triggerResult) 67 | assertEquals(null, mutationError) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/ErrorRetryCountOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class ErrorRetryCountOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun errorRetryCountNull() { 30 | val key = Random.nextInt().toString() 31 | val errorRetryCountList = mutableListOf() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | useSWR(key = key, fetcher = { 35 | delay(100) 36 | throw DummyException1 37 | }) { 38 | errorRetryCount = null 39 | onErrorRetry = { _, _, config, _, _ -> 40 | errorRetryCountList += config.errorRetryCount 41 | } 42 | } 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(1000) 46 | assertEquals(listOf(null), errorRetryCountList) 47 | } 48 | 49 | @Test 50 | fun errorRetryCount3() { 51 | val key = Random.nextInt().toString() 52 | val errorRetryCountList = mutableListOf() 53 | composeTestRule.setContent { 54 | SWRGlobalScope = rememberCoroutineScope() 55 | useSWR(key = key, fetcher = { 56 | delay(100) 57 | throw DummyException1 58 | }) { 59 | errorRetryCount = 3 60 | onErrorRetry = { _, _, config, _, _ -> 61 | errorRetryCountList += config.errorRetryCount 62 | } 63 | } 64 | } 65 | 66 | composeTestRule.mainClock.advanceTimeBy(1000) 67 | assertEquals(listOf(3), errorRetryCountList) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/ErrorRetryIntervalOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.time.Duration 17 | import kotlin.time.Duration.Companion.seconds 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class ErrorRetryIntervalOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun errorRetryIntervalOption5seconds() { 32 | val key = Random.nextInt().toString() 33 | val errorRetryIntervalList = mutableListOf() 34 | composeTestRule.setContent { 35 | SWRGlobalScope = rememberCoroutineScope() 36 | useSWR(key = key, fetcher = { 37 | delay(100) 38 | throw DummyException1 39 | }) { 40 | errorRetryInterval = 5.seconds 41 | onErrorRetry = { _, _, config, _, _ -> 42 | errorRetryIntervalList += config.errorRetryInterval 43 | } 44 | } 45 | } 46 | 47 | composeTestRule.mainClock.advanceTimeBy(1000) 48 | assertEquals(listOf(5.seconds), errorRetryIntervalList) 49 | } 50 | 51 | @Test 52 | fun errorRetryIntervalOption10seconds() { 53 | val key = Random.nextInt().toString() 54 | val errorRetryIntervalList = mutableListOf() 55 | composeTestRule.setContent { 56 | SWRGlobalScope = rememberCoroutineScope() 57 | useSWR(key = key, fetcher = { 58 | delay(100) 59 | throw DummyException1 60 | }) { 61 | errorRetryInterval = 10.seconds 62 | onErrorRetry = { _, _, config, _, _ -> 63 | errorRetryIntervalList += config.errorRetryInterval 64 | } 65 | } 66 | } 67 | 68 | composeTestRule.mainClock.advanceTimeBy(1000) 69 | assertEquals(listOf(10.seconds), errorRetryIntervalList) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/FallbackDataOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class FallbackDataOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun noFallbackData() { 30 | val key = Random.nextInt().toString() 31 | val stateList = mutableListOf>() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | stateList += useSWR(key = key, fetcher = { 35 | delay(100) 36 | "fetched" 37 | }) { 38 | fallbackData = null 39 | } 40 | } 41 | 42 | composeTestRule.mainClock.advanceTimeBy(1000) 43 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 44 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 45 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 46 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 47 | } 48 | 49 | @Test 50 | fun withFallbackData() { 51 | val key = Random.nextInt().toString() 52 | val stateList = mutableListOf>() 53 | composeTestRule.setContent { 54 | SWRGlobalScope = rememberCoroutineScope() 55 | stateList += useSWR(key = key, fetcher = { 56 | delay(100) 57 | "fetched" 58 | }) { 59 | fallbackData = "fallback" 60 | } 61 | } 62 | 63 | composeTestRule.mainClock.advanceTimeBy(1000) 64 | assertEquals(listOf("fallback"), stateList.map { it.data }) 65 | assertEquals(listOf(null), stateList.map { it.error }) 66 | assertEquals(listOf(false), stateList.map { it.isLoading }) 67 | assertEquals(listOf(false), stateList.map { it.isValidating }) 68 | } 69 | 70 | @Test 71 | fun withFallbackDataAndRevalidateOnMount() { 72 | val key = Random.nextInt().toString() 73 | val stateList = mutableListOf>() 74 | composeTestRule.setContent { 75 | SWRGlobalScope = rememberCoroutineScope() 76 | stateList += useSWR(key = key, fetcher = { 77 | delay(100) 78 | "fetched" 79 | }) { 80 | fallbackData = "fallback" 81 | revalidateOnMount = true 82 | } 83 | } 84 | 85 | composeTestRule.mainClock.advanceTimeBy(1000) 86 | assertEquals(listOf("fallback", "fallback", "fetched"), stateList.map { it.data }) 87 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 88 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 89 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/FallbackOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.config.SWRConfig 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class FallbackOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun noFallback() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | SWRConfig(options = { 36 | fallback = emptyMap() 37 | }) { 38 | stateList += useSWR(key = key, fetcher = { 39 | delay(100) 40 | "fetched" 41 | }) 42 | } 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(1000) 46 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 47 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 48 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 49 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 50 | } 51 | 52 | @Test 53 | fun withFallback() { 54 | val key = Random.nextInt().toString() 55 | val stateList = mutableListOf>() 56 | composeTestRule.setContent { 57 | SWRGlobalScope = rememberCoroutineScope() 58 | SWRConfig(options = { 59 | fallback = mapOf(key to "fallback") 60 | }) { 61 | stateList += useSWR(key = key, fetcher = { 62 | delay(100) 63 | "fetched" 64 | }) 65 | } 66 | } 67 | 68 | composeTestRule.mainClock.advanceTimeBy(1000) 69 | assertEquals(listOf("fallback"), stateList.map { it.data }) 70 | assertEquals(listOf(null), stateList.map { it.error }) 71 | assertEquals(listOf(false), stateList.map { it.isLoading }) 72 | assertEquals(listOf(false), stateList.map { it.isValidating }) 73 | } 74 | 75 | @Test 76 | fun withFallbackAndRevalidateOnMount() { 77 | val key = Random.nextInt().toString() 78 | val stateList = mutableListOf>() 79 | composeTestRule.setContent { 80 | SWRGlobalScope = rememberCoroutineScope() 81 | SWRConfig(options = { 82 | fallback = mapOf(key to "fallback") 83 | }) { 84 | stateList += useSWR(key = key, fetcher = { 85 | delay(100) 86 | "fetched" 87 | }) { 88 | revalidateOnMount = true 89 | } 90 | } 91 | } 92 | 93 | composeTestRule.mainClock.advanceTimeBy(1000) 94 | assertEquals(listOf("fallback", "fallback", "fetched"), stateList.map { it.data }) 95 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 96 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 97 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/FetcherOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.config.SWRConfig 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class FetcherOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun localFetcher() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | stateList += useSWR(key = key) { 36 | fetcher = { 37 | delay(100) 38 | "fetched" 39 | } 40 | } 41 | } 42 | 43 | composeTestRule.mainClock.advanceTimeBy(2500) 44 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 45 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 46 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 47 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 48 | } 49 | 50 | @Test 51 | fun savedFetcher() { 52 | val key = Random.nextInt().toString() 53 | val stateList = mutableListOf>() 54 | val stateList2 = mutableListOf>() 55 | composeTestRule.setContent { 56 | SWRGlobalScope = rememberCoroutineScope() 57 | stateList += useSWR(key = key, fetcher = { 58 | delay(100) 59 | "fetched" 60 | }) 61 | stateList2 += useSWR(key = key) 62 | } 63 | 64 | composeTestRule.mainClock.advanceTimeBy(2500) 65 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 66 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 67 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 68 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 69 | 70 | assertEquals(listOf(null, null, "fetched"), stateList2.map { it.data }) 71 | assertEquals(listOf(null, null, null), stateList2.map { it.error }) 72 | assertEquals(listOf(false, true, false), stateList2.map { it.isLoading }) 73 | assertEquals(listOf(false, true, false), stateList2.map { it.isValidating }) 74 | } 75 | 76 | @Test 77 | fun globalFetcher() { 78 | val key = Random.nextInt().toString() 79 | val stateList = mutableListOf>() 80 | composeTestRule.setContent { 81 | SWRGlobalScope = rememberCoroutineScope() 82 | SWRConfig( 83 | options = { 84 | fetcher = { 85 | delay(100) 86 | "fetched" 87 | } 88 | } 89 | ) { 90 | stateList += useSWR(key = key) 91 | } 92 | } 93 | composeTestRule.mainClock.advanceTimeBy(2500) 94 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 95 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 96 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 97 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/IsPausedOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import kotlin.random.Random 17 | import kotlin.test.Test 18 | import kotlin.test.assertEquals 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class IsPausedOptionTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun withIsPaused() { 33 | val key = Random.nextInt().toString() 34 | val stateList = mutableListOf>() 35 | lateinit var scope: CoroutineScope 36 | composeTestRule.setContent { 37 | scope = rememberCoroutineScope() 38 | SWRGlobalScope = rememberCoroutineScope() 39 | stateList += useSWR(key = key, fetcher = { 40 | delay(100) 41 | throw DummyException1 42 | }) { 43 | isPaused = { true } 44 | } 45 | } 46 | 47 | composeTestRule.mainClock.advanceTimeBy(2500) 48 | scope.launch { stateList.last().mutate() } 49 | 50 | composeTestRule.mainClock.advanceTimeBy(2500) 51 | assertEquals(listOf(null), stateList.map { it.data }) 52 | assertEquals(listOf(null), stateList.map { it.error }) 53 | assertEquals(listOf(false), stateList.map { it.isLoading }) 54 | assertEquals(listOf(false), stateList.map { it.isValidating }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/LoadingTimeoutOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.useSWR 9 | import kotlinx.coroutines.delay 10 | import org.junit.Rule 11 | import org.junit.runner.RunWith 12 | import kotlin.random.Random 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class LoadingTimeoutOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun focusThrottleInterval3Seconds() { 30 | val key = Random.nextInt().toString() 31 | val keyList = mutableListOf() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | useSWR(key = key, fetcher = { 35 | delay(10.seconds) 36 | "fetched" 37 | }) { 38 | loadingTimeout = 3.seconds 39 | onLoadingSlow = { key, _ -> 40 | keyList.add(key) 41 | } 42 | } 43 | } 44 | 45 | composeTestRule.mainClock.advanceTimeBy(2000) 46 | assertEquals(emptyList(), keyList) 47 | composeTestRule.mainClock.advanceTimeBy(2000) 48 | assertEquals(listOf(key), keyList) 49 | } 50 | 51 | @Test 52 | fun focusThrottleInterval5Seconds() { 53 | val key = Random.nextInt().toString() 54 | val keyList = mutableListOf() 55 | composeTestRule.setContent { 56 | SWRGlobalScope = rememberCoroutineScope() 57 | useSWR(key = key, fetcher = { 58 | delay(10.seconds) 59 | "fetched" 60 | }) { 61 | loadingTimeout = 5.seconds 62 | onLoadingSlow = { key, _ -> 63 | keyList.add(key) 64 | } 65 | } 66 | } 67 | 68 | composeTestRule.mainClock.advanceTimeBy(2000) 69 | assertEquals(emptyList(), keyList) 70 | composeTestRule.mainClock.advanceTimeBy(2000) 71 | assertEquals(emptyList(), keyList) 72 | } 73 | 74 | @Test 75 | fun focusThrottleInterval0Seconds() { 76 | val key = Random.nextInt().toString() 77 | val keyList = mutableListOf() 78 | composeTestRule.setContent { 79 | SWRGlobalScope = rememberCoroutineScope() 80 | useSWR(key = key, fetcher = { 81 | delay(10.seconds) 82 | "fetched" 83 | }) { 84 | loadingTimeout = 0.seconds 85 | onLoadingSlow = { key, _ -> 86 | keyList.add(key) 87 | } 88 | } 89 | } 90 | 91 | composeTestRule.mainClock.advanceTimeBy(2000) 92 | assertEquals(listOf(key), keyList) 93 | composeTestRule.mainClock.advanceTimeBy(2000) 94 | assertEquals(listOf(key), keyList) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/OnErrorOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.test.fail 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class OnErrorOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun withOnError() { 31 | val key = Random.nextInt().toString() 32 | val onErrorList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | useSWR(key = key, fetcher = { 36 | delay(100) 37 | throw DummyException1 38 | }) { 39 | onSuccess = { _, _, _ -> 40 | fail("Must not reach here") 41 | } 42 | onError = { error, key, _ -> 43 | onErrorList += error to key 44 | } 45 | } 46 | } 47 | 48 | composeTestRule.mainClock.advanceTimeBy(3000) 49 | assertEquals(listOf>(DummyException1 to key), onErrorList) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/OnErrorRetryOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.DummyException2 8 | import com.kazakago.swr.compose.NetworkRule 9 | import com.kazakago.swr.compose.internal.SWRGlobalScope 10 | import com.kazakago.swr.compose.state.SWRState 11 | import com.kazakago.swr.compose.useSWR 12 | import kotlinx.coroutines.delay 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class OnErrorRetryOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun defaultOnErrorRetry() { 32 | val key = Random.nextInt().toString() 33 | var result: () -> String = { throw DummyException1 } 34 | val stateList = mutableListOf>() 35 | composeTestRule.setContent { 36 | SWRGlobalScope = rememberCoroutineScope() 37 | stateList += useSWR(key = key, fetcher = { 38 | delay(100) 39 | result() 40 | }) 41 | } 42 | 43 | composeTestRule.mainClock.advanceTimeBy(100) 44 | result = { throw DummyException2 } 45 | 46 | composeTestRule.mainClock.advanceTimeBy(15000) 47 | assertEquals(listOf(null, null, null, null, null), stateList.map { it.data }) 48 | assertEquals(listOf(null, null, DummyException1, DummyException1, DummyException2), stateList.map { it.error }) 49 | assertEquals(listOf(false, true, false, false, false), stateList.map { it.isLoading }) 50 | assertEquals(listOf(false, true, false, true, false), stateList.map { it.isValidating }) 51 | } 52 | 53 | @Test 54 | fun customOnErrorRetry() { 55 | val key = Random.nextInt().toString() 56 | var result: () -> String = { throw DummyException1 } 57 | val stateList = mutableListOf>() 58 | composeTestRule.setContent { 59 | SWRGlobalScope = rememberCoroutineScope() 60 | stateList += useSWR(key = key, fetcher = { 61 | delay(100) 62 | result() 63 | }) { 64 | onErrorRetry = { _, key, _, revalidate, options -> 65 | delay(100) 66 | revalidate(key, options) 67 | } 68 | } 69 | } 70 | 71 | composeTestRule.mainClock.advanceTimeBy(100) 72 | result = { throw DummyException2 } 73 | 74 | composeTestRule.mainClock.advanceTimeBy(600) 75 | assertEquals(listOf(null, null, null, null, null, null, null, null, null), stateList.map { it.data }) 76 | assertEquals(listOf(null, null, DummyException1, DummyException1, DummyException2, DummyException2, DummyException2, DummyException2, DummyException2), stateList.map { it.error }) 77 | assertEquals(listOf(false, true, false, false, false, false, false, false, false), stateList.map { it.isLoading }) 78 | assertEquals(listOf(false, true, false, true, false, true, false, true, false), stateList.map { it.isValidating }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/OnLoadingSlowOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.useSWR 9 | import kotlinx.coroutines.delay 10 | import org.junit.Rule 11 | import org.junit.runner.RunWith 12 | import kotlin.random.Random 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class OnLoadingSlowOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun onLoadingSlow() { 30 | val key = Random.nextInt().toString() 31 | val keyList = mutableListOf() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | useSWR(key = key, fetcher = { 35 | delay(5.seconds) 36 | "fetched" 37 | }) { 38 | onLoadingSlow = { key, _ -> 39 | keyList.add(key) 40 | } 41 | } 42 | } 43 | 44 | composeTestRule.mainClock.advanceTimeBy(5000) 45 | assertEquals(listOf(key), keyList) 46 | } 47 | 48 | @Test 49 | fun onLoadingSlow2() { 50 | val key = Random.nextInt().toString() 51 | val keyList = mutableListOf() 52 | composeTestRule.setContent { 53 | SWRGlobalScope = rememberCoroutineScope() 54 | useSWR(key = key, fetcher = { 55 | delay(2.seconds) 56 | "fetched" 57 | }) { 58 | onLoadingSlow = { key, _ -> 59 | keyList.add(key) 60 | } 61 | } 62 | } 63 | 64 | composeTestRule.mainClock.advanceTimeBy(5000) 65 | assertEquals(emptyList(), keyList) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/OnSuccessOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.useSWR 9 | import kotlinx.coroutines.delay 10 | import org.junit.Rule 11 | import org.junit.runner.RunWith 12 | import kotlin.random.Random 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.fail 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class OnSuccessOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun withOnSuccess() { 30 | val key = Random.nextInt().toString() 31 | val onSuccessList = mutableListOf>() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | useSWR(key = key, fetcher = { 35 | delay(100) 36 | "fetched" 37 | }) { 38 | onSuccess = { data, key, _ -> 39 | onSuccessList += data to key 40 | } 41 | onError = { _, _, _ -> 42 | fail("Must not reach here") 43 | } 44 | } 45 | } 46 | 47 | composeTestRule.mainClock.advanceTimeBy(3000) 48 | assertEquals(listOf("fetched" to key), onSuccessList) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RefreshIntervalOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.time.Duration 17 | import kotlin.time.Duration.Companion.seconds 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class RefreshIntervalOptionTest { 21 | 22 | @get:Rule 23 | val networkRule = NetworkRule() 24 | 25 | @get:Rule 26 | val composeTestRule = createComposeRule().apply { 27 | mainClock.autoAdvance = false 28 | } 29 | 30 | @Test 31 | fun noRefreshInterval() { 32 | val key = Random.nextInt().toString() 33 | val stateList = mutableListOf>() 34 | composeTestRule.setContent { 35 | SWRGlobalScope = rememberCoroutineScope() 36 | stateList += useSWR(key = key, fetcher = { 37 | delay(100) 38 | "fetched" 39 | }) { 40 | refreshInterval = Duration.ZERO 41 | } 42 | } 43 | 44 | composeTestRule.mainClock.advanceTimeBy(35000) 45 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 46 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 47 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 48 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 49 | } 50 | 51 | @Test 52 | fun withRefreshInterval() { 53 | val key = Random.nextInt().toString() 54 | val stateList = mutableListOf>() 55 | composeTestRule.setContent { 56 | SWRGlobalScope = rememberCoroutineScope() 57 | stateList += useSWR(key = key, fetcher = { 58 | delay(100) 59 | "fetched" 60 | }) { 61 | refreshInterval = 10.seconds 62 | } 63 | } 64 | 65 | composeTestRule.mainClock.advanceTimeBy(35000) 66 | assertEquals(listOf(null, null, "fetched", "fetched", "fetched", "fetched", "fetched", "fetched", "fetched"), stateList.map { it.data }) 67 | assertEquals(listOf(null, null, null, null, null, null, null, null, null), stateList.map { it.error }) 68 | assertEquals(listOf(false, true, false, false, false, false, false, false, false), stateList.map { it.isLoading }) 69 | assertEquals(listOf(false, true, false, true, false, true, false, true, false), stateList.map { it.isValidating }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RefreshWhenHiddenOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import com.kazakago.swr.compose.NetworkRule 9 | import com.kazakago.swr.compose.internal.SWRGlobalScope 10 | import com.kazakago.swr.compose.state.SWRState 11 | import com.kazakago.swr.compose.useSWR 12 | import kotlinx.coroutines.delay 13 | import org.junit.Rule 14 | import org.junit.runner.RunWith 15 | import kotlin.random.Random 16 | import kotlin.test.Test 17 | import kotlin.test.assertEquals 18 | import kotlin.time.Duration.Companion.seconds 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class RefreshWhenHiddenOptionTest { 22 | 23 | @get:Rule 24 | val networkRule = NetworkRule() 25 | 26 | @get:Rule 27 | val composeTestRule = createAndroidComposeRule().apply { 28 | mainClock.autoAdvance = false 29 | } 30 | 31 | @Test 32 | fun noRefreshWhenHidden() { 33 | val key = Random.nextInt().toString() 34 | val stateList = mutableListOf>() 35 | composeTestRule.setContent { 36 | SWRGlobalScope = rememberCoroutineScope() 37 | stateList += useSWR(key = key, fetcher = { 38 | delay(100) 39 | "fetched" 40 | }) { 41 | refreshInterval = 10.seconds 42 | refreshWhenHidden = false 43 | } 44 | } 45 | 46 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED) 47 | 48 | composeTestRule.mainClock.advanceTimeBy(35000) 49 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 50 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 51 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 52 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 53 | } 54 | 55 | @Test 56 | fun withRefreshWhenHidden() { 57 | val key = Random.nextInt().toString() 58 | val stateList = mutableListOf>() 59 | composeTestRule.setContent { 60 | SWRGlobalScope = rememberCoroutineScope() 61 | stateList += useSWR(key = key, fetcher = { 62 | delay(100) 63 | "fetched" 64 | }) { 65 | refreshInterval = 10.seconds 66 | refreshWhenHidden = true 67 | } 68 | } 69 | 70 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED) 71 | 72 | composeTestRule.mainClock.advanceTimeBy(35000) 73 | assertEquals(listOf(null, null, "fetched", "fetched", "fetched", "fetched", "fetched", "fetched", "fetched"), stateList.map { it.data }) 74 | assertEquals(listOf(null, null, null, null, null, null, null, null, null), stateList.map { it.error }) 75 | assertEquals(listOf(false, true, false, false, false, false, false, false, false), stateList.map { it.isLoading }) 76 | assertEquals(listOf(false, true, false, true, false, true, false, true, false), stateList.map { it.isValidating }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RefreshWhenOfflineOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.time.Duration.Companion.seconds 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class RefreshWhenOfflineOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun noRefreshWhenOffline() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | stateList += useSWR(key = key, fetcher = { 36 | delay(100) 37 | "fetched" 38 | }) { 39 | refreshInterval = 10.seconds 40 | refreshWhenOffline = false 41 | } 42 | } 43 | 44 | networkRule.changeNetwork(isConnected = false) 45 | 46 | composeTestRule.mainClock.advanceTimeBy(35000) 47 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 48 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 49 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 50 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 51 | } 52 | 53 | @Test 54 | fun withRefreshWhenOffline() { 55 | val key = Random.nextInt().toString() 56 | val stateList = mutableListOf>() 57 | composeTestRule.setContent { 58 | SWRGlobalScope = rememberCoroutineScope() 59 | stateList += useSWR(key = key, fetcher = { 60 | delay(100) 61 | "fetched" 62 | }) { 63 | refreshInterval = 10.seconds 64 | refreshWhenOffline = true 65 | } 66 | } 67 | 68 | networkRule.changeNetwork(isConnected = false) 69 | 70 | composeTestRule.mainClock.advanceTimeBy(35000) 71 | assertEquals(listOf(null, null, "fetched", "fetched", "fetched", "fetched", "fetched", "fetched", "fetched"), stateList.map { it.data }) 72 | assertEquals(listOf(null, null, null, null, null, null, null, null, null), stateList.map { it.error }) 73 | assertEquals(listOf(false, true, false, false, false, false, false, false, false), stateList.map { it.isLoading }) 74 | assertEquals(listOf(false, true, false, true, false, true, false, true, false), stateList.map { it.isValidating }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RevalidateIfStaleOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.cache.LocalSWRCache 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class RevalidateIfStaleOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun withRevalidateIfStale() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | LocalSWRCache.current.state(key = key).let { 36 | if (it.value == null) it.value = "cached" 37 | } 38 | stateList += useSWR(key = key, fetcher = { 39 | delay(100) 40 | "fetched" 41 | }) { 42 | revalidateIfStale = true 43 | } 44 | } 45 | 46 | composeTestRule.mainClock.advanceTimeBy(2500) 47 | assertEquals(listOf("cached", "cached", "fetched"), stateList.map { it.data }) 48 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 49 | assertEquals(listOf(false, false, false), stateList.map { it.isLoading }) 50 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 51 | } 52 | 53 | @Test 54 | fun noRevalidateIfStale() { 55 | val key = Random.nextInt().toString() 56 | val stateList = mutableListOf>() 57 | composeTestRule.setContent { 58 | SWRGlobalScope = rememberCoroutineScope() 59 | LocalSWRCache.current.state(key = key).let { 60 | if (it.value == null) it.value = "cached" 61 | } 62 | stateList += useSWR(key = key, fetcher = { 63 | delay(100) 64 | "fetched" 65 | }) { 66 | revalidateIfStale = false 67 | } 68 | } 69 | 70 | composeTestRule.mainClock.advanceTimeBy(2500) 71 | assertEquals(listOf("cached"), stateList.map { it.data }) 72 | assertEquals(listOf(null), stateList.map { it.error }) 73 | assertEquals(listOf(false), stateList.map { it.isLoading }) 74 | assertEquals(listOf(false), stateList.map { it.isValidating }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RevalidateOnFocusOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class RevalidateOnFocusOptionTest { 20 | 21 | @get:Rule 22 | val composeTestRule = createAndroidComposeRule().apply { 23 | mainClock.autoAdvance = false 24 | } 25 | 26 | @Test 27 | fun withRevalidateOnFocus() { 28 | val key = Random.nextInt().toString() 29 | val stateList = mutableListOf>() 30 | composeTestRule.setContent { 31 | SWRGlobalScope = rememberCoroutineScope() 32 | stateList += useSWR(key = key, fetcher = { 33 | delay(100) 34 | "fetched" 35 | }) { 36 | revalidateOnFocus = true 37 | } 38 | } 39 | 40 | composeTestRule.mainClock.advanceTimeBy(2500) 41 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED) 42 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) 43 | 44 | composeTestRule.mainClock.advanceTimeBy(2500) 45 | assertEquals(listOf(null, null, "fetched", "fetched", "fetched"), stateList.map { it.data }) 46 | assertEquals(listOf(null, null, null, null, null), stateList.map { it.error }) 47 | assertEquals(listOf(false, true, false, false, false), stateList.map { it.isLoading }) 48 | assertEquals(listOf(false, true, false, true, false), stateList.map { it.isValidating }) 49 | } 50 | 51 | @Test 52 | fun noRevalidateOnFocus() { 53 | val key = Random.nextInt().toString() 54 | val stateList = mutableListOf>() 55 | composeTestRule.setContent { 56 | SWRGlobalScope = rememberCoroutineScope() 57 | stateList += useSWR(key = key, fetcher = { 58 | delay(100) 59 | "fetched" 60 | }) { 61 | revalidateOnFocus = false 62 | } 63 | } 64 | 65 | composeTestRule.mainClock.advanceTimeBy(2500) 66 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED) 67 | composeTestRule.mainClock.advanceTimeBy(5000) 68 | composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED) 69 | 70 | composeTestRule.mainClock.advanceTimeBy(2500) 71 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 72 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 73 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 74 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RevalidateOnMountOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class RevalidateOnMountOptionTest { 19 | 20 | @get:Rule 21 | val composeTestRule = createAndroidComposeRule().apply { 22 | mainClock.autoAdvance = false 23 | } 24 | 25 | @Test 26 | fun withRevalidateOnMount() { 27 | val key = Random.nextInt().toString() 28 | val stateList = mutableListOf>() 29 | composeTestRule.setContent { 30 | SWRGlobalScope = rememberCoroutineScope() 31 | stateList += useSWR(key = key, fetcher = { 32 | delay(100) 33 | "fetched" 34 | }) { 35 | revalidateOnMount = true 36 | } 37 | } 38 | 39 | composeTestRule.mainClock.advanceTimeBy(2500) 40 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 41 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 42 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 43 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 44 | } 45 | 46 | @Test 47 | fun noRevalidateOnMount() { 48 | val key = Random.nextInt().toString() 49 | val stateList = mutableListOf>() 50 | composeTestRule.setContent { 51 | SWRGlobalScope = rememberCoroutineScope() 52 | stateList += useSWR(key = key, fetcher = { 53 | delay(100) 54 | "fetched" 55 | }) { 56 | revalidateOnMount = false 57 | } 58 | } 59 | 60 | composeTestRule.mainClock.advanceTimeBy(2500) 61 | assertEquals(listOf(null), stateList.map { it.data }) 62 | assertEquals(listOf(null), stateList.map { it.error }) 63 | assertEquals(listOf(false), stateList.map { it.isLoading }) 64 | assertEquals(listOf(false), stateList.map { it.isValidating }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/RevalidateOnReconnectOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.NetworkRule 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.state.SWRState 9 | import com.kazakago.swr.compose.useSWR 10 | import kotlinx.coroutines.delay 11 | import org.junit.Rule 12 | import org.junit.runner.RunWith 13 | import kotlin.random.Random 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class RevalidateOnReconnectOptionTest { 19 | 20 | @get:Rule 21 | val networkRule = NetworkRule() 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule().apply { 25 | mainClock.autoAdvance = false 26 | } 27 | 28 | @Test 29 | fun withRevalidateOnReconnect() { 30 | val key = Random.nextInt().toString() 31 | val stateList = mutableListOf>() 32 | composeTestRule.setContent { 33 | SWRGlobalScope = rememberCoroutineScope() 34 | stateList += useSWR(key = key, fetcher = { 35 | delay(100) 36 | "fetched" 37 | }) { 38 | revalidateOnReconnect = true 39 | } 40 | } 41 | 42 | networkRule.changeNetwork(isConnected = true) 43 | 44 | composeTestRule.mainClock.advanceTimeBy(2500) 45 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 46 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 47 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 48 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 49 | 50 | networkRule.changeNetwork(isConnected = true) 51 | 52 | composeTestRule.mainClock.advanceTimeBy(2500) 53 | assertEquals(listOf(null, null, "fetched", "fetched", "fetched"), stateList.map { it.data }) 54 | assertEquals(listOf(null, null, null, null, null), stateList.map { it.error }) 55 | assertEquals(listOf(false, true, false, false, false), stateList.map { it.isLoading }) 56 | assertEquals(listOf(false, true, false, true, false), stateList.map { it.isValidating }) 57 | } 58 | 59 | @Test 60 | fun noRevalidateOnReconnect() { 61 | val key = Random.nextInt().toString() 62 | val stateList = mutableListOf>() 63 | composeTestRule.setContent { 64 | SWRGlobalScope = rememberCoroutineScope() 65 | stateList += useSWR(key = key, fetcher = { 66 | delay(100) 67 | "fetched" 68 | }) { 69 | revalidateOnReconnect = false 70 | } 71 | } 72 | 73 | composeTestRule.mainClock.advanceTimeBy(2500) 74 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 75 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 76 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 77 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 78 | 79 | networkRule.changeNetwork(isConnected = true) 80 | 81 | composeTestRule.mainClock.advanceTimeBy(2500) 82 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 83 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 84 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 85 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/ShouldRetryOnErrorOptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class ShouldRetryOnErrorOptionTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun withShouldRetryOnError() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | stateList += useSWR(key = key, fetcher = { 36 | delay(100) 37 | throw DummyException1 38 | }) { 39 | shouldRetryOnError = true 40 | } 41 | } 42 | 43 | composeTestRule.mainClock.advanceTimeBy(15000) 44 | assertEquals(listOf(null, null, null, null, null), stateList.map { it.data }) 45 | assertEquals(listOf(null, null, DummyException1, DummyException1, DummyException1), stateList.map { it.error }) 46 | assertEquals(listOf(false, true, false, false, false), stateList.map { it.isLoading }) 47 | assertEquals(listOf(false, true, false, true, false), stateList.map { it.isValidating }) 48 | } 49 | 50 | @Test 51 | fun noShouldRetryOnError() { 52 | val key = Random.nextInt().toString() 53 | val stateList = mutableListOf>() 54 | composeTestRule.setContent { 55 | SWRGlobalScope = rememberCoroutineScope() 56 | stateList += useSWR(key = key, fetcher = { 57 | delay(100) 58 | throw DummyException1 59 | }) { 60 | shouldRetryOnError = false 61 | } 62 | } 63 | 64 | composeTestRule.mainClock.advanceTimeBy(15000) 65 | assertEquals(listOf(null, null, null), stateList.map { it.data }) 66 | assertEquals(listOf(null, null, DummyException1), stateList.map { it.error }) 67 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 68 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /swr/src/androidUnitTest/kotlin/com/kazakago/swr/compose/validation/UseSWRTest.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validation 2 | 3 | import androidx.compose.runtime.rememberCoroutineScope 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.kazakago.swr.compose.DummyException1 7 | import com.kazakago.swr.compose.NetworkRule 8 | import com.kazakago.swr.compose.internal.SWRGlobalScope 9 | import com.kazakago.swr.compose.state.SWRState 10 | import com.kazakago.swr.compose.useSWR 11 | import kotlinx.coroutines.delay 12 | import org.junit.Rule 13 | import org.junit.runner.RunWith 14 | import kotlin.random.Random 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class UseSWRTest { 20 | 21 | @get:Rule 22 | val networkRule = NetworkRule() 23 | 24 | @get:Rule 25 | val composeTestRule = createComposeRule().apply { 26 | mainClock.autoAdvance = false 27 | } 28 | 29 | @Test 30 | fun validate() { 31 | val key = Random.nextInt().toString() 32 | val stateList = mutableListOf>() 33 | composeTestRule.setContent { 34 | SWRGlobalScope = rememberCoroutineScope() 35 | stateList += useSWR(key = key, fetcher = { 36 | delay(100) 37 | "fetched" 38 | }) 39 | } 40 | 41 | composeTestRule.mainClock.advanceTimeBy(2500) 42 | assertEquals(listOf(null, null, "fetched"), stateList.map { it.data }) 43 | assertEquals(listOf(null, null, null), stateList.map { it.error }) 44 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 45 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 46 | } 47 | 48 | @Test 49 | fun validateFailed() { 50 | val key = Random.nextInt().toString() 51 | val stateList = mutableListOf>() 52 | composeTestRule.setContent { 53 | SWRGlobalScope = rememberCoroutineScope() 54 | stateList += useSWR(key = key, fetcher = { 55 | delay(100) 56 | throw DummyException1 57 | }) 58 | } 59 | 60 | composeTestRule.mainClock.advanceTimeBy(2500) 61 | assertEquals(listOf(null, null, null), stateList.map { it.data }) 62 | assertEquals(listOf(null, null, DummyException1), stateList.map { it.error }) 63 | assertEquals(listOf(false, true, false), stateList.map { it.isLoading }) 64 | assertEquals(listOf(false, true, false), stateList.map { it.isValidating }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/UseSWR.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.kazakago.swr.compose.config.LocalSWRConfig 5 | import com.kazakago.swr.compose.config.SWRConfig 6 | import com.kazakago.swr.compose.config.SWRConfigImpl 7 | import com.kazakago.swr.compose.internal.useSWRInternal 8 | import com.kazakago.swr.compose.state.SWRState 9 | import kotlinx.coroutines.CoroutineScope 10 | 11 | @Composable 12 | public fun useSWR( 13 | key: () -> KEY?, 14 | fetcher: (suspend (key: KEY) -> DATA)? = null, 15 | scope: CoroutineScope? = null, 16 | options: SWRConfig.() -> Unit = {}, 17 | ): SWRState { 18 | return useSWR(runCatching(key).getOrNull(), fetcher, scope, options) 19 | } 20 | 21 | @Composable 22 | public fun useSWR( 23 | key: KEY?, 24 | fetcher: (suspend (key: KEY) -> DATA)? = null, 25 | scope: CoroutineScope? = null, 26 | options: SWRConfig.() -> Unit = {}, 27 | ): SWRState { 28 | val globalConfig = LocalSWRConfig.current 29 | val config = SWRConfigImpl.from(globalConfig).apply { options() } 30 | return useSWRInternal(key, fetcher, scope, config) 31 | } 32 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/UseSWRConfig.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.kazakago.swr.compose.cache.LocalSWRCache 6 | import com.kazakago.swr.compose.cache.LocalSWRSystemCache 7 | import com.kazakago.swr.compose.config.LocalSWRConfig 8 | import com.kazakago.swr.compose.config.SWRConfigImpl 9 | import com.kazakago.swr.compose.mutate.SWRMutate 10 | import com.kazakago.swr.compose.mutate.SWRMutateImpl 11 | import com.kazakago.swr.compose.state.SWRConfigState 12 | import com.kazakago.swr.compose.state.SWRConfigStateImpl 13 | import com.kazakago.swr.compose.validate.SWRValidate 14 | import com.kazakago.swr.compose.validate.SWRValidateImpl 15 | 16 | @Composable 17 | public fun useSWRConfig(): SWRConfigState { 18 | val cache = LocalSWRCache.current 19 | val systemCache = LocalSWRSystemCache.current 20 | val globalConfig = LocalSWRConfig.current 21 | val config = SWRConfigImpl.from(globalConfig) 22 | val validate: SWRValidate = remember(config, cache, systemCache) { 23 | SWRValidateImpl(config, cache, systemCache, null) 24 | } 25 | val mutate: SWRMutate = remember(cache, systemCache, validate) { 26 | SWRMutateImpl(null, cache, systemCache, validate) 27 | } 28 | return SWRConfigStateImpl( 29 | mutate = mutate, 30 | config = config, 31 | cache = cache, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/UseSWRImmutable.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.kazakago.swr.compose.config.LocalSWRConfig 5 | import com.kazakago.swr.compose.config.SWRConfig 6 | import com.kazakago.swr.compose.config.SWRConfigImpl 7 | import com.kazakago.swr.compose.internal.useSWRInternal 8 | import com.kazakago.swr.compose.state.SWRState 9 | import kotlinx.coroutines.CoroutineScope 10 | 11 | @Composable 12 | public fun useSWRImmutable( 13 | key: () -> KEY?, 14 | fetcher: (suspend (key: KEY) -> DATA)? = null, 15 | scope: CoroutineScope? = null, 16 | options: SWRConfig.() -> Unit = {}, 17 | ): SWRState { 18 | return useSWRImmutable(runCatching(key).getOrNull(), fetcher, scope, options) 19 | } 20 | 21 | @Composable 22 | public fun useSWRImmutable( 23 | key: KEY?, 24 | fetcher: (suspend (key: KEY) -> DATA)? = null, 25 | scope: CoroutineScope? = null, 26 | options: SWRConfig.() -> Unit = {}, 27 | ): SWRState { 28 | val globalConfig = LocalSWRConfig.current 29 | val config = SWRConfigImpl.from(globalConfig).apply { 30 | options() 31 | revalidateIfStale = false 32 | revalidateOnFocus = false 33 | revalidateOnReconnect = false 34 | } 35 | return useSWRInternal(key, fetcher, scope, config) 36 | } 37 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/UseSWRMutation.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import com.kazakago.swr.compose.cache.LocalSWRCache 8 | import com.kazakago.swr.compose.cache.LocalSWRSystemCache 9 | import com.kazakago.swr.compose.config.LocalSWRConfig 10 | import com.kazakago.swr.compose.config.SWRConfigImpl 11 | import com.kazakago.swr.compose.config.SWRTriggerConfig 12 | import com.kazakago.swr.compose.state.SWRMutationState 13 | import com.kazakago.swr.compose.state.SWRMutationStateImpl 14 | import com.kazakago.swr.compose.trigger.SWRReset 15 | import com.kazakago.swr.compose.trigger.SWRResetImpl 16 | import com.kazakago.swr.compose.trigger.SWRTrigger 17 | import com.kazakago.swr.compose.trigger.SWRTriggerImpl 18 | import com.kazakago.swr.compose.validate.SWRValidate 19 | import com.kazakago.swr.compose.validate.SWRValidateImpl 20 | 21 | @Composable 22 | public fun useSWRMutation( 23 | key: () -> KEY?, 24 | fetcher: (suspend (key: KEY, arg: ARG) -> DATA), 25 | options: SWRTriggerConfig.() -> Unit = {}, 26 | ): SWRMutationState { 27 | return useSWRMutation(runCatching(key).getOrNull(), fetcher, options) 28 | } 29 | 30 | @Composable 31 | public fun useSWRMutation( 32 | key: KEY?, 33 | fetcher: (suspend (key: KEY, arg: ARG) -> DATA), 34 | options: SWRTriggerConfig.() -> Unit = {}, 35 | ): SWRMutationState { 36 | val cache = LocalSWRCache.current 37 | val systemCache = LocalSWRSystemCache.current 38 | val globalConfig = LocalSWRConfig.current 39 | val config = SWRConfigImpl.from(globalConfig) 40 | val mutateConfig = SWRTriggerConfig().apply { options() } 41 | val data: MutableState = remember(key) { mutableStateOf(null) } 42 | val error: MutableState = remember(key) { mutableStateOf(null) } 43 | val isMutating: MutableState = remember(key) { mutableStateOf(false) } 44 | val validate: SWRValidate = remember(mutateConfig, cache, systemCache) { 45 | SWRValidateImpl(config, cache, systemCache, null) 46 | } 47 | val trigger: SWRTrigger = remember(key, mutateConfig, cache, validate) { 48 | SWRTriggerImpl(key, mutateConfig, cache, data, error, isMutating, fetcher, validate) 49 | } 50 | val reset: SWRReset = remember(key) { 51 | SWRResetImpl(data, error, isMutating) 52 | } 53 | return remember(key, data.value, error.value, trigger, reset, isMutating.value) { 54 | SWRMutationStateImpl(data = data.value, error = error.value, trigger = trigger, reset = reset, isMutating = isMutating.value) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/UseSWRPreload.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.kazakago.swr.compose.cache.LocalSWRCache 6 | import com.kazakago.swr.compose.cache.LocalSWRSystemCache 7 | import com.kazakago.swr.compose.config.LocalSWRConfig 8 | import com.kazakago.swr.compose.config.SWRConfigImpl 9 | import com.kazakago.swr.compose.preload.SWRPreload 10 | import com.kazakago.swr.compose.preload.SWRPreloadImpl 11 | import com.kazakago.swr.compose.validate.SWRValidate 12 | import com.kazakago.swr.compose.validate.SWRValidateImpl 13 | 14 | @Composable 15 | public fun useSWRPreload( 16 | key: () -> KEY?, 17 | fetcher: (suspend (key: KEY) -> DATA)? = null, 18 | ): SWRPreload { 19 | return useSWRPreload(runCatching(key).getOrNull(), fetcher) 20 | } 21 | 22 | @Composable 23 | public fun useSWRPreload( 24 | key: KEY?, 25 | fetcher: (suspend (key: KEY) -> DATA)? = null, 26 | ): SWRPreload { 27 | val globalConfig = LocalSWRConfig.current 28 | val config = SWRConfigImpl.from(globalConfig) 29 | val cache = LocalSWRCache.current 30 | val systemCache = LocalSWRSystemCache.current 31 | if (key != null && fetcher != null) systemCache.setFetcher(key, fetcher) 32 | val validate: SWRValidate = remember(config, cache, systemCache) { 33 | SWRValidateImpl(config, cache, systemCache, fetcher) 34 | } 35 | return SWRPreloadImpl(key, validate) 36 | } 37 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/cache/SWRCache.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.cache 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.ProvidableCompositionLocal 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.mutableStateOf 7 | 8 | public val LocalSWRCache: ProvidableCompositionLocal = compositionLocalOf { 9 | SWRCacheImpl() 10 | } 11 | 12 | public interface SWRCache { 13 | public fun state(key: KEY): MutableState 14 | public fun clear() 15 | } 16 | 17 | @Suppress("UNCHECKED_CAST") 18 | internal class SWRCacheImpl : SWRCache { 19 | 20 | private val cacheMap: MutableMap> = mutableMapOf() 21 | 22 | override fun state(key: KEY): MutableState { 23 | return (cacheMap as MutableMap>).getOrPut(key) { mutableStateOf(null) } 24 | } 25 | 26 | override fun clear() { 27 | cacheMap.clear() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/cache/SWRSystemCache.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.cache 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.ProvidableCompositionLocal 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.mutableStateOf 7 | import kotlinx.coroutines.Job 8 | 9 | public val LocalSWRSystemCache: ProvidableCompositionLocal = compositionLocalOf { 10 | SWRSystemCacheImpl() 11 | } 12 | 13 | public interface SWRSystemCache { 14 | public fun isValidatingState(key: KEY): MutableState 15 | public fun errorState(key: KEY): MutableState 16 | public fun getFetcher(key: KEY): (suspend (key: KEY) -> DATA)? 17 | public fun setFetcher(key: KEY, fetcher: (suspend (key: KEY) -> DATA)) 18 | public fun getValidatedTimerJob(key: KEY): Job? 19 | public fun setValidatedTimerJob(key: KEY, job: Job) 20 | public fun getFocusedTimerJob(key: KEY): Job? 21 | public fun setFocusedTimerJob(key: KEY, job: Job) 22 | public fun getRetryingJobSet(key: KEY): Set 23 | public fun setRetryingJobSet(key: KEY, jobs: Set) 24 | public fun clear() 25 | } 26 | 27 | @Suppress("UNCHECKED_CAST") 28 | internal class SWRSystemCacheImpl : SWRSystemCache { 29 | 30 | private val isValidatingStateMap: MutableMap> = mutableMapOf() 31 | private val errorStateMap: MutableMap> = mutableMapOf() 32 | private val fetcherMap: MutableMap Any)> = mutableMapOf() 33 | private val validatedTimeMap: MutableMap = mutableMapOf() 34 | private val focusedTimeMap: MutableMap = mutableMapOf() 35 | private val isRetryingJobSetMap: MutableMap> = mutableMapOf() 36 | 37 | override fun isValidatingState(key: KEY): MutableState { 38 | return (isValidatingStateMap as MutableMap>).getOrPut(key) { 39 | mutableStateOf(false) 40 | } 41 | } 42 | 43 | override fun errorState(key: KEY): MutableState { 44 | return (errorStateMap as MutableMap>).getOrPut(key) { 45 | mutableStateOf(null) 46 | } 47 | } 48 | 49 | override fun getFetcher(key: KEY): (suspend (key: KEY) -> DATA)? { 50 | return (fetcherMap as MutableMap DATA)>)[key] 51 | } 52 | 53 | override fun setFetcher(key: KEY, fetcher: (suspend (key: KEY) -> DATA)) { 54 | (fetcherMap as MutableMap DATA)>)[key] = fetcher 55 | } 56 | 57 | override fun getValidatedTimerJob(key: KEY): Job? { 58 | return (validatedTimeMap as MutableMap)[key] 59 | } 60 | 61 | override fun setValidatedTimerJob(key: KEY, job: Job) { 62 | (validatedTimeMap as MutableMap)[key] = job 63 | } 64 | 65 | override fun getFocusedTimerJob(key: KEY): Job? { 66 | return (focusedTimeMap as MutableMap)[key] 67 | } 68 | 69 | override fun setFocusedTimerJob(key: KEY, job: Job) { 70 | (focusedTimeMap as MutableMap)[key] = job 71 | } 72 | 73 | override fun getRetryingJobSet(key: KEY): Set { 74 | return (isRetryingJobSetMap as MutableMap>)[key] ?: emptySet() 75 | } 76 | 77 | override fun setRetryingJobSet(key: KEY, jobs: Set) { 78 | (isRetryingJobSetMap as MutableMap>)[key] = jobs 79 | } 80 | 81 | override fun clear() { 82 | isValidatingStateMap.clear() 83 | errorStateMap.clear() 84 | fetcherMap.clear() 85 | validatedTimeMap.clear() 86 | focusedTimeMap.clear() 87 | isRetryingJobSetMap.clear() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/config/SWRInfiniteConfig.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.config 2 | 3 | import com.kazakago.swr.compose.validate.SWRValidate 4 | import com.kazakago.swr.compose.validate.SWRValidateOptions 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlin.time.Duration 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | public data class SWRInfiniteConfig( 10 | override var fetcher: (suspend (key: KEY) -> DATA)?, 11 | override var revalidateIfStale: Boolean, 12 | override var revalidateOnMount: Boolean?, 13 | override var revalidateOnFocus: Boolean, 14 | override var revalidateOnReconnect: Boolean, 15 | override var refreshInterval: Duration, 16 | override var refreshWhenHidden: Boolean, 17 | override var refreshWhenOffline: Boolean, 18 | override var shouldRetryOnError: Boolean, 19 | override var dedupingInterval: Duration, 20 | override var focusThrottleInterval: Duration, 21 | override var loadingTimeout: Duration, 22 | override var errorRetryInterval: Duration, 23 | override var errorRetryCount: Int?, 24 | override var fallback: Map, 25 | override var fallbackData: DATA? = null, 26 | override var keepPreviousData: Boolean, 27 | override var onLoadingSlow: ((key: KEY, config: SWRConfig) -> Unit)?, 28 | override var onSuccess: ((data: DATA, key: KEY, config: SWRConfig) -> Unit)?, 29 | override var onError: ((error: Throwable, key: KEY, config: SWRConfig) -> Unit)?, 30 | override var onErrorRetry: suspend (error: Throwable, key: KEY, config: SWRConfig, revalidate: SWRValidate, options: SWRValidateOptions) -> Unit, 31 | override var isPaused: () -> Boolean, 32 | public var initialSize: Int = 1, 33 | public var revalidateAll: Boolean = false, 34 | public var revalidateFirstPage: Boolean = true, 35 | public var persistSize: Boolean = false, 36 | override var scope: CoroutineScope?, 37 | ) : SWRConfig { 38 | 39 | internal companion object { 40 | internal fun from(globalConfig: SWRGlobalConfig): SWRInfiniteConfig { 41 | val config = globalConfig.copy() 42 | return SWRInfiniteConfig( 43 | fetcher = config.fetcher as (suspend (key: KEY) -> DATA)?, 44 | revalidateIfStale = config.revalidateIfStale, 45 | revalidateOnMount = config.revalidateOnMount, 46 | revalidateOnFocus = config.revalidateOnFocus, 47 | revalidateOnReconnect = config.revalidateOnReconnect, 48 | refreshInterval = config.refreshInterval, 49 | refreshWhenHidden = config.refreshWhenHidden, 50 | refreshWhenOffline = config.refreshWhenOffline, 51 | shouldRetryOnError = config.shouldRetryOnError, 52 | dedupingInterval = config.dedupingInterval, 53 | focusThrottleInterval = config.focusThrottleInterval, 54 | loadingTimeout = config.loadingTimeout, 55 | errorRetryInterval = config.errorRetryInterval, 56 | errorRetryCount = config.errorRetryCount, 57 | fallback = config.fallback as Map, 58 | keepPreviousData = config.keepPreviousData, 59 | onLoadingSlow = config.onLoadingSlow as ((key: KEY, config: SWRConfig) -> Unit)?, 60 | onSuccess = config.onSuccess as ((data: DATA, key: KEY, config: SWRConfig) -> Unit)?, 61 | onError = config.onError as ((error: Throwable, key: KEY, config: SWRConfig) -> Unit)?, 62 | onErrorRetry = config.onErrorRetry as suspend (error: Throwable, key: KEY, config: SWRConfig, revalidate: SWRValidate, options: SWRValidateOptions) -> Unit, 63 | isPaused = config.isPaused, 64 | scope = config.scope, 65 | ) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/config/SWRMutateConfig.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.config 2 | 3 | public data class SWRMutateConfig( 4 | var optimisticData: DATA? = null, 5 | var revalidate: Boolean = true, 6 | var populateCache: Boolean = true, 7 | var rollbackOnError: Boolean = true, 8 | var throwOnError: Boolean = true, 9 | ) 10 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/config/SWRTriggerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.config 2 | 3 | public data class SWRTriggerConfig( 4 | var optimisticData: DATA? = null, 5 | var revalidate: Boolean = true, 6 | var populateCache: Boolean = false, 7 | var rollbackOnError: Boolean = true, 8 | var throwOnError: Boolean = true, 9 | var onSuccess: ((data: DATA, key: KEY, config: SWRTriggerConfig) -> Unit)? = null, 10 | var onError: ((error: Throwable, key: KEY, config: SWRTriggerConfig) -> Unit)? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/internal/DataHolder.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.internal 2 | 3 | internal data class DataHolder( 4 | var data: DATA? = null, 5 | ) 6 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/internal/GlobalKonnection.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.internal 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import dev.tmapps.konnection.Konnection 5 | 6 | @VisibleForTesting 7 | internal var GlobalKonnection: Konnection 8 | set(value) { 9 | internalKonnection = value 10 | } 11 | get() { 12 | var localKonnection = internalKonnection 13 | if (localKonnection == null) { 14 | localKonnection = Konnection.instance 15 | internalKonnection = localKonnection 16 | } 17 | return localKonnection 18 | } 19 | private var internalKonnection: Konnection? = null 20 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/internal/SWRGlobalScope.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.internal 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlin.coroutines.EmptyCoroutineContext 5 | 6 | internal var SWRGlobalScope: CoroutineScope = CoroutineScope(EmptyCoroutineContext) 7 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/internal/UseSWRInternal.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.internal 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.rememberCoroutineScope 8 | import com.kazakago.swr.compose.cache.LocalSWRCache 9 | import com.kazakago.swr.compose.cache.LocalSWRSystemCache 10 | import com.kazakago.swr.compose.config.SWRConfig 11 | import com.kazakago.swr.compose.mutate.SWRMutate 12 | import com.kazakago.swr.compose.mutate.SWRMutateImpl 13 | import com.kazakago.swr.compose.state.SWRState 14 | import com.kazakago.swr.compose.state.SWRStateImpl 15 | import com.kazakago.swr.compose.validate.SWRValidate 16 | import com.kazakago.swr.compose.validate.SWRValidateImpl 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.launch 19 | 20 | @Composable 21 | internal fun useSWRInternal( 22 | key: KEY?, 23 | fetcher: (suspend (key: KEY) -> DATA)?, 24 | scope: CoroutineScope?, 25 | config: SWRConfig, 26 | ): SWRState { 27 | val cache = LocalSWRCache.current 28 | val systemCache = LocalSWRSystemCache.current 29 | val currentScope = scope ?: config.scope ?: rememberCoroutineScope() 30 | if (key != null && fetcher != null) systemCache.setFetcher(key, fetcher) 31 | val validate: SWRValidate = remember(config, cache, systemCache) { 32 | SWRValidateImpl(config, cache, systemCache, fetcher) 33 | } 34 | val mutate: SWRMutate = remember(key, cache, systemCache, validate) { 35 | SWRMutateImpl(key, cache, systemCache, validate) 36 | } 37 | 38 | val loadedDataHolder: DataHolder = remember { DataHolder() } 39 | val loadedData: DATA? by remember(key) { cache.state(key) } 40 | val fallbackData: DATA? = remember(key) { config.fallbackData ?: config.fallback[key] ?: (if (config.keepPreviousData) loadedDataHolder.data else null) } 41 | val error: MutableState = remember(key) { systemCache.errorState(key) } 42 | val isValidating: MutableState = remember(key) { systemCache.isValidatingState(key) } 43 | loadedDataHolder.data = loadedData 44 | 45 | RevalidateOnMount(key, config, isValidating) { currentScope.launch { validate(key) } } 46 | RevalidateOnFocus(key, config) { currentScope.launch { validate(key) } } 47 | RevalidateOnReconnect(key, config) { currentScope.launch { validate(key) } } 48 | RefreshInterval(key, config) { currentScope.launch { validate(key) } } 49 | 50 | return remember(key, loadedData, fallbackData, error.value, isValidating.value, mutate) { 51 | val data = loadedData ?: fallbackData 52 | val isLoading: Boolean = (loadedData == null) && (error.value == null) && isValidating.value 53 | SWRStateImpl(data = data, error = error.value, isLoading = isLoading, isValidating = isValidating.value, mutate = mutate) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/mutate/SWRMutate.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.mutate 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.cache.SWRCache 5 | import com.kazakago.swr.compose.cache.SWRSystemCache 6 | import com.kazakago.swr.compose.config.SWRMutateConfig 7 | import com.kazakago.swr.compose.validate.SWRValidate 8 | 9 | public interface SWRMutate { 10 | public suspend operator fun invoke( 11 | key: KEY? = null, 12 | data: (suspend () -> DATA)? = null, 13 | options: SWRMutateConfig.() -> Unit = {}, 14 | ) 15 | 16 | public companion object { 17 | public fun empty(): SWRMutate = object : SWRMutate { 18 | override suspend fun invoke(key: KEY?, data: (suspend () -> DATA)?, options: SWRMutateConfig.() -> Unit) {} 19 | } 20 | } 21 | } 22 | 23 | @Immutable 24 | internal data class SWRMutateImpl( 25 | private val key: KEY?, 26 | private val cache: SWRCache, 27 | private val systemCache: SWRSystemCache, 28 | private val validate: SWRValidate, 29 | ) : SWRMutate { 30 | 31 | override suspend operator fun invoke( 32 | key: KEY?, 33 | data: (suspend () -> DATA)?, 34 | options: SWRMutateConfig.() -> Unit, 35 | ) { 36 | val currentKey = key ?: this.key ?: return 37 | val config = SWRMutateConfig().apply { options() } 38 | val oldData: DATA? = cache.state(currentKey).value 39 | if (config.optimisticData != null) { 40 | cache.state(currentKey).value = config.optimisticData 41 | systemCache.errorState(currentKey).value = null 42 | } 43 | runCatching { 44 | if (data != null) data() else null 45 | }.onSuccess { newData -> 46 | if (config.populateCache && newData != null) { 47 | cache.state(currentKey).value = newData 48 | systemCache.errorState(currentKey).value = null 49 | } 50 | if (config.revalidate) { 51 | validate(currentKey) 52 | } 53 | }.onFailure { throwable -> 54 | if (config.rollbackOnError) { 55 | cache.state(currentKey).value = oldData 56 | } 57 | if (config.throwOnError) { 58 | throw throwable 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/preload/SWRPreload.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.preload 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.validate.SWRValidate 5 | 6 | public interface SWRPreload { 7 | public suspend operator fun invoke() 8 | } 9 | 10 | @Immutable 11 | internal data class SWRPreloadImpl( 12 | private val key: KEY?, 13 | private val validate: SWRValidate, 14 | ) : SWRPreload { 15 | 16 | override suspend operator fun invoke() { 17 | validate(key) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/retry/SWRRetryDefault.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.retry 2 | 3 | import com.kazakago.swr.compose.config.SWRConfig 4 | import com.kazakago.swr.compose.validate.SWRValidate 5 | import com.kazakago.swr.compose.validate.SWRValidateOptions 6 | import kotlinx.coroutines.delay 7 | import kotlin.math.floor 8 | import kotlin.random.Random 9 | 10 | public val OnErrorRetryDefault: suspend (error: Throwable, key: Any, config: SWRConfig<*, *>, revalidate: SWRValidate, options: SWRValidateOptions) -> Unit = { _, key, config, revalidate, options -> 11 | if (!options.dedupe && config.errorRetryCount.let { it == null || options.retryCount <= it }) { 12 | val exponentialBackoff = floor((Random.nextDouble() + 0.5) * 1.shl(options.retryCount)).toLong() * config.errorRetryInterval.inWholeMilliseconds 13 | delay(exponentialBackoff) 14 | revalidate(key, options) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/state/SWRConfigState.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.state 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.mutableStateOf 5 | import com.kazakago.swr.compose.cache.SWRCache 6 | import com.kazakago.swr.compose.config.SWRConfig 7 | import com.kazakago.swr.compose.config.SWRConfigImpl 8 | import com.kazakago.swr.compose.config.SWRGlobalConfig 9 | import com.kazakago.swr.compose.mutate.SWRMutate 10 | 11 | public interface SWRConfigState { 12 | public val mutate: SWRMutate 13 | public val config: SWRConfig 14 | public val cache: SWRCache 15 | 16 | public operator fun component1(): SWRMutate = mutate 17 | public operator fun component2(): SWRConfig = config 18 | public operator fun component3(): SWRCache = cache 19 | 20 | public companion object { 21 | public fun empty( 22 | mutate: SWRMutate = SWRMutate.empty(), 23 | config: SWRConfig = SWRConfigImpl.from(SWRGlobalConfig()), 24 | cache: SWRCache = object : SWRCache { 25 | override fun state(key: KEY) = mutableStateOf(null) 26 | override fun clear() {} 27 | } 28 | ): SWRConfigState = SWRConfigStateImpl(mutate, config, cache) 29 | } 30 | 31 | } 32 | 33 | @Immutable 34 | internal data class SWRConfigStateImpl( 35 | override val mutate: SWRMutate, 36 | override val config: SWRConfig, 37 | override val cache: SWRCache, 38 | ) : SWRConfigState 39 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/state/SWRInfiniteState.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.state 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.mutate.SWRMutate 5 | 6 | public interface SWRInfiniteState { 7 | public val data: List? 8 | public val error: Throwable? 9 | public val isLoading: Boolean 10 | public val isValidating: Boolean 11 | public val mutate: SWRMutate> 12 | public val size: Int 13 | public val setSize: ((size: Int) -> Unit) 14 | 15 | public operator fun component1(): List? = data 16 | public operator fun component2(): Throwable? = error 17 | public operator fun component3(): Boolean = isLoading 18 | public operator fun component4(): Boolean = isValidating 19 | public operator fun component5(): SWRMutate> = mutate 20 | public operator fun component6(): Int = size 21 | public operator fun component7(): ((size: Int) -> Unit) = setSize 22 | 23 | public companion object { 24 | public fun empty( 25 | data: List = emptyList(), 26 | error: Throwable? = null, 27 | isLoading: Boolean = false, 28 | isValidating: Boolean = false, 29 | mutate: SWRMutate> = SWRMutate.empty(), 30 | size: Int = 1, 31 | setSize: (size: Int) -> Unit = {}, 32 | ): SWRInfiniteState = SWRInfiniteStateImpl(data = data, error = error, isLoading = isLoading, isValidating = isValidating, mutate = mutate, size = size, setSize = setSize) 33 | } 34 | } 35 | 36 | @Immutable 37 | internal data class SWRInfiniteStateImpl( 38 | override val data: List?, 39 | override val error: Throwable?, 40 | override val isLoading: Boolean, 41 | override val isValidating: Boolean, 42 | override val mutate: SWRMutate>, 43 | override val size: Int, 44 | override val setSize: (size: Int) -> Unit, 45 | ) : SWRInfiniteState 46 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/state/SWRMutationState.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.state 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.trigger.SWRReset 5 | import com.kazakago.swr.compose.trigger.SWRTrigger 6 | 7 | public interface SWRMutationState { 8 | public val data: DATA? 9 | public val error: Throwable? 10 | public val trigger: SWRTrigger 11 | public val reset: SWRReset 12 | public val isMutating: Boolean 13 | 14 | public operator fun component1(): DATA? = data 15 | public operator fun component2(): Throwable? = error 16 | public operator fun component3(): SWRTrigger = trigger 17 | public operator fun component4(): SWRReset = reset 18 | public operator fun component5(): Boolean = isMutating 19 | 20 | public companion object { 21 | public fun empty( 22 | data: DATA? = null, 23 | error: Throwable? = null, 24 | trigger: SWRTrigger = SWRTrigger.empty(), 25 | reset: SWRReset = SWRReset.empty(), 26 | isMutating: Boolean = false, 27 | ): SWRMutationState = SWRMutationStateImpl(data = data, error = error, trigger = trigger, reset = reset, isMutating = isMutating) 28 | } 29 | } 30 | 31 | @Immutable 32 | internal data class SWRMutationStateImpl( 33 | override val data: DATA?, 34 | override val error: Throwable?, 35 | override val trigger: SWRTrigger, 36 | override val reset: SWRReset, 37 | override val isMutating: Boolean, 38 | ) : SWRMutationState 39 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/state/SWRState.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.state 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.mutate.SWRMutate 5 | 6 | public interface SWRState { 7 | public val data: DATA? 8 | public val error: Throwable? 9 | public val isLoading: Boolean 10 | public val isValidating: Boolean 11 | public val mutate: SWRMutate 12 | 13 | public operator fun component1(): DATA? = data 14 | public operator fun component2(): Throwable? = error 15 | public operator fun component3(): Boolean = isLoading 16 | public operator fun component4(): Boolean = isValidating 17 | public operator fun component5(): SWRMutate = mutate 18 | 19 | public companion object { 20 | public fun empty( 21 | data: DATA? = null, 22 | error: Throwable? = null, 23 | isLoading: Boolean = false, 24 | isValidating: Boolean = false, 25 | mutate: SWRMutate = SWRMutate.empty(), 26 | ): SWRState = SWRStateImpl(data = data, error = error, isLoading = isLoading, isValidating = isValidating, mutate = mutate) 27 | } 28 | } 29 | 30 | @Immutable 31 | internal data class SWRStateImpl( 32 | override val data: DATA?, 33 | override val error: Throwable?, 34 | override val isLoading: Boolean, 35 | override val isValidating: Boolean, 36 | override val mutate: SWRMutate, 37 | ) : SWRState 38 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/trigger/SWRReset.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.trigger 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.MutableState 5 | 6 | public interface SWRReset { 7 | public operator fun invoke() 8 | 9 | public companion object { 10 | public fun empty(): SWRReset = object : SWRReset { 11 | override fun invoke() {} 12 | } 13 | } 14 | } 15 | 16 | @Immutable 17 | internal data class SWRResetImpl( 18 | private val data: MutableState, 19 | private val error: MutableState, 20 | private val isMutating: MutableState, 21 | ) : SWRReset { 22 | 23 | override operator fun invoke() { 24 | data.value = null 25 | error.value = null 26 | isMutating.value = false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/trigger/SWRTrigger.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.trigger 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.MutableState 5 | import com.kazakago.swr.compose.cache.SWRCache 6 | import com.kazakago.swr.compose.config.SWRTriggerConfig 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import com.kazakago.swr.compose.validate.SWRValidate 9 | import kotlinx.coroutines.launch 10 | 11 | public interface SWRTrigger { 12 | public suspend operator fun invoke( 13 | arg: ARG, 14 | options: SWRTriggerConfig.() -> Unit = {}, 15 | ): DATA? 16 | 17 | public companion object { 18 | public fun empty(): SWRTrigger = object : SWRTrigger { 19 | override suspend fun invoke(arg: ARG, options: SWRTriggerConfig.() -> Unit): DATA? = null 20 | } 21 | } 22 | } 23 | 24 | @Immutable 25 | internal data class SWRTriggerImpl( 26 | private val key: KEY?, 27 | private val config: SWRTriggerConfig, 28 | private val cache: SWRCache, 29 | private val data: MutableState, 30 | private val error: MutableState, 31 | private val isMutating: MutableState, 32 | private val fetcher: (suspend (key: KEY, arg: ARG) -> DATA), 33 | private val validate: SWRValidate, 34 | ) : SWRTrigger { 35 | 36 | override suspend fun invoke( 37 | arg: ARG, 38 | options: SWRTriggerConfig.() -> Unit, 39 | ): DATA? { 40 | val currentKey = key ?: return null 41 | val config = config.apply { options() } 42 | val oldData: DATA? = data.value 43 | if (config.optimisticData != null) { 44 | data.value = config.optimisticData 45 | error.value = null 46 | } 47 | isMutating.value = true 48 | runCatching { 49 | fetcher(currentKey, arg) 50 | }.onSuccess { newData -> 51 | data.value = newData 52 | error.value = null 53 | isMutating.value = false 54 | config.onSuccess?.invoke(newData, currentKey, config) 55 | if (config.populateCache && newData != null) { 56 | cache.state(currentKey).value = newData 57 | } 58 | if (config.revalidate) { 59 | SWRGlobalScope.launch { validate(currentKey) } 60 | } 61 | }.onFailure { throwable -> 62 | isMutating.value = false 63 | config.onError?.invoke(throwable, currentKey, config) 64 | if (config.rollbackOnError) { 65 | data.value = oldData 66 | } 67 | if (config.throwOnError) { 68 | error.value = throwable 69 | throw throwable 70 | } 71 | } 72 | return data.value 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/validate/SWRValidate.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validate 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.kazakago.swr.compose.cache.SWRCache 5 | import com.kazakago.swr.compose.cache.SWRSystemCache 6 | import com.kazakago.swr.compose.config.SWRConfig 7 | import com.kazakago.swr.compose.internal.SWRGlobalScope 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.currentCoroutineContext 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.launch 12 | 13 | public interface SWRValidate { 14 | public suspend operator fun invoke( 15 | key: KEY?, 16 | options: SWRValidateOptions? = null, 17 | ) 18 | } 19 | 20 | @Immutable 21 | internal data class SWRValidateImpl( 22 | private val config: SWRConfig, 23 | private val cache: SWRCache, 24 | private val systemCache: SWRSystemCache, 25 | private val _fetcher: (suspend (key: KEY) -> DATA)?, 26 | ) : SWRValidate { 27 | 28 | override suspend operator fun invoke( 29 | key: KEY?, 30 | options: SWRValidateOptions?, 31 | ) { 32 | val currentKey = key ?: return 33 | val fetcher = _fetcher ?: systemCache.getFetcher(currentKey) ?: config.fetcher ?: return 34 | val error = systemCache.errorState(currentKey) 35 | val isValidating = systemCache.isValidatingState(currentKey) 36 | val validatedTimerJob = systemCache.getValidatedTimerJob(currentKey) 37 | if (isValidating.value || config.isPaused()) return 38 | if (validatedTimerJob != null && validatedTimerJob.isActive) return 39 | 40 | isValidating.value = true 41 | val timeoutJob = CoroutineScope(currentCoroutineContext()).launch { 42 | delay(config.loadingTimeout) 43 | config.onLoadingSlow?.invoke(currentKey, config) 44 | } 45 | runCatching { 46 | fetcher(currentKey) 47 | }.onSuccess { newData -> 48 | timeoutJob.cancel() 49 | cache.state(currentKey).value = newData 50 | val newValidatedTimerJob = SWRGlobalScope.launch { delay(config.dedupingInterval) } 51 | systemCache.setValidatedTimerJob(currentKey, newValidatedTimerJob) 52 | error.value = null 53 | config.onSuccess?.invoke(newData, currentKey, config) 54 | }.onFailure { throwable -> 55 | timeoutJob.cancel() 56 | val revalidateOptions = getValidateOptions(currentKey, options) 57 | error.value = throwable 58 | config.onError?.invoke(throwable, currentKey, config) 59 | if (config.shouldRetryOnError) { 60 | val job = CoroutineScope(currentCoroutineContext()).launch { config.onErrorRetry(throwable, currentKey, config, this@SWRValidateImpl, revalidateOptions) } 61 | systemCache.setRetryingJobSet(currentKey, systemCache.getRetryingJobSet(currentKey) + job) 62 | } 63 | } 64 | isValidating.value = false 65 | } 66 | 67 | private fun getValidateOptions(key: KEY, options: SWRValidateOptions?): SWRValidateOptions { 68 | return SWRValidateOptions( 69 | retryCount = (options?.retryCount ?: 0) + 1, 70 | dedupe = systemCache.getRetryingJobSet(key).any { it.children.none() && it.isActive }, 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /swr/src/commonMain/kotlin/com/kazakago/swr/compose/validate/SWRValidateOptions.kt: -------------------------------------------------------------------------------- 1 | package com.kazakago.swr.compose.validate 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | public data class SWRValidateOptions( 7 | public val retryCount: Int, 8 | public val dedupe: Boolean, 9 | ) 10 | --------------------------------------------------------------------------------