Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First drop of experimental Compose support libraries (to release-3.x) #4802

Merged
merged 4 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gradle/libraries.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ android-plugin = "7.2.1"
android-plugin-min = "3.4.2"
android-sdkversion-compile = "31"
android-sdkversion-min = "15"
android-sdkversion-compose-min = "21"
android-sdkversion-target = "30"
androidx-sqlite = "2.1.0"
antlr = "4.9.3"
# This is used by the gradle integration tests to get the artifacts locally
apollo = "3.7.6-SNAPSHOT"
cache = "2.0.2"
# See https://developer.android.com/jetpack/androidx/releases/compose-kotlin
compose-compiler = "1.4.4"
dokka = "1.7.10"
guava = "31.1-jre"
javaPoet = "1.13.0"
Expand Down Expand Up @@ -68,6 +71,7 @@ assertj = { group = "org.assertj", name = "assertj-core", version = "3.21.0" }
atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu", version = "0.17.0" }
benmanes-versions = { group = "com.github.ben-manes", name = "gradle-versions-plugin", version = "0.33.0" }
clikt = { group = "com.github.ajalt.clikt", name = "clikt", version = "3.4.0" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version = "1.3.3" }
dokka-base = { group = "org.jetbrains.dokka", name = "dokka-base", version.ref = "dokka" }
dokka-plugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" }
google-testing-compile = { group = "com.google.testing.compile", name = "compile-testing", version = "0.19" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
public final class com/apollographql/apollo3/compose/paging/ApolloPagingSourceKt {
}

public final class com/apollographql/apollo3/compose/paging/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}

27 changes: 27 additions & 0 deletions libraries/apollo-compose-paging-support/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("apollo.library")
}

dependencies {
api(project(":apollo-compose-support"))
api("androidx.paging:paging-compose:1.0.0-alpha18")
}

android {
compileSdk = golatac.version("android.sdkversion.compile").toInt()

defaultConfig {
minSdk = golatac.version("android.sdkversion.compose.min").toInt()
targetSdk = golatac.version("android.sdkversion.target").toInt()
}

buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = golatac.version("compose.compiler")
}
}
4 changes: 4 additions & 0 deletions libraries/apollo-compose-paging-support/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=apollo-compose-paging-support
POM_NAME=Apollo Compose Paging Support
POM_DESCRIPTION=Apollo Compose Paging Support
POM_PACKAGING=aar
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest
package="com.apollographql.apollo3.compose.paging">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package com.apollographql.apollo3.compose.paging

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.apollographql.apollo3.ApolloCall
import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.compose.exception
import com.apollographql.apollo3.compose.tryExecute

@ApolloExperimental
class ApolloPagingSource<Data : Operation.Data, Value : Any>(
/**
* The call to fetch the next page, given the response for the current page.
* This is also used for the initial/refresh call.
* The response will be null for the initial/refresh call.
* Return null if there is no next page.
*/
private val appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,

/**
* Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
private val itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },

/**
* The call to fetch the previous page, given the response for the current page.
* Return null if there is no previous page.
* Can be null if prepend is not supported.
*/
private val prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,

/**
* Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
private val itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> LoadResult.Page.COUNT_UNDEFINED },

/**
* Extract the list of items from a response.
* Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
*/
private val getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
) : PagingSource<ApolloCall<Data>, Value>() {
private var loadedItemsCount = 0

override fun getRefreshKey(state: PagingState<ApolloCall<Data>, Value>): ApolloCall<Data>? = null

override suspend fun load(params: LoadParams<ApolloCall<Data>>): LoadResult<ApolloCall<Data>, Value> {
val call = when (params) {
is LoadParams.Refresh -> {
loadedItemsCount = 0
appendCall(null, params.loadSize)
}

is LoadParams.Append -> params.key
is LoadParams.Prepend -> params.key
}

val response = call?.tryExecute() ?: error("appendCall must not return null for the initial/refresh call")
if (response.exception != null) return LoadResult.Error(response.exception!!)
val itemsResult: Result<List<Value>> = getItems(response)
if (itemsResult.isFailure) return LoadResult.Error(itemsResult.exceptionOrNull()!!)
val data = itemsResult.getOrThrow()
loadedItemsCount += data.size
val itemsBefore = if (params.placeholdersEnabled) {
itemsBefore(response, loadedItemsCount)
} else {
LoadResult.Page.COUNT_UNDEFINED
}
val itemsAfter = if (params.placeholdersEnabled) {
itemsAfter(response, loadedItemsCount)
} else {
LoadResult.Page.COUNT_UNDEFINED
}
return LoadResult.Page(
data = data,
prevKey = prependCall?.invoke(response, params.loadSize),
nextKey = appendCall(response, params.loadSize),
itemsBefore = itemsBefore,
itemsAfter = itemsAfter,
)
}
}

