Skip to content

Commit

Permalink
useQuery hook from react-query (#2)
Browse files Browse the repository at this point in the history
* feat: useQuery hook from react-query

* feat: useMutation

* test: useMutation

* misc changes

* Rename module

* demo: useQuery

* demo: useMutation
  • Loading branch information
pavi2410 authored Sep 14, 2023
1 parent 3945f63 commit d6339e4
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 2 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
implementation(projects.react)
implementation(projects.hooks)
implementation(projects.network)
implementation(projects.query)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package me.pavi2410.useCompose.app.screens

import androidx.compose.runtime.Composable
import androidx.navigation.NavController

sealed class ExampleScreen(
val route: String,
Expand All @@ -13,6 +12,8 @@ sealed class ExampleScreen(
object Reducer : ExampleScreen("reducer", "Reducer", { ReducerExample() })
object Toggle : ExampleScreen("toggle", "Toggle", { ToggleExample() })
object Network : ExampleScreen("network", "Network", { NetworkExample() })
object Query : ExampleScreen("query", "Query", { QueryExample() })
object Mutation : ExampleScreen("mutation", "Mutation", { MutationExample() })
}

val exampleScreens
Expand All @@ -21,5 +22,7 @@ val exampleScreens
ExampleScreen.Context,
ExampleScreen.Reducer,
ExampleScreen.Toggle,
ExampleScreen.Network
ExampleScreen.Network,
ExampleScreen.Query,
ExampleScreen.Mutation,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package me.pavi2410.useCompose.app.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import kotlinx.coroutines.delay
import me.pavi2410.useCompose.query.useMutation

@Composable
fun MutationExample() {
Column {
var token by remember { mutableStateOf("") }

val loginMutation = useMutation { (username, password) ->
delay(500)
"secret_token:$username/$password"
}

Button(
modifier = Modifier.testTag("login_button"),
onClick = {
// todo: is this blocking the main thread?
// todo: this makes me think I need a mutateAsync too...
loginMutation.mutate("pavi2410", "secretpw123") {
token = it
}
}
) {
Text("Login")
}

Text(
if (token.isEmpty()) "Please login" else "Welcome! token = $token",
modifier = Modifier.testTag("status")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.pavi2410.useCompose.app.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import kotlinx.coroutines.delay
import me.pavi2410.useCompose.query.useQuery

@Composable
fun QueryExample() {
Column {
val data = useQuery {
delay(500)
"secret_token"
}

Text(data.toString())
}
}
1 change: 1 addition & 0 deletions query/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
68 changes: 68 additions & 0 deletions query/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
plugins {
id("com.android.library")
kotlin("android")
id("maven-publish")
}

android {
compileSdk = 31

defaultConfig {
minSdk = 21
targetSdk = 31

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.get()
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
implementation(libs.compose.ui)
implementation(libs.compose.material)
implementation(libs.compose.tooling.preview)

testImplementation("junit:junit:4.+")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
// Test rules and transitive dependencies:
androidTestImplementation(libs.compose.test.junit4)
// Needed for createComposeRule, but not createAndroidComposeRule:
debugImplementation(libs.compose.test.manifest)
}

afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
create<MavenPublication>("maven") {
from(components["release"])

groupId = "me.pavi2410.useCompose"
artifactId = "query"
version = "1.0.0"
}
}
}
}
Empty file added query/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions query/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package me.pavi2410.useCompose.query

import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import kotlinx.coroutines.delay
import org.junit.Rule
import org.junit.Test

class ReactQueryTest {

@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity

@Suppress("BlockingMethodInNonBlockingContext")
@Test
fun useQueryTest() {
val someData = """{data: "some data"}"""

// Start the app
composeTestRule.setContent {
Surface(color = MaterialTheme.colors.background) {
Column {
val queryResult by useQuery {
fetchSomeData(someData)
}

when (queryResult) {
is QueryState.Loading -> {
Text("Fetching data...", modifier = Modifier.testTag("loading"))
}
is QueryState.Error -> {
Text("something went wrong...", modifier = Modifier.testTag("error"))
}
is QueryState.Content -> {
val data = (queryResult as QueryState.Content<String>).data
Text("got data -> $data", modifier = Modifier.testTag("data"))
}
}
}
}
}

composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
composeTestRule.onNodeWithTag("error").assertDoesNotExist()
composeTestRule.onNodeWithTag("data").assertDoesNotExist()

Thread.sleep(1000) // wait for data to fetch

composeTestRule.onNodeWithTag("loading").assertDoesNotExist()
composeTestRule.onNodeWithTag("data").assertIsDisplayed()
composeTestRule.onNodeWithTag("data").assertTextEquals("got data -> $someData")
}

@Test
fun useMutationTest() {
// Start the app
composeTestRule.setContent {
Surface(color = MaterialTheme.colors.background) {
Column {
var token by remember { mutableStateOf("") }

val loginMutation = useMutation { (username, password) ->
doLogin(username, password)
}

Button(
modifier = Modifier.testTag("login_button"),
onClick = {
// todo: is this blocking the main thread?
// todo: this makes me think I need a mutateAsync too...
loginMutation.mutate("pavi2410", "secretpw123") {
token = it
}
}
) {
Text("Login")
}

Text(
if (token.isEmpty()) "Please login" else "Welcome! token = $token",
modifier = Modifier.testTag("status")
)
}
}
}

composeTestRule.onNodeWithTag("login_button").performClick()
composeTestRule.onNodeWithTag("status").assertTextEquals("Please login")

Thread.sleep(1000) // wait for data to fetch

// verify that mutation is called
composeTestRule.onNodeWithTag("status").assertTextEquals("Welcome! token = secret_token")
}

private suspend fun fetchSomeData(someData: String): String {
delay(500)
return someData
}

private suspend fun doLogin(username: String, password: String): String {
delay(5)
return "secret_token"
}
}
7 changes: 7 additions & 0 deletions query/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.pavi2410.useCompose.query">

<uses-permission android:name="android.permission.INTERNET" />

</manifest>
56 changes: 56 additions & 0 deletions query/src/main/java/me/pavi2410/useCompose/query/hooks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package me.pavi2410.useCompose.query

import android.annotation.SuppressLint
import androidx.compose.runtime.*
import kotlinx.coroutines.*

sealed interface QueryState<out T> {
object Loading : QueryState<Nothing>
data class Error(val message: Throwable) : QueryState<Nothing>
data class Content<T>(val data: T) : QueryState<T>
}

sealed interface MutationState<T> {
object Idle : MutationState<Nothing>
object Loading : MutationState<Nothing>
data class Error(val message: Throwable) : MutationState<Nothing>
data class Content<T>(val data: T) : MutationState<T>
}

interface Mutation<T> {
fun mutate(vararg args: String, callback: (T) -> Unit)
fun cancel()
// val state: MutationState<T>
}

@Composable
fun <T> useQuery(query: suspend CoroutineScope.() -> T): State<QueryState<T>> {
return produceState<QueryState<T>>(initialValue = QueryState.Loading) {
value = withContext(Dispatchers.IO) {
val res = query()
QueryState.Content(res)
}
}
}

@SuppressLint("ComposableNaming")
@Composable
fun <T> useMutation(query: suspend CoroutineScope.(args: Array<out String>) -> T): Mutation<T> {
// var mutationState by remember { mutableStateOf(MutationState.Idle) }
val coroutineScope = rememberCoroutineScope()
return object: Mutation<T> {
override fun mutate(vararg args: String, callback: (T) -> Unit) {
coroutineScope.launch {
// mutationState = MutationState.Loading
callback(query(args))
// mutationState = MutationState.Success
}
}
override fun cancel() {
coroutineScope.cancel()
}

// override val state: MutationState<T>
// get() = mutationState
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ include(":react")
include(":hooks")
include(":network")
include(":colors")
include(":query")

0 comments on commit d6339e4

Please sign in to comment.