Open martinbonnin opened 3 years ago
With Jetpack compose getting more and more traction, should we move our tutorial to Jetpack Compose?
It sounds great.
Which existing users can we speak with who already are using Jetpack Compose with our Kotlin client, what has their experience been so far?
We created a declarative interface like apollo-web. because apollo-kotlin doesn't have it. It would be hard for all developers to do that. And not yet able to handle all the options of the Apollo web. I would be very happy if you could officially support a declarative interface like apollo web in apollo kotlin. And it would have a great impact on the spread of Jetpack Compose and GraphQL on Android.
context(ApolloClient)
@Composable
fun UserProfile() {
val (data, loading, error) = apollo.query(q = UserProfileQuery())
when {
loading -> Spinner()
error != null -> ErrorDialog(error = error)
else -> Text(text = data.viewer.name)
}
}
function UserProfile() {
const { data, loading, error } = useQuery(UserProfileDocument);
if (loading) {
return <Spinner />;
}
if (error) {
return <ErrorDialog error={error}/>;
}
return <div>data.viewer.name</div>;
};
If this can be achieved, Web and Android development will feel very close.
Thanks @sonatard for the feedback! This looks interesting!
The exact syntax for the Kotlin code would have to be a bit different, right? (I'm confused by the when
there). Unless I'm missing something!
(The "Compose-ification" of the tutorial is in progress).
@BoD
The exact syntax for the Kotlin code would have to be a bit different, right? (I'm confused by the when there). Unless I'm missing something!
What seems to be the difference when
? I didn't build it, so I could be wrong.
(The "Compose-ification" of the tutorial is https://github.com/apollographql/apollo-kotlin-tutorial/issues/38).
👍
I think a possible syntax for what you suggest could resemble something like:
val result by apollo.query(q = UserProfileQuery())
when (result) {
Loading -> Spinner()
is Error -> ErrorDialog(error = result.error)
is Data -> Text(text = result.data.viewer.name)
}
I used this when
expiression!
https://kotlinlang.org/docs/control-flow.html#when-expression
But Kotlin would be better represented by an algebraic data type with a sealed interface as you wrote 👍
Hi. We are looking forward to the development of the Compose Extension. Let me introduce an option that we value when writing Jetpack Compose.
It is the skip
option for useQuery. By using this, you can suppress the execution of the query without using Effect.
https://www.apollographql.com/docs/react/api/react/hooks/#params
In React, it is mentioned that using Effect recklessly can cause problems.
https://react.dev/learn/you-might-not-need-an-effect
We like that using the skip option allows us to write more declarative code.
// Effect version
val (fetch, data, loading, error) = apollo.lazyQuery<XXXQuery>()
LaunchedEffect(key1 = value) {
if (value != null) {
fetch(value)
}
}
In our wrapper, using the skip option enables the following refactoring:
// skip version
val (data, loading, error) = apollo.query(skip = value == null, q = XXXQuery)
@sonatard if the query is not executed, there still need to be an initial state right? Is it going to be loading
? Something else?
Our wrapper is if skip
is true, data=null loading=false error=null. I think apollo-web behaves in the same way. But I haven't confirmed it.
data=null loading=false error=null
Gotcha 👍 That's an interesting one as we're usually trying to model either data != null or error != null. Will need to let that sink in a bit but thanks for the feature request 👍
@BoD @martinbonnin Thank you for releasing the Jetpack Compose support!
For mutations, we have prepared a wrapper like the following. It is an interface aligned with Apollo Web's mutations. https://www.apollographql.com/docs/react/data/mutations/
Is there any plan to support mutations? or should we implement this individually?
context(ApolloClient)
@Composable
fun FavoriteButton() {
val (favoriteProductMutation, data, loading, error) = apollo.mutation<FavoriteProductMutation.Data>()
when {
error != null -> {
ErrorDialog(error = error)
}
loading -> {
Spinner()
}
else -> {
Button(onClick = {
favoriteProductMutation()
}) {
Text(text = "fav")
}
}
}
}
Hi! This is interesting, thanks for sharing.
At the moment, the toState()
that was introduced in compose-support
can be used for mutations. It returns a nullable ApolloResponse
which is initially null
(loading), and has .exception
and .errors
for the error cases, as well as .data
for the success case. So in essence it is similar to what you describe (minus the destructuring syntax).
The interesting difference is the returned lambda that executes the mutation. Not 100% sure about this one, the pattern is a bit unfamiliar to me (wondering if maybe it's a common JS pattern). In toState()
the call executes the operation immediately. But in your example this looks useful indeed.
That's good food for thoughts!
In the meantime, if you are starting to use the compose-support
, feedback is very welcome!
Thank you for your reply!
Not 100% sure about this one, the pattern is a bit unfamiliar to me (wondering if maybe it's a common JS pattern).
Yes, this is a very common pattern for declarative UI(React etc) in Javascript.
Nowadays, many libraries are built with this interface, such as Apollo, Relay, urql, React Query, SWR, and others.
That's good food for thoughts! In the meantime, if you are starting to use the compose-support, feedback is very welcome!
We have already implemented a mutation wrapper internally. We plan to refactor it by compose-support
. Please let me consult with we again after the refactoring.
Hi! I prepared a Query function for Jetpack Compose that supports skip and lazyQuery. Support for pagination is currently under consideration. We are further customizing this, so we don't necessarily want these to be merged into Apollo Kotlin, but we would be happy if it could be of any reference. Also, please let me know if you have any advice on how to do better.
sealed class QueryResult<out D> {
data class Init(val exec: () -> Unit) : QueryResult<Nothing>()
data object Loading : QueryResult<Nothing>()
data class Success<D>(val data: D, val fetch: () -> Unit) : QueryResult<D>()
data class Error(val error: ApolloException, val fetch: () -> Unit) : QueryResult<Nothing>()
}
@Composable
fun <D : Query.Data> ApolloClient.query(
q: Query<D>,
watch: Boolean = true,
skip: Boolean = false,
option: ApolloCall<D>.() -> Unit = {},
): QueryResult<D> {
var executed by remember(skip) { mutableStateOf(!skip) }
if (!executed) {
return QueryResult.Init(exec = { executed = true })
}
var refetching by remember { mutableStateOf(false) }
val keys = arrayOf(q.hashCode(), option, refetching)
val apolloResponseFlow = remember(keys = keys) {
when (watch) {
true -> {
query(q)
.apply(option)
.apply { if (refetching) fetchPolicy(FetchPolicy.NetworkOnly) }
.watch()
.onEach { refetching = false }
}
false -> {
query(q)
.apply(option)
.apply { if (refetching) fetchPolicy(FetchPolicy.NetworkOnly) }
.toFlow()
.onEach { refetching = false }
}
}
}
val apolloResponse by apolloResponseFlow.collectAsState(initial = null)
return apolloResponse?.let { response ->
val loading = response.data == null && response.exception is CacheMissException
val error = if (response.exception is CacheMissException) null else response.exception
val data = response.data
return when {
loading || refetching -> QueryResult.Loading
error != null -> QueryResult.Error(error = error, fetch = { refetching = true })
data != null -> QueryResult.Success(data = data, fetch = { refetching = true })
else -> QueryResult.Loading
}
} ?: QueryResult.Loading
}
@Composable
fun Page() {
val query = apollo.query(q = PageQuery())
when (query) {
is QueryResult.Init -> Unit
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
@Composable
fun Page() {
val query = apollo.query(q = PageQuery(), watch = false)
when (query) {
is QueryResult.Init -> Unit
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
@Composable
fun Page() {
val query = apollo.query(q = PageQuery(), skip = true)
when (query) {
is QueryResult.Init -> Button(onClick = { query.exec() }) {
Text(text = "Lazy Query")
}
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
@Composable
fun Page(skip: Boolean) {
val query = apollo.query(q = PageQuery(), skip = skip)
when (query) {
is QueryResult.Init -> Unit
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
@Composable
fun Page() {
val query = apollo.query(q = PageQuery())
when (query) {
is QueryResult.Init -> Unit
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Column {
Button(onClick = { query.fetch() }) {
Text(text = "Refetch")
}
Text(text = query.data.user.name)
}
}
}
}
Thanks for sharing @sonatard , this is certainly interesting! It looks good to me. Similarly to what you shared previously I’m still intrigued about the Init
case and the ability to pass skip = true
as to me this can be handled at the call site instead. But maybe that simplifies the syntax.
One small detail: why not name exec
as fetch
? That way it’s the same for all cases.
One small detail: why not name exec as fetch? That way it’s the same for all cases.
I have changed the name because it does not work correctly if fetch is executed before skip becomes false (executed state is true).
exec -> fetch fetch -> refetch might also be good.
Similarly to what you shared previously I’m still intrigued about the Init case and the ability to pass skip = true as to me this can be handled at the call site instead. But maybe that simplifies the syntax.
I fixed without skip. But I didn't test. I prefer having the skip feature in the query because it creates less noise when writing standard code.
@Composable
fun Page() {
var skip by remember { mutableStateOf(true) }
val query = if (!skip) apollo.query(q = PageQuery()) else QueryResult.Init(exec = { skip = false })
when (query) {
is QueryResult.Init -> Button(onClick = { skip = false }) {
Text(text = "Lazy Query")
}
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
@Composable
fun Page(skip: Boolean) {
val query = if (!skip) apollo.query(q = PageQuery()) else QueryResult.Init(exec = { skip = false })
when (query) {
is QueryResult.Init -> Unit
is QueryResult.Loading -> Spinner()
is QueryResult.Error -> ErrorDialog(e = query.error)
is QueryResult.Success -> {
Text(text = query.data.user.name)
}
}
}
Considerations