feat(android): platform-owned GraphQL consumption#115
Conversation
- Add AndroidManifest.xml with launcher activity and app_name/theme refs - Add MainActivity (ComponentActivity + setContent + enableEdgeToEdge) - Add ForgeTheme using Material3 lightColorScheme/darkColorScheme - Add res/values/strings.xml and themes.xml - Enable Compose buildFeatures + composeOptions (compiler 1.5.15) - Wire Compose BOM 2024.09.00; add material3 and tooling deps - Update README with assembleDebug / installDebug build commands Resolves #80 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add gradle.properties: android.useAndroidX=true (was missing; AAPT rejected AndroidX deps at runtime classpath check) - Fix themes.xml parent: Theme.Material.NoTitleBar → Theme.Material.Light.NoActionBar (NoTitleBar variant does not exist in the platform resource set) - Add compileOptions (sourceCompatibility/targetCompatibility = 17) and kotlinOptions (jvmTarget = 17) to app/build.gradle.kts to resolve JVM-target mismatch between compileDebugJavaWithJavac (1.8 default) and compileDebugKotlin (21 from local JDK) assembleDebug now completes: BUILD SUCCESSFUL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Apollo Kotlin codegen with platform-owned operations (ExperienceBySlug, Experiences) sourced from apps/cms/schema.graphql. Implement GraphQLContentClient adapter with bearer-token auth via HTTP interceptor. Add parity checklist to README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (3)
WalkthroughAdds Android Apollo Kotlin GraphQL integration: platform-owned operations, Apollo codegen and Gradle setup, a GraphQLContentClient with bearer-token auth, two GraphQL queries, a Compose entry activity and theme, resources, and related docs/config. Changes
Sequence DiagramsequenceDiagram
participant MA as MainActivity
participant GCC as GraphQLContentClient
participant Apollo as Apollo Client
participant Auth as AuthInterceptor
participant API as CMS GraphQL API
MA->>GCC: getContent(locale, slug)
GCC->>Apollo: execute(ExperienceBySlugQuery)
Apollo->>Auth: intercept(request)
Auth->>API: HTTP POST (Authorization: Bearer ...)
API-->>Auth: GraphQL response
Auth-->>Apollo: response
Apollo-->>GCC: ExperienceBySlug result
GCC->>GCC: map to MobileContentItem
GCC-->>MA: MobileContentItem?
MA->>GCC: listExperiences(locale, page, pageSize)
GCC->>Apollo: execute(ExperiencesQuery)
Apollo->>Auth: intercept(request)
Auth->>API: HTTP POST (Authorization: Bearer ...)
API-->>Auth: GraphQL response
Auth-->>Apollo: response
Apollo-->>GCC: Experiences result
GCC->>GCC: map to List<MobileContentItem>
GCC-->>MA: List<MobileContentItem>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mobile/android/app/build.gradle.kts`:
- Around line 25-26: The build currently injects a static secret via
buildConfigField("String", "GRAPHQL_TOKEN", ...), which compiles the token into
the APK; remove any non-empty default for GRAPHQL_TOKEN in the build.gradle.kts
and stop shipping credentials in BuildConfig.GROUPQL_TOKEN; instead retrieve
tokens at runtime (e.g., user/session token from authenticated backend or a
secure backend proxy) or inject per-build secrets via CI/properties without
embedding them in source (use an empty default and load real token from a secure
runtime source).
In `@mobile/android/app/src/main/graphql/schema.graphqls`:
- Around line 1-1153: Generated GraphQL schema ("This file was generated by
Nexus Schema") is failing Prettier CI; update the generation pipeline so the
produced schema is consistently formatted or explicitly excluded from Prettier.
Fix by either: (A) integrating Prettier formatting into the schema generation
step used by schema-sync/codegen so the output of the generator (the generated
schema file) is run through Prettier before committing, or (B) adding the
generated schema pattern to Prettier ignore rules so CI skips it; update the
schema generation task or .prettierignore entry accordingly and ensure the
change addresses the Prettier CI warning.
- Around line 1-3: This file is a committed duplicate of the Strapi-generated
schema (schema.graphqls) and should be removed; instead update the Apollo
codegen/config so it consumes the canonical Strapi schema at
apps/cms/**/schema.graphql (or add a build/CI sync step that copies the
canonical schema into the mobile build) and delete the duplicate
mobile/android/app/src/main/graphql/schema.graphqls; ensure any references in
the Apollo/GraphQL generation step point to the canonical Strapi-generated
schema to avoid drift.
In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt`:
- Around line 38-45: The current mapper for computing body returns an empty
string inside mapNotNull which counts as a non-null result and may cause an
empty value to be chosen first; update the mapping logic in
GraphQLContentClient.kt (the variable body creation) so each branch returns null
when there is no actual content (e.g., change the InfoBlocks branch to return
section.description ?: section.heading ?: null and ensure CTA/Promo branches
return null when blank), keep using mapNotNull(...).firstOrNull() ?: "" so the
fallback to "" happens only after no non-empty values were found.
- Line 35: Replace the silent use of firstOrNull() on response.data?.experiences
with an explicit uniqueness check: inspect response.data?.experiences (the
collection used to set the experience variable) and if it contains more than one
element fail fast (throw an exception or return a descriptive error/log) instead
of picking the first item; otherwise, when exactly one element exists, assign it
to the experience variable as before. Ensure the check occurs before assigning
to experience and include a clear error message that references the
slug/criteria that produced multiple matches.
- Around line 30-35: Both getContent and listExperiences currently read
response.data without checking GraphQL errors; update the handling after
apolloClient.query(...).execute() (e.g. the ExperienceBySlugQuery call in
getContent and the query in listExperiences) to first inspect response.errors
and handle non-empty errors explicitly: log or surface the errors (include error
messages) and return/throw an appropriate failure instead of silently falling
back to null/empty; only proceed to use response.data (accessing
response.data?.experiences?.firstOrNull() etc.) when response.errors is
null/empty.
In `@mobile/android/app/src/main/kotlin/com/forge/mobile/MainActivity.kt`:
- Line 24: The Text composable in MainActivity.kt currently hardcodes visible
text ("Forge Android"); extract this string into Android string resources
(res/values/strings.xml) as a key like forge_android and replace the hardcoded
literal with a resource lookup (e.g., use stringResource(R.string.forge_android)
in the composable or getString(R.string.forge_android) where appropriate), and
add the necessary import (androidx.compose.ui.res.stringResource) so the UI
reads from resources for proper localization.
In `@mobile/android/README.md`:
- Around line 1-60: Run Prettier on mobile/android/README.md to fix the markdown
formatting drift reported by CI: open the README (file name
mobile/android/README.md) and apply the repository's Prettier config (e.g., npx
prettier --write mobile/android/README.md or your IDE formatter) so tables, code
blocks, and line wrapping conform to the project's style; commit the reformatted
README to make forge-ci green.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (13)
mobile/android/README.mdmobile/android/app/build.gradle.ktsmobile/android/app/src/main/AndroidManifest.xmlmobile/android/app/src/main/graphql/ExperienceBySlug.graphqlmobile/android/app/src/main/graphql/Experiences.graphqlmobile/android/app/src/main/graphql/schema.graphqlsmobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.ktmobile/android/app/src/main/kotlin/com/forge/mobile/MainActivity.ktmobile/android/app/src/main/kotlin/com/forge/mobile/ui/theme/Theme.ktmobile/android/app/src/main/res/values/strings.xmlmobile/android/app/src/main/res/values/themes.xmlmobile/android/build.gradle.ktsmobile/android/gradle.properties
- Security: read GRAPHQL_TOKEN from local.properties instead of hardcoding an empty string in build.gradle.kts - Schema: point Apollo directly to apps/cms/schema.graphql (canonical source) and delete the committed duplicate schema.graphqls copy - Apollo: add fieldsOnDisjointTypesMustMerge=false to fix codegen failure caused by heading/description/ctaLink having different nullability across disjoint union members (ComponentSectionsCta vs ComponentSectionsInfoBlocks vs ComponentSectionsPromoBanner) - GraphQLContentClient: fail fast when >1 experience returned for a slug instead of silently using firstOrNull() - GraphQLContentClient: drop ?: "" fallback in mapNotNull so null sections are filtered rather than emitting an empty string - MainActivity: replace hardcoded "Forge Android" with stringResource(R.string.home_title) for i18n parity - strings.xml: add home_title string resource - .editorconfig: allow PascalCase for @composable functions (Compose naming convention) to fix ktlint violation in Theme.kt - .gitignore: ignore local.properties and schema.graphqls at both root and mobile/android level - README: update config docs to reflect local.properties token pattern and direct schema reference; run Prettier - ktlintFormat: auto-fix formatting violations in GraphQLContentClient.kt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both getContent and listExperiences were accessing response.data without checking response.errors. Apollo Kotlin does not throw on GraphQL errors — they are surfaced in ApolloResponse.errors while execute() returns normally. Add an explicit errors check after each execute() call and throw IllegalStateException with the error messages so callers are not silently given null/empty results on server-side failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt (1)
32-35:⚠️ Potential issue | 🟠 MajorHandle GraphQL
response.errorsbefore consumingresponse.data.Both query paths currently treat GraphQL errors as empty/null data paths. That can silently hide server contract failures.
Proposed fix
override suspend fun getContent( locale: String, slug: String, ): MobileContentItem? { val response = apolloClient .query(ExperienceBySlugQuery(slug = slug, locale = com.apollographql.apollo.api.Optional.present(locale))) .execute() + response.errors?.takeIf { it.isNotEmpty() }?.let { errors -> + throw IllegalStateException( + "ExperienceBySlug failed: ${errors.joinToString { it.message }}" + ) + } val experiences = response.data?.experiences.orEmpty() @@ suspend fun listExperiences( locale: String, page: Int = 1, pageSize: Int = 25, ): List<MobileContentItem> { val response = apolloClient @@ ) .execute() + response.errors?.takeIf { it.isNotEmpty() }?.let { errors -> + throw IllegalStateException( + "Experiences failed: ${errors.joinToString { it.message }}" + ) + } return response.data?.experiences?.map { experience ->In Apollo Kotlin 4.x, does query(...).execute() throw on GraphQL errors, or are GraphQL errors returned in ApolloResponse.errors while data may be null/partial?Also applies to: 80-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt` around lines 32 - 35, Check the ApolloResponse for GraphQL errors before reading or consuming response.data: after calling apolloClient.query(ExperienceBySlugQuery(...)).execute() (and the similar query path around lines 80-90), inspect response.errors (or response.hasErrors()) and if non-empty throw or return an error result/log with the errors instead of proceeding to use response.data; update the handling in the functions that call ExperienceBySlugQuery and the other query invocation to fail fast on GraphQL errors and only consume response.data when errors is empty and data is non-null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mobile/android/README.md`:
- Around line 47-50: The README's claim that the secret is "never compiled into
... the APK artifact" is inaccurate because app/build.gradle.kts injects the
token into BuildConfig via buildConfigField("String", "GRAPHQL_TOKEN", ...),
which gets compiled into the APK; update mobile/android/README.md to state that
the token is kept out of version control (from local.properties) but is included
in the built APK as BuildConfig.GRAPHQL_TOKEN and thus extractable by
decompilation, and suggest alternatives (e.g., fetching from a backend or using
Android Keystore) or note how to override the endpoint in app/build.gradle.kts
if needed.
---
Duplicate comments:
In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt`:
- Around line 32-35: Check the ApolloResponse for GraphQL errors before reading
or consuming response.data: after calling
apolloClient.query(ExperienceBySlugQuery(...)).execute() (and the similar query
path around lines 80-90), inspect response.errors (or response.hasErrors()) and
if non-empty throw or return an error result/log with the errors instead of
proceeding to use response.data; update the handling in the functions that call
ExperienceBySlugQuery and the other query invocation to fail fast on GraphQL
errors and only consume response.data when errors is empty and data is non-null.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
.gitignoremobile/android/.editorconfigmobile/android/.gitignoremobile/android/README.mdmobile/android/app/build.gradle.ktsmobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.ktmobile/android/app/src/main/kotlin/com/forge/mobile/MainActivity.ktmobile/android/app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (3)
- mobile/android/app/build.gradle.kts
- mobile/android/app/src/main/kotlin/com/forge/mobile/MainActivity.kt
- mobile/android/app/src/main/res/values/strings.xml
There was a problem hiding this comment.
🧹 Nitpick comments (2)
mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt (2)
109-111: Consider implementingCloseablefor idiomatic resource management.Adding
Closeableallows usage with Kotlin'suse {}or Java's try-with-resources patterns.♻️ Suggested change
+import java.io.Closeable + class GraphQLContentClient( endpoint: String, bearerToken: String, -) : ContentClient { +) : ContentClient, Closeable {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt` around lines 109 - 111, Update the GraphQLContentClient class to implement java.io.Closeable so callers can use Kotlin's use {} or Java try-with-resources; add the import for java.io.Closeable, change the class declaration to implement Closeable, and mark the existing fun close() as override (keeping the body that calls apolloClient.close()) so the type is recognized as a Closeable resource.
34-34: Consider importingOptionalto reduce verbosity.The fully qualified
com.apollographql.apollo.api.Optional.present()is used multiple times. A single import would improve readability.♻️ Suggested import
import com.apollographql.apollo.network.http.HttpInterceptorChain +import com.apollographql.apollo.api.Optional import com.forge.mobile.graphql.ExperienceBySlugQueryThen replace usages:
- .query(ExperienceBySlugQuery(slug = slug, locale = com.apollographql.apollo.api.Optional.present(locale))) + .query(ExperienceBySlugQuery(slug = slug, locale = Optional.present(locale)))Also applies to: 87-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt` at line 34, Add an import for com.apollographql.apollo.api.Optional and replace fully-qualified calls to com.apollographql.apollo.api.Optional.present(...) with Optional.present(...) where used (e.g., in the ExperienceBySlugQuery slug/locale call and the other Optional.present usages around the content fetch methods), so the code uses the shorter Optional symbol instead of the long package-qualified name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt`:
- Around line 109-111: Update the GraphQLContentClient class to implement
java.io.Closeable so callers can use Kotlin's use {} or Java try-with-resources;
add the import for java.io.Closeable, change the class declaration to implement
Closeable, and mark the existing fun close() as override (keeping the body that
calls apolloClient.close()) so the type is recognized as a Closeable resource.
- Line 34: Add an import for com.apollographql.apollo.api.Optional and replace
fully-qualified calls to com.apollographql.apollo.api.Optional.present(...) with
Optional.present(...) where used (e.g., in the ExperienceBySlugQuery slug/locale
call and the other Optional.present usages around the content fetch methods), so
the code uses the shorter Optional symbol instead of the long package-qualified
name.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt
The previous wording said the token is "never compiled into the APK artifact" which is inaccurate — buildConfigField embeds the value in BuildConfig and it exists in the APK binary. Clarify that local.properties keeps the token out of version control, but it is still compiled into the APK and a short-lived/scoped token should be used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .gitignore: keep local.properties + add iOS xcuserdata/Generated ignores from main - README.md: keep PR's GraphQL integration docs; drop superseded startup-screen note - build.gradle.kts: remove duplicate buildFeatures/compileOptions/kotlinOptions blocks introduced by merge; keep Apollo runtime dependency - MainActivity.kt: take main's KDoc comments and explicit R import; use startup_title string name to match main's convention; remove now-redundant explicit R import (ktlint: unnecessary import) - Theme.kt: take main's KDoc comments for DarkColorScheme, LightColorScheme, ForgeTheme - strings.xml: align on startup_title name from main Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Claude <claude@anthropic.com>
Summary
GraphQLContentClientadapter with bearer-token auth via HTTP interceptorapps/cms/schema.graphqlfor type-safe code generationBuilds on #80
Resolves #81
Test plan
./gradlew :app:assembleDebugand confirm BUILD SUCCESSFUL.graphqloperationsGraphQLContentClientcompiles with correct Apollo API usage🤖 Generated with Claude Code
Summary by CodeRabbit
Documentation
New Features
Build Configuration
Chores