Raccoon for Lemmy

Raccoon for Lemmy 🦝 documentation

View on GitHub

Technical manual

This page contains a series of technical notes that can be useful for those who want to contribute to the development of the app or simply become more familiar with the project structure.

Setting up the development environment

This is a Kotlin Multiplatform (KMP) project that uses the Gradle build tool. The recommended development environment is Android Studio with the Kotlin Multiplatform Mobile plugin installed. Since the project is using Gradle 8.8 with the Android Gradle Plugin (AGP) version 8.5.0 you should use Android Studio Koala or later (have a look here for a compatibility matrix between versions of Gradle, AGP and Android Studio). Alternatively, you can try and use IntelliJ IDEA or Fleet but some extra steps may be needed to ensure everything fits and runs together.

In order for Gradle to build, you will need to have a JDK installed on your local development machine, if you are using stock Android Studio it ships with the JetBrains runtime, you could have a look in the Settings dialog under the section “Build, Execution, Deployment > Build Tools > Gradle” in the “Gradle JDK” location drop-down menu. If you want to use your custom JDK (e.g. under Linux you want to try OpenJDK instead), please make sure that it has a suitable version, according to this page, so between 8 and 21.

Finally, since building this project requires a quite lot of RAM due to its multi-module structure and to the fact that it is quite a complex project, please make sure that the gradle.properties file in the root folder contains proper memory settings for the JVM and the Kotlin compile daemon:

org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4096M"

The first thing that Android Studio does upon first opening the project is a Gradle sync, this may take some time since at the beginning it has to download all the dependencies and build the cache. A Gradle sync is required every time:

In case it does not suggest it to you automatically, you will fine the “Sync Project with Gradle Files” button in the top left corner of the toolbar, right before the “Search Everywhere” button. The operation can be, depending on your hardware and connection speed, quite time consuming so be patient.

Project structure

This is a Gradle project, it is setup to download a Gradle distribution and resolve dependencies according to the definitions contained in the gradle/libs.versions.toml file.

Also, please note that in the settings.gradle.kts file we are using the option:

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

to reference Gradle subprojects in each build.gradle.kts with a type safe notation e.g. implementation(projects.core.utils).

Tech stack

Here is a list of the technologies used in the project, with a short historical explanation in the cases where the initial choice changed over time and the reasons why the change was made.

