- Introduction
- Happy coding!
This doc is a quick introduction about the project and its architecture.
Its aim is to help new developers to understand the overall project and where to start developing.
Other useful documentation:
- all the docs in this folder!
- the contributing doc, that you should also read carefully.
Matrix website: matrix.org, discover page. Note: Matrix.org is also hosting a homeserver (.well-known file). The reference homeserver (this is how Matrix servers are called) implementation is Synapse. But other implementations exist. The Matrix specification is here to ensure that any Matrix client, such as Element Android and its SDK can talk to any Matrix server.
Have a quick look to the client-server API documentation: Client-server documentation. Other network API exist, the list is here: (https://spec.matrix.org/latest/)
Matrix is an open source protocol. Change are possible and are tracked using this GitHub repository. Changes to the protocol are called MSC: Matrix Spec Change. These are PullRequest to this project.
Matrix object are Json data. Unstable prefixes must be used for Json keys when the MSC is not merged (i.e. accepted).
There are many object and data in the Matrix worlds. Let's focus on the most important and used, Room and Event
Room is a place which contains ordered Events. They are identified with their room_id. Nearly all the data are stored in rooms, and shared using
homeserver to all the Room Member.
Note: Spaces are also Rooms with a different type.
Events are items of a Room, where data is embedded.
There are 2 types of Room Event:
- Regular Events: contain useful content for the user (message, image, etc.), but are not necessarily displayed as this in the timeline (reaction, message edition, call signaling).
- State Events: contain the state of the Room (name, topic, etc.). They have a non null value for the key
state_key.
Also all the Room Member details are in State Events: one State Event per member. In this case, the state_key is the matrixId (= userId).
Important Fields of an Event:
event_id: unique across the Matrix universe;room_id: the room the Event belongs to;type: describe what the Event contain, especially in thecontentsection, and how the SDK should handle this Event;content: dynamic Event data; depends on thetype.
So we have a triple event_id, type, state_key which uniquely defines an Event.
This is managed by the Rust SDK.
The Rust SDK is hosted here: https://github.com/matrix-org/matrix-rust-sdk.
This repository contains an implementation of a Matrix client-server library written in Rust.
With some bindings we can embed this sdk inside other environments, like Swift or Kotlin, with the help of Uniffi. From these kotlin bindings we can generate native libs (.so files) and kotlin classes/interfaces.
To use these bindings in an android project, we need to wrap this up into an android library (as the form of an .aar file). This is the goal of https://github.com/matrix-org/matrix-rust-components-kotlin. This repository is used for distributing kotlin releases of the Matrix Rust SDK. It'll provide the corresponding aar and also publish them on maven.
Most of the time you want to use the releases made on maven with gradle:
implementation("org.matrix.rustcomponents:sdk-android:latest-version")You can also have access to the aars through the release page.
If you want to make changes to the SDK or test them before integrating it with your codebase, you can build the SDK locally too.
Prerequisites:
- Install the Android NDK (Native Development Kit). To do this from within
Android Studio:
- Tools > SDK Manager
- Click the SDK Tools tab.
- Select the NDK (Side by side) checkbox
- Click OK.
- Click OK.
- When the installation is complete, click Finish.
- Install
cargo-ndk:cargo install cargo-ndk - Install the Android Rust toolchain for your machine's hardware:
rustup target add aarch64-linux-android x86_64-linux-android - Depending on the location of the Android SDK, you may need to set
ANDROID_HOME:export ANDROID_HOME=$HOME/android/sdk
You can then build the Rust SDK by running the script
tools/sdk/build_rust_sdk.sh and just answering
the questions.
This will prompt you for the path to the Rust SDK, then build it and
matrix-rust-components-kotlin, eventually producing an aar file at
./libraries/rustsdk/matrix-rust-sdk.aar, which will be picked up
automatically by the Element X Android build.
Troubleshooting:
- You may need to set
ANDROID_NDK_HOMEe.gexport ANDROID_NDK_HOME=~/Library/Android/sdk/ndk. - If you get the error
thread 'main' panicked at 'calledOption::unwrap()on aNonevalue', .cargo/registry/src/index.crates.io-6f17d22bba15001f/cargo-ndk-2.11.0/src/cli.rs:345:18try updating your Cargo NDK version. In this case, 2.11.0 is too old socargo install cargo-ndkto install a newer version. - If you get the error
Unsupported class file major version <n>, try changing your JVM version by settingJAVA_HOMEand, if building via Android Studio, "File | Settings | Build, Execution, Deployment | Build Tools | Gradle | Gradle JDK".
You can switch back to using the published version of the SDK by deleting libraries/rustsdk/matrix-rust-sdk.aar.
The project should compile out of the box.
This Android project is a multi modules project.
appmodule is the Android application module. Other modules are libraries;featuresmodules contain some UI and can be seen as screen or flow of screens of the application;librariesmodules contain classes that can be useful for other modules to work.
A few details about some modules:
libraries-coremodule contains utility classes;libraries-designsystemmodule contains Composables which can be used across the app (theme, etc.);libraries-elementresourcesmodule contains resource from Element Android (mainly strings);libraries-matrixmodule contains wrappers around the Matrix Rust SDK.
Most of the time a feature module should not know anything about other feature module.
The navigation glue is currently done in the app module.
Here is the current simplified module dependency graph:
flowchart TD
subgraph Application
app([:app])--implementation-->appnav([:appnav])
end
subgraph Features
featureapi([:features:*:api])
featureimpl([:features:*:impl])
end
subgraph Libraries
subgraph Matrix
matrixapi([:matrix:api])
matriximpl([:matrix:impl])
end
libraryarch([:libraries:architecture])
libraryapi([:libraries:*:api])
libraryimpl([:libraries:*:impl])
end
subgraph Matrix RustSdk
RustSdk([Rust Sdk])
end
app--implementation-->featureimpl
app--implementation-->libraryimpl
appnav--implementation-->featureapi
appnav--implementation-->libraryarch
featureimpl--api-->featureapi
featureimpl--implementation-->matrixapi
featureimpl--implementation-->libraryapi
featureimpl--implementation-->libraryarch
matriximpl--implementation-->matrixapi
matrixapi--api-->RustSdk
matriximpl--api-->RustSdk
featureapi--implementation-->libraryarch
libraryimpl--api-->libraryapi
This Android project mainly handle the application layer of the whole software. The communication with the Matrix server, as well as the local storage, the cryptography (encryption and decryption of Event, key management, etc.) is managed by the Rust SDK.
The application is responsible to store the session credentials though.
Compose is essentially two libraries : Compose Compiler and Compose UI. The compiler (and his runtime) is actually not specific to UI at all and offer powerful state management APIs. See https://jakewharton.com/a-jetpack-compose-by-any-other-name/
Some useful links:
- https://developer.android.com/jetpack/compose/mental-model
- https://developer.android.com/jetpack/compose/libraries
- https://developer.android.com/jetpack/compose/modifiers-list
- https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#api-guidelines-for-jetpack-compose
About Preview
Main libraries and frameworks used in this application:
- Navigation state with Appyx. Please watch this video to learn more about Appyx!
- Dependency injection: Metro
- Reactive State management with Compose runtime and Molecule
Some patterns are inspired by Circuit
Here are the main points:
PresenterandViewdoes not communicate with each other directly, but throughStateandEvent- Views are compose first
- Presenters are also compose first, and have a single
present(): Statemethod. It's using the power of compose-runtime/compiler. - The point of connection between a
Viewand aPresenteris aNode. - A
Nodeis also responsible for managing DI graph if any, see for instanceLoggedInAppScopeFlowNode. - A
ParentNodehas some childrenNodeand only know about them. - This is a single activity full compose application. The
MainActivityis responsible for holding and configuring theRootNode. - There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed.
This documentation provides you with the steps to install and use the AS plugin for generating modules in your project. The plugin and templates will help you quickly create new features with a standardized structure.
A. Installation
Follow these steps to install and configure the plugin and templates:
- Install the AS plugin for generating modules : Generate Module from Template
- From repository root, run
./tools/templates/generate_templates.shto generate the template zip file - Import file templates in AS :
- Navigate to File/Manage IDE Settings/Import Settings
- Pick the
tmp/file_templates.zipfiles - Click on OK
- Configure generate-module-from-template plugin :
- Navigate to AS/Settings/Tools/Module Template Settings
- Click on + / Import From File
- Pick the
tools/templates/FeatureModule.json
Everything should be ready to use.
B. Usage
Example for a new feature called RoomDetails:
- Right-click on the features package and click on Create Module from Template
- Fill the 2 text fields like so:
- MODULE_NAME = roomdetails
- FEATURE_NAME = RoomDetails
- Click on Next
- Verify that the structure looks ok and click on Finish
- The modules api/impl should be created under
features/roomdetailsdirectory. - Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
- You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the
Template Presentation Classes. To use it, just right click on the package where you want to generate classes, and click onTemplate Presentation Classes. Fill the text field with the base name of the classes, ieRootRoomDetailsin therootpackage.
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
suffix Presenter,states MUST have a suffix State, etc. Also we want to have a common naming along all the modules.
Note Firebase is implemented, but Unified Push is not yet fully implemented on the project, so this is not possible to choose this push provider in the app at the moment.
Please see the dedicated documentation for more details.
This is the classical scenario:
- App receives a Push. Note: Push is ignored if app is in foreground;
- App asks the SDK to load Event data (fastlane mode). We have a change to get the data faster and display the notification faster;
- App asks the SDK to perform a sync request.
We are using Gradle version catalog on this project.
All the dependencies (including android artifact, gradle plugin, etc.) should be declared in ../gradle/libs.versions.toml file.
Some dependency, mainly because they are not shared can be declared in build.gradle.kts files.
Renovate is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one. A dependency dashboard issue is maintained by the tool and allow to perform some actions.
We have 3 tests frameworks in place, and this should be sufficient to guarantee a good code coverage and limit regressions hopefully:
- Maestro to test the global usage of the application. See the related documentation.
- Combination of Showkase and Paparazzi, to test UI pixel perfect. To add test,
just add
@Previewfor the composable you are adding. See the related documentation and see in the template the file TemplateView.kt. We create PreviewProvider to provide different states. See for instance the file TemplateStateProvider.kt - Tests on presenter with Molecule and Turbine. See in the template the class TemplatePresenterTests.
Note For now we want to avoid using class mocking (with library such as mockk), because this should be not necessary. We prefer to create Fake
implementation of our interfaces. Mocking can be used to mock Android framework classes though, such as Bitmap for instance.
kover is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does not participate to the code coverage results.
Kover configuration is defined in the app build.gradle.kts file.
To compute the code coverage, run:
./gradlew :app:koverHtmlReportand open the Html report: ../app/build/reports/kover/html/index.html
To ensure that the code coverage threshold are OK, you can run
./gradlew :app:koverVerifyNote that the CI performs this check on every pull requests.
Also, if the rule Global minimum code coverage. is in error because code coverage is > maxValue, minValue and maxValue can be updated for this rule in
the file build.gradle.kts (you will see further instructions there).
**Important warning: ** NEVER log private user data, or use the flag LOG_PRIVATE_DATA. Be very careful when logging data class, all the content will be
output!
Timber is used to log data to logcat. We do not use directly the Log class. If possible please use a tag, as per
Timber.tag(loggerTag.value).d("my log")because automatic tag (= class name) will not be available on the release version.
Also generally it is recommended to provide the Throwable to the Timber log functions.
Last point, note that Timber.v function may have no effect on some devices. Prefer using Timber.d and up.
Translations are handled through localazy. See the dedicated README.md file for information on how to configure new modules etc.
Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report.
Bug reports can contain:
- a screenshot of the current application state
- the application logs from up to 15 application starts
- the logcat logs
The data will be sent to an internal server, which is not publicly accessible. A GitHub issue will also be created to a private GitHub repository.
Rageshake can be very useful to get logs from a release version of the application.
Warning
Developer options can result in unexpected application behavior or destructive actions. Use with caution and only if you are instructed by someone at Element or are already familiar.
These options provide advanced controls for testing and debugging. They are visible by default in debug and nightly builds but are hidden in release versions.
Enabling in release builds: Navigate to application settings and tap the version number at the bottom 7 times. After tapping, a new "Developer options" entry will appear at the bottom of the list.
The developer options include feature flags, notification/push history, Element call customization, Rust SDK log levels, per-feature tracing toggles, Showkase to debug UI components, rageshake controls, app crash controls, cache details/controls, persistent storage maintenance tasks.
Keywords: Developer settings, developer mode
- Using logcat, filtering with
Compositionscan help you to understand what screen are currently displayed on your device. Searching for string displayed on the screen can also help to find the running code in the codebase. - When this is possible, prefer using
sealed interfaceinstead ofsealed class; - When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI will detect this String and will warn the user about it. (TODO Not supported yet!)
- Very occasionally the gradle cache misbehaves and causes problems with code generation. Adding
--no-build-cacheto thegradlewcommand line can help to fix compilation issue.
The team is here to support you, feel free to ask anything to other developers.
Also please feel free to update this documentation, if incomplete/wrong/obsolete/etc.
Thanks!