-
Notifications
You must be signed in to change notification settings - Fork 5
Tool Renderers
This page explains how GodTools turns downloaded tool content (XML manifests plus resource files) into on-screen UI. It covers what a "tool" is, the shared godtools-shared parser/renderer dependency, the common infrastructure in ui/base-tool, and each of the seven renderer modules, plus the small shortcuts and qr-code feature modules. For how content gets onto the device in the first place, see Sync & Downloads; for the app-level navigation that launches these renderers, see UI Architecture.
A tool is a piece of ministry content published by the mobile-content-api backend (see API Layer). The client-side model is Tool in library/model/src/main/kotlin/org/cru/godtools/model/Tool.kt, with a Tool.Type enum that determines which renderer opens it:
Tool.Type |
JSON:API resource-type
|
Rendered by | Notes |
|---|---|---|---|
TRACT |
tract |
ui/tract-renderer |
Paged gospel presentations; supports parallel language and live share |
LESSON |
lesson |
ui/lesson-renderer |
Swipeable lessons with progress/resume and feedback |
CYOA |
cyoa |
ui/cyoa-renderer |
"Choose your own adventure" branching page navigation; supports parallel language |
ARTICLE |
article |
ui/article-renderer + ui/article-aem-renderer
|
Category/article lists backed by AEM-hosted HTML |
META |
metatool |
— | Groups tool variants; never rendered directly |
UNKNOWN |
— | — | Fallback for unrecognized types |
Tool.Type.supportsParallelLanguage is true only for TRACT and CYOA (Tool.kt line 154).
Each tool has per-language Translations (library/model/src/main/kotlin/org/cru/godtools/model/Translation.kt); a downloaded translation consists of a manifest XML file (Translation.manifestFileName) plus the resource files (images, Lottie animations, page XML) the manifest references.
Parsing and (increasingly) rendering are not implemented in this repo. They come from the Kotlin Multiplatform godtools-shared artifacts, declared in gradle/libs.versions.toml:
| Catalog alias | Maven module | Purpose |
|---|---|---|
godtoolsShared-parser |
org.cru.godtools.kotlin:parser |
Parses manifest/page XML into a Manifest object model (org.cru.godtools.shared.tool.parser.*) |
godtoolsShared-renderer |
org.cru.godtools.kotlin:renderer |
Multiplatform Compose renderer (RenderLesson, RenderTractHero, RenderContentStack, State, ProvideRendererServices) |
godtoolsShared-common |
org.cru.godtools.kotlin:common |
Shared common code |
godtoolsShared-analytics |
org.cru.godtools.kotlin:analytics |
Shared analytics constants |
godtoolsShared-user-activity |
org.cru.godtools.kotlin:user-activity |
User activity tracking |
The version is pinned by the godtoolsShared entry in gradle/libs.versions.toml (currently 1.4.0-SNAPSHOT), resolved from https://cruglobal.jfrog.io/artifactory/maven-mobile/ (configured in settings.gradle.kts, which also adds a repo scoped to the transitive deezer.kustomexport annotation dependency).
ui/base-tool/build.gradle.kts exposes the parser and renderer as api(...) dependencies, so every renderer module that depends on :ui:base-tool gets them transitively.
Both first-party -SNAPSHOT dependency families are developed in separate CruGlobal repositories and published to https://cruglobal.jfrog.io/artifactory/maven-mobile/:
| Artifacts | Source repository | Notes |
|---|---|---|
org.cru.godtools.kotlin:* (godtools-shared, 1.4.0-SNAPSHOT) |
CruGlobal/kotlin-mpp-godtools-tool-parser | Kotlin Multiplatform; its module:parser, module:renderer, module:common, module:analytics, and module:user-activity Gradle modules publish the artifacts in the table above (root build.gradle.kts sets group = "org.cru.godtools.kotlin") |
org.ccci.gto.android:* (gto-support, 4.6.0-SNAPSHOT) |
CruGlobal/android-gto-support | One Gradle module per artifact (gto-support-db, gto-support-jsonapi, gto-support-circuit, …); used far beyond the renderers — the session interceptor and JSON:API converter (API Layer), Room type converters (Data Layer), and test utilities (Testing) all come from it |
Non-release builds of each repo's default branch publish under the fixed -SNAPSHOT version, so the code behind RenderLesson, ManifestParser, SessionInterceptor, etc. can change without any commit in this repo. So a bug in shared code is fixed upstream: clone the source repo above, fix it there, and iterate against this app with the local loop below.
Which snapshot build am I actually on? Each -SNAPSHOT resolves to a timestamped unique build. The currently-published build is listed in Artifactory's metadata, e.g. https://cruglobal.jfrog.io/artifactory/maven-mobile/org/cru/godtools/kotlin/parser/1.4.0-SNAPSHOT/maven-metadata.xml — the <snapshot> block gives the <timestamp> and <buildNumber> (unique versions look like 1.4.0-20260810.234026-48), which tells you when it was published and therefore which upstream default-branch commits it contains. Gradle caches changing modules for 24 hours by default; ./gradlew --refresh-dependencies forces re-resolution (see Getting Started).
Local development loop — to run this app against a local build of a shared library:
-
Clone the upstream repo and make your change there.
-
Publish it locally with the same coordinates:
./gradlew publishToMavenLocal(both repos publish viamaven-publish; non-release builds get the-SNAPSHOTversion automatically, and their base versions ingradle.properties—1.4.0/4.6.0— match what this repo consumes). -
In this repo, add
mavenLocal()before the Artifactory repo in thedependencyResolutionManagement.repositoriesblock ofsettings.gradle.kts, scoped so only the shared groups resolve locally — and do not commit this change:mavenLocal { content { includeGroup("org.ccci.gto.android") includeGroup("org.cru.godtools.kotlin") } } -
Rebuild. Gradle searches repositories in declaration order and never caches local repositories, so each
publishToMavenLocalis picked up by the next build. Repeat step 2 after every upstream change; remove themavenLocal()block when done. (A Gradle composite build —includeBuild(...)with dependency substitutions — is an alternative, but thepublishToMavenLocalloop matches how the artifacts are actually consumed.)
A change spanning this repo and a shared library must land upstream first: merge the shared-library PR, wait for its CI to publish the new snapshot to Artifactory (confirm via the maven-metadata.xml above), then build here with --refresh-dependencies before merging the dependent change.
The pipeline from network to pixels:
flowchart TD
CDN["Mobile Content CDN<br/>CdnApi: GET translations/files/{filename}"]
API["mobile-content-api<br/>TranslationsApi: GET translation zip"]
DM["GodToolsDownloadManager<br/>library/download-manager"]
FS["ToolFileSystem<br/>filesDir/resources"]
MM["ManifestManager<br/>ui/base-tool"]
DB["TranslationsRepository<br/>library/db (Room)"]
PARSER["ManifestParser<br/>godtools-shared parser"]
MANIFEST["Manifest object model"]
STATE["ToolStateHolder → shared renderer State"]
RFS["chrooted okio FileSystem<br/>TOOL_RESOURCE_FILE_SYSTEM"]
CDN -->|"per-file download"| DM
API -->|"zip fallback"| DM
DM -->|"manifest XML + page XML,<br/>images, animations"| FS
FS -->|"openInputStream"| MM
MM --> PARSER
PARSER -->|"ParserResult.Data"| MANIFEST
PARSER -->|"ParserResult.Error"| MM
MM -->|"Corrupted / NotFound:<br/>mark not downloaded"| DB
DB -.->|"Dispatcher re-downloads"| DM
MANIFEST --> LESSON
MANIFEST --> TRACT
MANIFEST --> CYOA
MANIFEST --> TIPS
MANIFEST --> ARTLIST
subgraph compose["Compose UI via godtools-shared renderer"]
LESSON["lesson-renderer<br/>RenderLesson"]
TRACT["tract-renderer<br/>DataBinding controllers + RenderTractHero"]
CYOA["cyoa-renderer<br/>fragments + RenderContentStack"]
TIPS["tips-renderer<br/>bottom sheet + RenderContentStack"]
ARTLIST["article-renderer<br/>fragment-hosted Compose lists +<br/>RenderArticleCategory"]
end
AEM["article-aem-renderer<br/>AEM WebView"]
ARTLIST -->|"article selected"| AEM
STATE --> LESSON & TRACT & CYOA & TIPS
RFS -.->|"ProvideRendererServices"| compose
Step by step:
-
Download —
GodToolsDownloadManager(library/download-manager/src/main/kotlin/org/cru/godtools/downloadmanager/GodToolsDownloadManager.kt) downloads a translation either file-by-file (manifest first, then everymanifest.relatedFilesentry, preferring the CDN viaCdnApi.downloadPublishedFilewithTranslationsApi.downloadFileas fallback) or as a single zip (TranslationsApi.download(translation.id), extracted inextractZipFor). Files land inToolFileSystem(library/base/src/main/kotlin/org/cru/godtools/base/ToolFileSystem.kt), a wrapper aroundfilesDir/resources. Successful downloads are recorded per file and the translation is marked downloaded. See Sync & Downloads for the triggering flows (favorite tools, language changes, etc.). -
Parse —
ManifestManager(ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/service/ManifestManager.kt) asks the sharedManifestParserto parse the manifest file. Parsing runs onDispatchers.IO.limitedParallelism(8), is deduplicated with a per-fileMutexMap, and results are cached in aWeakLruCacheof size 6. The parser reads files through anAndroidXmlPullParserFactorywhoseopenFiledelegates toToolFileSystem.openInputStream(BaseToolRendererModule.kt). -
Self-heal — if parsing returns
ParserResult.Error.CorruptedorNotFound,ManifestManager.getManifestcallstranslationsRepository.markBrokenManifestNotDownloaded(...), and the download pipeline re-downloads the broken translation instead of surfacing an error: theGodToolsDownloadManager.Dispatcherobserves not-downloaded translations and re-fetches them in the background, andBaseToolActivitycallsdownloadLatestPublishedTranslationAsyncwhile the tool is open. (Sync only fetches JSON:API metadata — it never downloads translation files.) -
State —
ToolStateHolder(ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/viewmodel/ToolStateHolder.kt) is a@HiltViewModelthat keeps the shared rendererorg.cru.godtools.shared.renderer.state.StateinSavedStateHandleand (temporarily, per the TODO in the file) pipesState.contentEventsinto greenrobot EventBusEvents so legacy controllers can react to content events. -
Render — each renderer wraps shared-renderer composables in
ProvideRendererServices(resources = resourceFileSystem, tipsRepository = tipsRepository), whereresourceFileSystemis the@Named(TOOL_RESOURCE_FILE_SYSTEM)read-only okioFileSystemchrooted toToolFileSystem.rootDir()(BaseToolRendererModule.kt). That is how shared composables resolve image and animation files by name.
BaseToolRendererModule (ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/BaseToolRendererModule.kt) provides the ParserConfig with the app version (DeviceType.ANDROID) and the supported content features FEATURE_ANIMATION, FEATURE_CONTENT_CARD, FEATURE_FLOW, and FEATURE_MULTISELECT. FEATURE_PAGE_COLLECTION is added only when the Firebase Remote Config boolean CONFIG_TOOL_CONTENT_FEATURE_PAGE_COLLECTION is enabled — content using page collections will not render on devices where that flag is off.
ui/base-tool (namespace org.cru.godtools.tool) is the module every renderer builds on. Besides the DI providers above, it contains:
BaseBindingActivity "ui/base"
└── BaseToolActivity<B> "ui/base-tool/.../activity/BaseToolActivity.kt"
├── BaseSingleToolActivity single locale: lesson, article
│ └── BaseArticleActivity article-specific base
└── MultiLanguageToolActivity primary + parallel locales: tract, cyoa
BaseToolActivity (ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/activity/BaseToolActivity.kt):
- Injects
GodToolsDownloadManager,ToolsRepository, a@Named(IS_CONNECTED_LIVE_DATA)connectivityLiveData, andFollowupService(injected solely so the followup-form capture service is running). - Calls
processIntent(...)inonCreateand immediatelyfinish()es whenisValidStartStateis false — subclasses must checkisFinishingaftersuper.onCreate(...). - Computes a
LoadingStateper tool/locale:LOADING,LOADED,NOT_FOUND,INVALID_TYPE,OFFLINE(enum atBaseToolActivity.ktline 225) — every renderer shows distinct UI for these. - Handles tool sync, the share menu, status-bar coloring from the manifest's
navBarColor, and feature discovery (theTapTargetViewshare-menu call-out — see UI Architecture).
MultiLanguageToolActivity (ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/activity/MultiLanguageToolActivity.kt) adds the primary/parallel language handling for tract and CYOA: locales come from the EXTRA_LANGUAGES intent extra, a TabLayout language toggle is driven by LanguageToggleController, and tool settings appear in SettingsBottomSheetDialogFragment (ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/ui/settings/).
Renderer modules do not depend on each other, so activities are started by string class name. ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/Activities.kt defines ACTIVITY_CLASS_LESSON = "org.cru.godtools.tool.lesson.ui.LessonActivity", and ui/base/src/main/kotlin/org/cru/godtools/base/ui/Activities.kt defines the class-name constants and create*Intent/start*Activity helpers for the dashboard, articles, CYOA, tract, and QR-code activities. Renaming or moving any of these activities silently breaks launches — the compiler cannot catch it.
BaseController<T : Base> (ui/base-tool/src/main/kotlin/org/cru/godtools/base/tool/ui/controller/BaseController.kt) is the View/DataBinding-era rendering abstraction still used by tract, CYOA, and tips: a tree of controllers bound to parser model nodes, resolving EventBus, LifecycleOwner, and tool State up the parent chain, and handling delayed analytics events, RTL layout, and tip completion.
| Module | Entry point | Rendering approach |
|---|---|---|
ui/lesson-renderer |
LessonActivity |
Fully Compose via shared RenderLesson
|
ui/tract-renderer |
TractActivity |
ViewPager + DataBinding controllers with embedded Compose (RenderTractHero, ModalOverlay) |
ui/cyoa-renderer |
CyoaActivity |
Fragment backstack + DataBinding controllers with embedded Compose (RenderContentStack) |
ui/article-renderer |
ArticlesActivity |
Fragments hosting Compose lists: shared RenderArticleCategory for categories, Material3 ListItems for articles |
ui/article-aem-renderer |
AemArticleActivity |
WebView serving cached AEM HTML |
ui/tips-renderer |
TipBottomSheetDialogFragment |
Bottom sheet + RenderContentStack
|
ui/tutorial-renderer |
TutorialScreen (Circuit) |
Pure Compose + Circuit; renders app tutorials, not tool content |
The asymmetry is intentional and transitional: lessons are the fully-migrated shared-Compose path, tract/CYOA/tips embed shared composables inside legacy DataBinding controllers, article list screens are fragment-hosted Compose, and AEM article content is WebView-based. Do not copy the tract controller pattern for new work.
LessonActivity (ui/lesson-renderer/src/main/kotlin/org/cru/godtools/tool/lesson/ui/LessonActivity.kt) extends BaseSingleToolActivity with supportedType = Manifest.Type.LESSON. It maps the base LoadingState to a LessonScreen.UiState (Offline / Missing / Loading with download progress / Loaded) and renders everything with the shared RenderLesson composable inside ProvideRendererServices(...), GodToolsTheme(darkTheme = false), and Circuit's ContentWithOverlays. Circuit overlays provide:
-
Resume progress —
LessonResumeDialogOverlay, driven by theEXTRA_RESUME_PAGEextra; page progress is persisted viaToolsRepository.updateToolProgress. -
Swipe tutorial —
LessonSwipeTutorialAnimatedModalOverlay, gated by remote configCONFIG_TUTORIAL_LESSON_PAGE_SWIPEand theFEATURE_LESSON_PAGE_SWIPEDfeature-discovery flag. -
Feedback —
LessonFeedbackDialogOverlay, shown on close once the user has passed page 3 (seeLessonActivityDataModel.showFeedback).
Share links are built on URI_SHARE_BASE (https://knowgod.com/, defined in library/base/src/main/kotlin/org/cru/godtools/base/Constants.kt) as /{locale}/lesson/{tool}[/{page}].
TractActivity (ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/activity/TractActivity.kt) extends MultiLanguageToolActivity<TractActivityBinding>. Pages render through ManifestPagerAdapter (ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/adapter/ManifestPagerAdapter.kt, a DataBindingPagerAdapter<TractPageBinding>) and the PageController/CardController tree in ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/ui/controller/. Compose is embedded at two points:
-
PageControllerrenders the page hero with the sharedRenderTractHerocomposable inside the binding. -
TractActivity.setupComposeOverlay()hosts tract modals via the Circuit overlayModalOverlay(ui/tract-renderer/src/main/kotlin/org/cru/godtools/tool/tract/ui/ModalOverlay.kt).
Tract also supports:
-
Live share —
TractPublisherController/TractSubscriberController(ui/tract-renderer/src/main/kotlin/org/cru/godtools/tract/liveshare/) mirror navigation between devices over a Scarlet ActionCable WebSocket. TheTractShareServiceinterface lives inlibrary/api/src/main/kotlin/org/cru/godtools/api/TractShareService.kt(channelsPublishChannel/SubscribeChannel, paramchannelId); Scarlet connects to"${apiConfig.mobileContentApiUrl}cable"(library/api/src/main/kotlin/org/cru/godtools/api/ApiModule.kt). Subscriber deep links carry theliveShareStreamparameter. The live-share tutorial is launched viaTutorialScreenResultContract. The full publisher/subscriber handshake — including thePublisherInfo→ share-link exchange and the re-subscribe-on-reconnect loop — is diagrammed in API Layer. -
Google Instant Apps —
TractActivityshows an install menu item and adjusted navigation when running as an instant app (InstantApps.isInstantApp). -
Tips — implements
TipBottomSheetDialogFragment.Callbacks(see below).
CyoaActivity (ui/cyoa-renderer/src/main/kotlin/org/cru/godtools/tool/cyoa/ui/CyoaActivity.kt) extends MultiLanguageToolActivity and manages a fragment backstack of page fragments: CyoaContentPageFragment, CyoaCardCollectionPageFragment, and CyoaPageCollectionPageFragment. Navigation between pages is driven by content events (showPage(...)), with handling for dismissed/invalid pages and parent-page up-navigation. Page content is rendered by controllers in ui/cyoa-renderer/src/main/kotlin/org/cru/godtools/tool/cyoa/ui/controller/ — ContentPageController and CardCollectionPageController call the shared RenderContentStack composable inside binding.compose.setContent { ProvideRendererServices(...) }.
ArticlesActivity (ui/article-renderer/src/main/kotlin/org/cru/godtools/article/ui/ArticlesActivity.kt) extends BaseArticleActivity and reuses the generic tool_generic_fragment_activity.xml layout from ui/base-tool. It hosts CategoriesFragment (manifest categories) and ArticlesFragment (articles in a category); selecting an article starts AemArticleActivity from the AEM module. Both fragments return a ComposeView from onCreateView: CategoriesFragment renders each manifest category with the shared RenderArticleCategory composable inside ProvideRendererServices(resourceFileSystem), while ArticlesFragment renders plain Material3 ListItems in a LazyColumn (wrapped in a PullToRefreshBox that forces an AEM re-sync), colored from the manifest's primary/background colors. Only the article content itself — AemArticleActivity's WebView — is non-Compose.
This module handles the actual article content, which lives in Adobe Experience Manager (AEM) rather than in tool XML — it is a parallel content pipeline:
-
Own Room database —
ui/article-aem-renderer/src/main/kotlin/org/cru/godtools/article/aem/db/ArticleRoomDatabase.ktwith modelsAemImport,Article,Resource,TranslationRef(separate from the main app DB described in Data Layer). -
Sync —
AemArticleManager(ui/article-aem-renderer/src/main/kotlin/org/cru/godtools/article/aem/service/AemArticleManager.kt) watches downloaded article translations, readsmanifest.aemImportsURIs, fetches AEM JSON, then downloads each article's HTML (api.downloadArticle(article.uri.addExtension("html"))) and its referenced resources intoAemFileSystem(filesDir/aem-resources,ui/article-aem-renderer/src/main/kotlin/org/cru/godtools/article/aem/util/AemFileSystem.kt). Its innerDispatcheris started eagerly via the module's@EagerSingletonwiring inAemArticleRendererModule.kt. -
API —
AemApi(ui/article-aem-renderer/src/main/kotlin/org/cru/godtools/article/aem/api/AemApi.kt) is a Retrofit interface using per-call@Urlparameters; the RetrofitbaseUrlis the literal placeholder"https://unused.example.com"(AemArticleRendererModule.kt) because real hosts come from the AEM import URIs in each manifest. -
Rendering —
AemArticleActivity/AemArticleFragmentdisplay a WebView;ArticleWebViewClient(ui/article-aem-renderer/src/main/kotlin/org/cru/godtools/article/aem/ui/ArticleWebViewClient.kt) overridesshouldInterceptRequestto serve cached resources from disk, so previously-synced articles work offline.
The full pipeline, from tool manifest to WebView:
flowchart TD
subgraph dispatcher["AemArticleManager.Dispatcher — @EagerSingleton (AemArticleRendererModule.kt)"]
TOOLS["ToolsRepository.getNormalToolsFlow()<br/>filter Tool.Type.ARTICLE"]
TRANS["TranslationsRepository<br/>downloaded translations"]
TOOLS --> TRANS
end
TRANS -->|"processDownloadedTranslations"| MANIFEST["ManifestManager.getManifest(translation)<br/>manifest.aemImports"]
MANIFEST -->|"addAemImports"| IMPORTS[("AemImport +<br/>TranslationRef rows")]
IMPORTS -->|"syncAemImport<br/>(stale or forced)"| JSON["AemApi.getJson<br/>GET {aemImport}.9999.json?_={ts}"]
JSON -->|"findAemArticles<br/>(AemJsonParser.kt)"| ARTICLES[("Article rows")]
ARTICLES -->|"downloadArticle<br/>(uuid != contentUuid)"| HTML["AemApi.downloadArticle<br/>GET {article}.html"]
HTML -->|"updateContent +<br/>extractResources (HtmlParser.kt:<br/>stylesheet links + img srcs)"| RESOURCES[("Resource rows")]
RESOURCES -->|"downloadResource"| DOWNLOAD["AemApi.downloadResource"]
DOWNLOAD -->|"FileManager.storeResponse<br/>SHA-1 dedup"| FILES["AemFileSystem<br/>filesDir/aem-resources"]
subgraph db["ArticleRoomDatabase — separate Room DB"]
IMPORTS
ARTICLES
RESOURCES
end
WV["AemArticleActivity / AemArticleFragment WebView<br/>loadDataWithBaseURL(Article.content)"]
WVC["ArticleWebViewClient<br/>shouldInterceptRequest"]
ARTICLES -.->|"ArticleDao.findLiveData<br/>(AemArticleViewModel)"| WV
WV -->|"css / img requests"| WVC
WVC -->|"ResourceDao.find(uri)"| RESOURCES
WVC -->|"serve cached file<br/>(404 if unknown)"| FILES
WVC -.->|"not yet downloaded:<br/>downloadResource on demand"| DOWNLOAD
Not shown: the Dispatcher also re-syncs stale AemImports at startup and runs a conflated cleanup actor that deletes orphaned cache files whenever the Resource table changes, and article deep links bypass the manifest path via AemArticleManager.downloadDeeplinkedArticle(uri). The prose version of this flow lives in Services & Integrations.
Training tips are supplemental teaching content attached to tract/CYOA pages. TipBottomSheetDialogFragment (ui/tips-renderer/src/main/kotlin/org/cru/godtools/tool/tips/ui/TipBottomSheetDialogFragment.kt) shows a tip in a bottom sheet, paging through tip pages with TipPageAdapter; TipPageController renders each page's content with the shared RenderContentStack composable. Tip completion is persisted through the TipsRepository bridge that BaseToolRendererModule builds over the Room-backed TrainingTipsRepository. ToggleTipsSettingsAction (ui/tips-renderer/src/main/kotlin/org/cru/godtools/tool/tips/ui/settings/ToggleTipsSettingsAction.kt) plugs the tips on/off toggle into the tool settings bottom sheet.
Unlike the others, this module renders app tutorials, not downloaded tool content. It is pure Compose + Circuit (it is one of only three modules with Circuit codegen enabled — see UI Architecture):
-
TutorialScreen(pageSet: PageSet)(ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/layout/TutorialScreen.kt) withPopResultsCanceled,Finished, andShowQrCode. -
PageSet(ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/PageSet.kt):FEATURES,LIVE_SHARE,LIVE_SHARE_START_PAGE_ONLY,TIPS. -
TutorialScreenResultContract(ui/tutorial-renderer/src/main/kotlin/org/cru/godtools/tutorial/TutorialScreenResultContract.kt) wrapsCircuitActivityas anActivityResultContract<PageSet, TutorialScreen.Result?>— the standard pattern for getting a Circuit result back into a non-Circuit activity (used byTractActivityfor the live-share tutorial).
No UI of its own. GodToolsShortcutManager (ui/shortcuts/src/main/kotlin/org/cru/godtools/shortcuts/GodToolsShortcutManager.kt) maintains dynamic and pinned launcher shortcuts for tools of type ARTICLE, CYOA, and TRACT only (SUPPORTED_TOOL_TYPES at line 62), building intents with the create*Intent helpers plus a SHORTCUT_LAUNCH extra. It is disabled in instant apps and reports shortcut usage when a ToolUsedEvent fires. UpdateShortcutsWorker (a @HiltWorker) refreshes shortcuts as unique WorkManager work, and LocaleUpdateBroadcastReceiver refreshes them when the device locale changes.
A single pure-Compose screen: QRCodeActivity (ui/qr-code/src/main/kotlin/org/cru/godtools/qrcode/activity/QRCodeActivity.kt) is a plain ComponentActivity that reads EXTRA_SHARE_URL, generates a 250×250 QR bitmap with ZXing's QRCodeWriter, and finishes on any tap. It is launched from the tract share flow and from the tutorial's ShowQrCode result. This module has Paparazzi snapshot tests (ui/qr-code/src/test) — see Testing.
# Run unit tests for a specific renderer module
./gradlew :ui:base-tool:test
./gradlew :ui:tract-renderer:test
./gradlew :ui:lesson-renderer:test
# Code style checks (required before committing)
./gradlew :build-logic:ktlintCheck ktlintCheck
# Verify Paparazzi snapshots (requires Git LFS)
./gradlew verifyPaparazzi-
Tool activities
finish()inonCreateon invalid intents. Any code aftersuper.onCreate(...)in a renderer activity must bail out whenisFinishingis true (seeTractActivity.kt,LessonActivity.kt,ArticlesActivity.kt). -
Manifest parse failures self-heal silently. A corrupted manifest marks the translation not-downloaded (
ManifestManager.kt), and theGodToolsDownloadManagerdownload pipeline — not sync — re-downloads it; you will not see a visible error. -
@Named(TOOL_RESOURCE_FILE_SYSTEM)does disk I/O at injection time — the provider callsrunBlocking { fileSystem.rootDir() }(BaseToolRendererModule.ktline 87). -
EventBus is still load-bearing. Content events, analytics, and controller communication flow through greenrobot EventBus;
ui/base-toolandui/tract-renderergenerate EventBus subscriber indexes viacreateEventBusIndex(...)in theirbuild.gradle.kts, andToolStateHolderbridges shared-renderer content events into it. -
EXTRA_LANGUAGESmust stay in single-string locale-array mode (ui/base/src/main/kotlin/org/cru/godtools/base/ui/Activities.kt) or legacy pinned shortcuts with primary+parallel languages break. -
Dark theme is intentionally disabled inside tool renderers —
LessonActivityandTractActivitywrap shared-renderer content inGodToolsTheme(darkTheme = false)because tool content defines its own colors. -
FEATURE_PAGE_COLLECTIONis remote-config gated — page-collection content silently fails to render whenCONFIG_TOOL_CONTENT_FEATURE_PAGE_COLLECTIONis off.
- Architecture Overview — where these modules sit in the overall module graph
-
UI Architecture — Circuit,
GodToolsTheme, dashboard navigation into renderers - Sync & Downloads — how translations get downloaded and pruned
-
Data Layer —
Translation/Toolpersistence and repositories -
API Layer —
TranslationsApi,CdnApi, and the live-share WebSocket - Testing — unit tests and Paparazzi snapshots for UI modules
Do not edit this page from the GitHub Wiki. It is generated from the wiki/ directory of CruGlobal/godtools-android, which publish-wiki.yml mirrors here on every push to develop that touches wiki/ — the sync deletes anything that directory does not contain, so changes made with the Edit or New page buttons above are overwritten on the next run. Edit the source and open a pull request against develop instead.
Getting going
Architecture
- Architecture Overview
- Services & Integrations
- API Layer
- Data Layer
- Sync & Downloads
- UI Architecture
- Tool Renderers
Tooling