Dependency Injection
The choice here is the Koin library. The main reason it was selected because of its great multiplatform support and the integration with the navigation library (which at the beginning of the project was not there yet, but was added later and proved to work great). You can find module definitions (beware, Gradle modules and Koin modules are two different concepts and should not be confused) in a `di` package inside each subproject, modules can include each other and all top-level modules are included in the shared module, more on it in "Module overview and dependencies".
Navigation
For navigation the Voyager library has been selected. Again, the choice was driven by its multi-platform support, its very practical approach and ease to set up and get going, compared to other multi-platform libraries like Decompose that were used in the past. Nonetheless, and this lesson was learned the hard way, easiness of use and compactness mean that things will go smooth in the future, and as the project grew the navigation library started to show its limits. Part of them were addressed by encapsulating the transition logic (to push/pop screens into the navigation stack and open/close modal bottom sheets) into a centralized component NavigationCoordinator.kt. Something similar was done for the navigation drawer in DrawerCoordinator.kt. Even the DI integration with Koin was not pain-free, the :core:navigation module contains some glue code that is used to work around some of the issues that were encountered.
Networking
Here, at least for Android developers, no surprises: the choice was Ktor which has great multiplatform support. Instead of using Retrofit, to create network adapters the Ktorfit library is used, which uses KSP to parse annotations and generate code.
Resource management
Initially the project used the Moko resources library to load fonts, icons and all the localized messages used in the UI. It worked great, since in those areas Compose multiplatform missed the needed functionalities. But as long as the project grew in size and more complex KSP configurations were needed, having all modules depend on resources became unmaintainable. This is why I migrated drawable and font loading to Compose build-in system. And, for localization, the choice was the Lyricist library, which better handles dynamic language changes.
Image loading
This was something that was expected to be simpler but unfortunately it wasn't. In the beginning the project used the Kamel library which had a major bug while rendering large images, which took a long time to be considered. The project was already relying on Kamel for many things, from loading images on demand to Markdown rendering, so deciding to switch was not easy at all. So as a first step, Coil 2.x was adopted on Android while keeping Kamel on iOS and, finally, when Coil 3.x became stable with multiplatform support all the project has been completely migrated.
Preference storage
Here the choice was the Multiplatform settings libary which not only works great but also offers support for encryption.
Primary persistence
This project was a chance to experiment with SQLDelight (in other multiplatform projects other libraries were tested like Exposed), whereas database encryption is obtained through SQLCipher Android, formerly Android Database SQLCipher.
Markdown rendering
This was another part, like image loading, where KMP was at the beginning quite lacky. After having given up for some time and used Markwon (Java + Views) on the Android part of the app, I decided to give a second chance to Multiplatform Markdown Renderer which was initially user for the multiplatform source set. The project grew and matured over time and it made it possible to add custom handlers (like modular plug-ins) which made possible to support Lemmy's custom features like spoilers. The migration from multiplatform renderer to Markwon and back to multiplatform renderer was not easy, but this project is about KMP so, as a consequence, a pure Kotlin and pure Compose solution had to be preferred. Even if it implies to sacrifice some functionality.
Video playback
The initial choice was to write a custom native implementation based on Exoplayer on Android and on AVPlayer on iOS. This solution kind of worked but had some issues and with later updates of Compose Multiplatform it was using deprecated functions on iOS. This is why the video player was eventually migrated to ComposeMultiplatformMediaPlayer.
Theme generation
The application allows to select a custom color and generate a Material 3 color scheme as a palette originate from that seed color. This is achieved by using the MaterialKolor library which was designed to be multiplatform and works as a charm for the purpose. Thanks!
Reorderable lists
The ability to reorder lists is achieved thanks to the Reorderable library which starting from version 1.3.1 has become multiplatform. This functionality is still experimental and is used only in the instance selection bottom sheet for anonymous users.
Web view
Initially a custom web view was implemented, relying on native views (WebView on Android and WKWebView on iOS), but in the end the project was migrated to Calf component to display portions of the Web.

Module structure

The project has different kinds of modules and, depending on the group a module belongs to, there are some rules about which other modules it can depend on.

Here is a description of the dependency flow:

Top-level modules

The main module (Android-specific) is :androidApp, which contains the Application subclass (MainApplication) and the main activity (MainActivity). The latter in its onCreate(Bundle?) invokes the MainView Composable function which in turns calls App, the main entry point of the multiplatform application which is defined in the :shared module.

:shared is the top module of the multiplatform application, which includes all the other modules and is not included by anything (except :androidApp). In its commonMain source set, this module contains App, the application entry point, the definition on the MainScreen (and its ViewModel) hosting the main navigation with the bottom tab bar. Another important part of this module resides in the platform specific source sets (androidMain and iosMain respectively) where two DiHelper.kt files (one for each platform) can be found, which contain the setup of the root of the project’s dependency injection in a platform specific way, an initialization function on iOS and a Koin module for Android (which is included in MainApplication).

Feature modules

These modules correspond to the main functions of the application, i.e. the sections of the main bottom navigation. In particular:

Domain modules

These are purely business logic modules that can be reused to provide application main parts:

Unit modules

These modules are the building blocks that are used to create user-visible parts of the application, i.e. the various screens, some of which are reusable in multiple points (e.g. the user detail, community detail or post detail, but also report/post/comment creation forms, etc.). In some cases even a dialog or a bottom-sheet can become a “unit”, especially if it is used in multiple points or contains a little more than pure UI (e.g. some presentation logic); simple pure-UI dialogs and sheets are located in the :core:commonui:modals module instead (but are being progressively converted to separate units).

Here is a list of the main unit modules and their purpose:

Core modules

These are the foundational blocks containing the design system and various reusable utilities that are called throughout the whole project. Here is a short description of them:

On second thoughts: