Android Project Docs with MkDocs Material, Dokka, and Firebase Hosting
Most Android projects ship without documentation. The few that have it rely on a
Confluence page that is out of date by the time it is merged. This post shows
the setup used in Unizonn Mobile v2: MkDocs Material for the documentation site,
Dokka for generated API reference, Firebase Hosting for deployment, and a single
GitHub Actions workflow that builds and ships everything on every PR targeting
main or staging. The result is a live URL that reviewers can open from a
pull request without cloning anything.
Prerequisites¶
Python 3.12, JDK 17, and a working Android project with the Gradle version
catalog (libs.versions.toml) are assumed. The setup uses
org.jetbrains.dokka 1.9.20 and mkdocs-material (latest pip release).
You will need a Firebase project and a service account with the
Firebase Hosting Admin role.
Versions used in this post
Kotlin 1.9.0 · AGP 8.3.2 · Dokka 1.9.20 · Python 3.12.3 · MkDocs Material (pip latest)
Background¶
Dokka generates HTML from KDoc comments. MkDocs Material turns Markdown into
a polished site. Firebase Hosting serves static assets from a CDN with a free
tier that is more than enough for project documentation. The missing piece is
glue: a Gradle task that builds both, a firebase.json that points at the
right output directory, and a workflow that runs it without manual intervention.
What most setups get wrong
Running mkdocs build and dokkaHtml as separate CI steps without a shared
output directory means the API reference is never served alongside the site.
The buildDocs task below solves this by running both in sequence into the
same html/ folder.
Implementation¶
1. Register Dokka in the version catalog¶
Add Dokka to gradle/libs.versions.toml before touching any build files. This
keeps the version in one place and makes it available to every module.
[versions]
dokka = "1.9.20"
[plugins]
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
2. Apply the plugin at the root and module level¶
The root build.gradle.kts declares the plugin without applying it, then applies
it to every subproject. The apply false pattern prevents the plugin from
generating tasks at the root level where it has nothing to document.
| build.gradle.kts | |
|---|---|
This registers dokkaHtml as a task in every subproject. Running
./gradlew dokkaHtml from the root will generate HTML under each module's
build/dokka/html/ directory.
Multi-module output directory
For a single-module project, dokkaHtml output lands at
app/build/dokka/html/. If you later add modules, switch to
dokkaHtmlMultiModule at the root level and point outputDirectory at
documentation/html/api/. The buildDocs task below handles both just
update the dokkaHtml call to dokkaHtmlMultiModule.
3. Write the MkDocs site scaffold¶
Create a documentation/ directory at the project root. This keeps the site
separate from the Android source tree and gives CI a clean working directory.
documentation/
├── docs/
│ ├── index.md
│ ├── architecture.md
│ ├── guidelines.md
│ └── api.md ← placeholder that redirects to api/index.html
├── mkdocs.yml
└── generate_docs.sh
The mkdocs.yml configures the Material theme with a light/dark palette toggle.
The nav entry for API points to the api/index.html that Dokka will generate
into this directory.
site_dir: html is deliberate. Firebase Hosting will be configured to serve
from documentation/html/, so both MkDocs output and Dokka output
must land there.
4. Write the build script¶
The generate_docs.sh script creates a Python virtual environment, installs
mkdocs-material, and runs mkdocs build. Keep it as a shell script rather
than a Gradle Exec task so it can be run locally without Gradle.
#!/bin/bash
set -ex
python3 -m venv venv
source venv/bin/activate
pip3 install mkdocs-material
mkdocs build
set -ex means the script fails immediately on any error and prints each
command before executing it. Without this, a failed pip install silently
produces an empty html/ directory and the CI step succeeds with nothing deployed.
5. Register the buildDocs Gradle task¶
Back in the root build.gradle.kts, register a buildDocs task that runs
generate_docs.sh first, then runs dokkaHtml. The doLast block runs after
generate_docs.sh exits, so Dokka output is written into the already-built
MkDocs site.
| build.gradle.kts | |
|---|---|
Running ./gradlew buildDocs locally should produce documentation/html/
with the full MkDocs site and documentation/html/api/ with the Dokka output.
Do not commit the html/ directory
Add documentation/html/ and documentation/venv/ to .gitignore.
The build output is regenerated on every CI run. Committing it produces
multi-megabyte diffs and creates merge conflicts that have nothing to do
with content changes.
6. Configure Firebase Hosting¶
Firebase Hosting needs to know where the built site lives. The firebase.json
at the project root points at documentation/html/, which is exactly where
both MkDocs and Dokka write their output.
{
"hosting": {
"public": "documentation/html",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
Run firebase init hosting once to generate the .firebaserc file with your
project ID. Commit .firebaserc but not firebase-debug.log.
7. Write the GitHub Actions workflow¶
The workflow triggers on pull requests targeting main or staging. It sets
up Python, JDK 17, and Gradle, runs ./gradlew buildDocs, then deploys to
Firebase using the official action. The final step notifies a Slack channel
regardless of whether the deploy succeeded or failed.
| .github/workflows/docs.yml | |
|---|---|
| |
channelId: live deploys to the live channel rather than a preview channel.
If you want per-PR preview URLs instead, remove this line the action will
create a temporary channel and post the URL back to the PR as a comment.
Use vars for the project ID, not secrets
PROJECT_ID does not need to be secret it is visible in the Firebase
console and in .firebaserc. GitHub vars (plain variables) are the
right home for it. Reserve secrets for the service account JSON.
8. Store the required secrets¶
In your GitHub repository settings under Settings → Secrets and variables → Actions:
| Name | Type | Value |
|---|---|---|
FIREBASE_SERVICE_ACCOUNT |
Secret | Service account JSON (base64 or raw) |
SLACK_WEBHOOK_URL |
Secret | Incoming webhook URL from Slack |
PROJECT_ID |
Variable | Firebase project ID (e.g. unizonn-mobile-v2) |
The service account needs the Firebase Hosting Admin IAM role. Create it in
the GCP console under the Firebase project, download the JSON key, and paste
the contents into the secret.
Testing¶
Verify the full build locally before pushing. From the project root:
Expected output:
> Task :buildDocs
+ python3 -m venv venv
+ source venv/bin/activate
+ pip3 install mkdocs-material
...
INFO - Building documentation to directory: /path/to/documentation/html
INFO - Documentation built in 2.34 seconds
> Task :app:dokkaHtml
...
BUILD SUCCESSFUL in 38s
Then confirm the directory structure before any Firebase deploy:
If api/ is missing, dokkaHtml did not write to the right location. Check
that DokkaTaskPartial.outputDirectory is not overriding the default path.
Pitfalls¶
generate_docs.sh must be executable
git does not preserve the executable bit across all platforms by default.
Run git update-index --chmod=+x documentation/generate_docs.sh and commit
the result. Without this, the CI Exec task will fail with Permission denied
even though it works locally.
MkDocs site_dir must be relative to mkdocs.yml
site_dir: html in mkdocs.yml resolves relative to the file's location,
which is documentation/. The output lands at documentation/html/.
Setting site_dir: ../html or an absolute path breaks the Firebase Hosting
config, which explicitly points at documentation/html.
Dokka output path when using subprojects {}
Applying Dokka via subprojects { apply(plugin = "org.jetbrains.dokka") }
generates output under each module's own build/dokka/html/ directory, not
the root. The buildDocs task runs dokkaHtml after MkDocs has already
built the site, so Dokka output does not end up inside documentation/html/api/
automatically. Either configure outputDirectory explicitly in each module's
Dokka task, or use dokkaHtmlMultiModule at the root level with a shared
output path.
Production considerations¶
The channelId: live setting means every PR deploy goes straight to the live
URL. For teams where documentation changes go through review, switch to preview
channels and add a separate workflow that promotes to live on merge to main.
The generate_docs.sh script reinstalls mkdocs-material on every run because
it creates a fresh virtual environment each time. Pin the version
(pip3 install mkdocs-material==9.5.x) and cache the virtual environment using
actions/cache keyed on requirements.txt to cut the step from roughly 30
seconds to under 5.
If the documentation site grows beyond a single module, replace dokkaHtml with
dokkaHtmlMultiModule and configure each module's DokkaTaskPartial with an
outputDirectory that writes into documentation/html/api/. The Firebase config
and workflow do not need to change.
Wrapping up¶
The result is a ./gradlew buildDocs command that builds the full documentation
site and a GitHub Actions workflow that ships it to a live URL on every PR no
manual deploys, no stale Confluence pages.
The next thing to add is versioned docs: a separate Firebase Hosting channel per
release tag, so older API reference stays accessible after upgrades. That is a
firebase hosting:channel:deploy v1.0 call away from the same workflow.