@ApolloExperimental
fun <Data : Operation.Data, Value : Any> Pager(
/**
* The [androidx.paging.Pager] configuration.
*/
config: PagingConfig,

/**
* The call to fetch the next page, given the response for the current page.
* This is also used for the initial/refresh call.
* The response will be null for the initial/refresh call.
* Return null if there is no next page.
*/
appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,

/**
* Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },

/**
* The call to fetch the previous page, given the response for the current page.
* Return null if there is no previous page.
* Can be null if prepend is not supported.
*/
prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,

/**
* Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },

/**
* Extract the list of items from a response.
* Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
*/
getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
): Pager<ApolloCall<Data>, Value> {
return Pager(
config = config,
pagingSourceFactory = {
ApolloPagingSource(
appendCall = appendCall,
itemsAfter = itemsAfter,
prependCall = prependCall,
itemsBefore = itemsBefore,
getItems = getItems,
)
}
)
}

@ApolloExperimental
@Composable
fun <Data : Operation.Data, Value : Any> rememberAndCollectPager(
/**
* The [androidx.paging.Pager] configuration.
*/
config: PagingConfig,

/**
* The call to fetch the next page, given the response for the current page.
* This is also used for the initial/refresh call.
* The response will be null for the initial/refresh call.
* Return null if there is no next page.
*/
appendCall: suspend (response: ApolloResponse<Data>?, loadSize: Int) -> ApolloCall<Data>?,

/**
* Count of items after the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
itemsAfter: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },

/**
* The call to fetch the previous page, given the response for the current page.
* Can be null if prepend is not supported.
* Return null if there is no previous page.
*/
prependCall: (suspend (response: ApolloResponse<Data>, loadSize: Int) -> ApolloCall<Data>?)? = null,

/**
* Count of items before the loaded data, given the response for the current page and the count of loaded items so far.
* Used to display placeholders.
* Return [PagingSource.LoadResult.Page.COUNT_UNDEFINED] if the count is unknown.
*/
itemsBefore: suspend (response: ApolloResponse<Data>, loadedItemsCount: Int) -> Int = { _, _ -> PagingSource.LoadResult.Page.COUNT_UNDEFINED },

/**
* Extract the list of items from a response.
* Return [Result.failure] if the response cannot be used (e.g. it contains GraphQL errors).
*/
getItems: suspend (response: ApolloResponse<Data>) -> Result<List<Value>>,
): LazyPagingItems<Value> {
val pager = remember {
Pager(
config = config,
appendCall = appendCall,
itemsAfter = itemsAfter,
prependCall = prependCall,
itemsBefore = itemsBefore,
getItems = getItems,
)
}
return pager.flow.collectAsLazyPagingItems()
}
13 changes: 13 additions & 0 deletions libraries/apollo-compose-support/api/apollo-compose-support.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public final class com/apollographql/apollo3/compose/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class com/apollographql/apollo3/compose/ExceptionsKt {
}

public final class com/apollographql/apollo3/compose/StateKt {
}

28 changes: 28 additions & 0 deletions libraries/apollo-compose-support/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("apollo.library")
}

dependencies {
api(golatac.lib("compose-runtime"))
api(project(":apollo-runtime"))
api(project(":apollo-normalized-cache"))
}

android {
compileSdk = golatac.version("android.sdkversion.compile").toInt()

defaultConfig {
minSdk = golatac.version("android.sdkversion.compose.min").toInt()
targetSdk = golatac.version("android.sdkversion.target").toInt()
}

buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = golatac.version("compose.compiler")
}
}
4 changes: 4 additions & 0 deletions libraries/apollo-compose-support/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=apollo-compose-support
POM_NAME=Apollo Compose Support
POM_DESCRIPTION=Apollo Compose Support
POM_PACKAGING=aar
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest
package="com.apollographql.apollo3.compose">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.apollographql.apollo3.compose

import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.annotations.ApolloInternal
import com.apollographql.apollo3.ApolloCall
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.ExecutionContext
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.exception.ApolloException
import com.benasher44.uuid.uuid4

@ApolloExperimental
val ApolloResponse<*>.exception: ApolloException?
get() = executionContext[ExceptionElement]?.exception

private class ExceptionElement(val exception: ApolloException) : ExecutionContext.Element {
override val key: ExecutionContext.Key<*> = Key

companion object Key : ExecutionContext.Key<ExceptionElement>
}

@ApolloInternal
fun <D : Operation.Data> ApolloResponse(call: ApolloCall<D>, exception: ApolloException) =
ApolloResponse.Builder(operation = call.operation, requestUuid = uuid4(), data = null)
.addExecutionContext(ExceptionElement(exception))
.build()

@ApolloInternal
suspend fun <D : Operation.Data> ApolloCall<D>.tryExecute(): ApolloResponse<D> = try {
execute()
} catch (e: ApolloException) {
ApolloResponse(call = this, exception = e)
}
Loading