Some level of documentation is necessary in software projects.
We like to say that “the code should speak for itself” and “docs always get outdated anyway”. Both of these arguments are valid, but they should not be an excuse to drop documentation altogether.
Having some documentation in place both:
A) makes it easier to onboard new colleagues,
B) gives you checklists for critical tasks, and
C) makes it easier to get back up to speed again if you’ve been away from the project.
At a minimum, I expect to see the following points covered in Android README files:
Project setup
How do I get started with the project? Which steps are needed to build and run it from a cold start?
Ideally, this section simply states something like this:
Clone the repo, open the project in Android Studio, hit "Run". Done!
If setup is more complicated, you should state exactly which steps are necessary to get started. What software do I need to install? Which specific bash commands do I need to run? Be specific.
Architecture overview
You should provide a high level overview of the project.
Which major components are present, what are the main moving parts of the app? How does data flow between UI and the domain model? How is state and persistence handled? Are there major tradeoffs and constraints I should be aware of?
Yes, you will figure this out eventually after spending time in the code. It really helps to have a high level overview up front, though.
Describing the project in this manner is a useful exercise for the writer as well. If you find it hard to summarize the architecture in a few paragraphs, it might mean your project is messy and unstructured.
Here’s how this section can look in a real project. This is taken from a client I’m working with (in this case, a bike sharing app):
The app is a stateless client: all operations/mutations are performed
by calling api endpoints over the network.
Local data is in effect immutable, the client just downloads updated
versions of data as needed. Local data is only modified as a result of
api operations.
The domain model objects are used throughout the app. They are plain
Kotlin/Java objects. They should not be directly tied to persistence
details, neither should they be directly tied to network api
details. The persistence/network layer translates to and from the
local domain model as needed, the rest of the app should not have to
know about those implementation details.
State is stored in a simple DataStore called GlobalState. It loads
from disk on app startup, persists itself to disk as needed in
background. While app is active, all data is kept in memory. The rest
of the app simply puts domain model objects into and out of it without
knowing anything about persistence details.
The polling module synchs app state with the backend when app comes to
foreground. Bike availability data is refreshed every 10 seconds while
in foreground. If user is on an unlocked bike ride, the trip state is
refreshed every other second. When app is in background, no network
or background processing takes place.
The polling module signals data/state changes by emitting events on an
event bus. The events carry no state, they only serve as
signals. Presenters objects listen to this event bus for relevant
events and pull immutable, updated data as needed from the datastore
described above, then tells the UI to update accordingly.
Activities and fragments are for presentation logic only. Each
activity or fragment should have its own presenter where business
logic is placed. The presenter reacts to data changes via the event
bus, and tells the fragment/activity how to update itself.
TODO: this works, but we should replace the eventbus with LiveData
and turn the presenters into ViewModels: these official Android
Architecture components mean fewer 3rd party dependencies, and would
let us remove all the bus.subscribe/unsubscribe calls in our
activity/lifecycle methods.
External dependencies
Which integration points does the app have with the rest of your project, and with the outside world? Very few apps exist as a standalone artifact. It could look something like this:
The app does not live in a vacuum. These are the external
APIs/services it depends on in fully rolled out system:
- Our core API (graphql based). Endpoints defined for prod/staging in
PLATFORM_API_BASE_URL_PROD and PLATFORM_API_BASE_URL_STAGING in
the gradle buildfile.
- The Mapbox SDK for rendering the map screen. Grep for
"mapbox_access_token" for the token which all the app flavors share
in the mapbox sdk init.
- Firebase for analytics, crash reports and cloud messaging. All our
apps share a single Firebase project, see
https://console.firebase.google.com/u/2/project/myproject
It's configured clientside for all the app flavors in
"google-services.json" in this repo. Update this each time a new
system/app flavor is added.
- Intercom SDK for customer service (bidirectional: users can contact
custex from the profile screen, and custex can push out
notifications inside the app via the Intercom SDK.) Each system/app
flavor defines its INTERCOM_API_KEY/INTERCOM_APP_ID in the gradle
buildfile.
- Bitrise for CI/build pipeline. Not configured inside the app, but we
download a backup of the serverside bitrise config occasionally for
disaster recovery, see "bitrise.yml" in the root dir. Our CI
workflows are defined and updated here:
https://app.bitrise.io/app/12345678
- Google Play ecosystem. See below for CI/release routines. We don't
publish the apps in any other Android ecosystems/stores ATM.
Version control workflow
All serious projects use source control — however, it’s beneficial to agree on a workflow, since git is a very flexible tool.
Describing this can be simple as the following:
We loosely use the "Git flow" approach: master is the release
branch - it should always be releasable, and only merged into
when we have tested and verified that everything works and is
good to go.
Daily development is done in the development branch. Features,
bugfixes and other tasks are done as branches off of develop,
then merged back into develop directly or via pull requests.
Keep commits atomic and self-explanatory, use rebase to clean
up messy branches before merging back into develop.
How to test the project
How do I verify that everything works?
Where are the automated tests? Any special things I should know about running them?
Few Android projects have full coverage of automated UI tests. Therefore, you will probably need to have a manual test process as well.
Describe that process, so the project is regression tested in a consistent way before each release. Be specific, describe each step.
This is an excerpt from a simple manual test script:
Test: Sign in existing user with active subscription
Test: Sign in existing user, buy product during onboarding to get new subscription
Test: Sign in existing user, redeem gift card during onboarding to get new subscription
Test: Sign up new user, product purchase during onboarding to get new subscription
Test: Sign up new user, gift card redeeming during onboarding to get new subscription
Test: With signed in user, buy product from profile page to get new subscription
Test: With signed in user, redeem gift from profile page to get new subscription
Note: this is very bare-bones. Ideally you’d want a checklist inside each test, of exactly what to verify, so you cover most each edge cases and code paths every time.
How to release a new version
How is the app built, signed and uploaded to Google Play? How are releases tagged for posterity? Where do I find the keystore and credentials so I’m able to create signed builds?
Again, be specific. The main reason I want these steps listed the README file is to avoid having to think hard about them during an important hotfix.
Here’s an example:
- In develop, update the major/minor/patch version,
increment the version code integer by one. Commit.
- Merge develop into master.
- Tag it:
git tag -a v3.0.19 -m "Fixed a tricky bug"
git push origin --tags
- This should trigger a release hook in Github. Wait until the
Bitrise CI system reacts, builds and uploads the signed apk file
to the internal test track in Google Play.
NOTE: this will only work if you push a *single* tag upstream,
or Github won't trigger the hook
(https://blog.bitrise.io/trigger-builds-with-git-tags).
- In Google Play, find the new version in the internal test track
- Add localized release notes for the new version
- Promote it to production: staged rollout to 10% of users.
- Two days later: roll out to all users if crash reports are stable
More of this could be automated. However, even if you completely automate the release process, the README file should still describe how to trigger that process and what it does.
Gotchas and special routines
Which processes, workflows and snags should I know about?
What project trivia is essential but easily forgotten?
Are there quirks with tools or dependencies that we need to remember?
Anything like this should show up in the README file. And again, be specific here.
For example, in a current project we use the Apollo library to consume a strongly typed GraphQl API. There are some manual steps we take from time to time.
We document that:
We use Apollo, which creates clientside stub classes to give us
type-safe api operations. Apollo generates these types
(via gradle plugin) during our builds, based on .graphql files
and a schema.json file.
The backend exposes its api schema. The android project needs a fresh
download of this schema in version control if we are consuming
new queries, mutations or fields.
We have automated the schema download.
Run the update-apollo-{local|staging|prod}.sh scripts to refresh
the schema from each of our environments. Example:
./update-apollo-prod-schema.sh
This downloads the latest schema from that env, and moves it
to the correct folder in the project.
Apollo should pick up the changes during gradle build and will
generate *Query stub classes based on any .graphql and .json schema
files present in ./app/src/main/graphql/**/*
Commit the latest schema changes for prod if there is git diff
in the schema and everything builds and tests ok.
Take your vitamins! Document your software!