Skip to content

Latest commit

 

History

History
669 lines (547 loc) · 41.7 KB

CONTRIBUTING.md

File metadata and controls

669 lines (547 loc) · 41.7 KB

CONTRIBUTING.md aka «the trash Panda's guide to the galaxy»

0. Introduction

0.1 Welcome

First of all, a word of welcoming to whomever has landed on this page. If you are here, that means that somehow you have found a link to this project over the Internet, you are possibly interested in it (either positively or negatively) and you are wondering whether you can contribute in any way.

This is great news, so thank you for your time and dedication. Whether you spend 1 minute or 1 hour here, it is really highly appreciated.

0.2 ToC

With no further ado, here is a short list of what you'll find here. Keep in mind that, like the project in under continuous development and evolution (hopefully), so is this guide. New sections will be added and new answers will be provided as long as questions are submitted.

  • section 1 will provide a general overview about the project and its contributors: who we are, what we are trying to achieve, why are we doing it and how;
  • section 2 illustrates the core values that are behind this project, its key goals and an acknowledgement to all the people who helped us to achieve those targets;
  • section 3 is a collection of operative instructions for those who want to contribute, either reporting bugs, asking for new features, submitting feedback, contributing with code, documentation or translations;
  • section 4 is dedicated to developers wanting to build the project locally in order to submit pull requests;
  • section 5 contains a technical illustration of the tech stack used in the project how the project is broken down into discrete components that interact with each other;
  • section 6 contains the architectural patterns used in the project and the coding conventions you should follow when submitting PRs, because readability and consistency matter and there are some rules (with exceptions) that should ideally be followed everywhere.

1 Project overview

1.1 Purpose

Raccoon for Lemmy was designed with three goals in mind: on the one side experiment and put to some stress test with a real world project Kotlin Multiplatform (KMP) and Compose multiplatform, in order to explore what could be achieved with multiplatform libraries and share as much code as possible in the commonMain source directory. The Android ecosystem of Lemmy apps has many cross-platform alternatives implemented with Flutter or React, a couple of native options written in Java and just one native pure-Kotlin application. Raccoon intends to be an Android-first app, but experimentation on other OSes given the multiplatform nature of its technologies are not excluded a priori.

The second goal was to offer a feature rich Lemmy client mainly aimed at "pro" users, i.e. users who are not content with just browsing the contents of the Fediverse, creating posts and answers. This is why Raccoon tried to offer from the very beginning features like:

  • a full-fledged explore section that allows multiple result types
  • instance info, with the list of communities of foreign instances as well as the possibility to visit communities on foreign instances in guest mode
  • multi-community, i.e. community aggregation
  • moderation tools (for community moderators) and access to community moderation log.

Finally, the third goal was to offer a Lemmy experience where everyone could "feel at home" (see the Code of Conduct below). This implies keeping the interface lean by default but having the possibility to customize a variety of aspects (theme, vote format, default feed/sort type) and adopt the user's native language. As a consequence, leanness, personalization and localization are first-class citizens in this project.

1.2 About the name

Lemmy users used to refer to themselves as "lemmings" and the platform logo clearly recalls that animal, so there was this tradition in the early days to use animals names for clients or at least the original developers of this app thought so (some examples being Jerboa and Fennec). And they really liked raccoons so they choose that. Raccoons are cunning animals that adapt to live in harsh conditions, finding a way to survive even with what we consider "trash". They look like masked criminals but do not harm anyone, they are lazy and chubby and so cute in the end.

In this group, we often quote every raccoon's motto: «Live Fast, Eat Trash» (abbreviated LFET).

2 Community

2.1 About us

This section is probably going to change over time, anyway we are a small group of volunteers who in their spare time contribute to the project.

You can find us on this Matrix space, where you can ask more questions in the General room or by writing directly to the administrator.

2.2 Code of Conduct

When interacting with other members of the community, be them end-users, developers, supporters or whatever, remember that we are raccoons, and we follow these principles:

Principle Explanation
R Respect We respect each other as people, remembering we are part of a group that goes beyond individual opinions, beliefs, preferences and habits.
A Availability We support each other with the skills and available time that we have, considering that we are volunteers and we operate on a best-effort basis.
C Commitment We are responsible for our choices and we agree that our choices are taken wisely, always considering the impact on other members of the group.
C Cohesion We remember that we are part of a community which is bigger than the individual members, so we do unto others as we would have them do unto us.
O Objectivity We are committed to telling the truth in the most objective way and, if we express subjective opinions, to do it in a clear and constructive way.
O Originality We bring our own personal experience and ideas which are acceptable even if "different": everyone's voice matters and deserves to be listened.
N Never give up We are tenacious and are not afraid by technical challenges, we embrace every difficult task as an opportunity to learn and acquire new skills.

2.3 Acknowledgements

This project would not be what it is were it not for the huge amount of patience and dedication of early adopters who sent me continuous feedback and ideas for improvement after every release, reported bugs, offered to help, submitted translations to their local language, etc.

A special thank to all those who contributed so far (in nearly chronological order):

3. HOW-TOs

3.1 General interactions

There are many ways you can interact with members of the community:

  • use the Matrix space to chat in real time with other team members, there are two rooms in the space: "General" is for general information about the app development and "Trashcan" is more for smalltalk and random topics.
  • use the Lemmy community to receive updates about the new releases, participate into public discussions in the Lemmy style and provide your feedback or even share your memes about raccoons with any other interested people
  • use the GitHub issue tracker to report bugs or request features
  • use the GitHub discussion section for technical questions about the release roadmap, questions about app internationalization, etc.
  • finally, if none of the above methods fits your needs you can write an email or send a private message to the original developer (my personal information are in the GitHub profile).

Always remember the guidelines contained in the Code of Conduct to interact with other people: our values are important and should be applied even with each other and not only with end users.

3.2 Create bug reports

The preferred way to submit a bug report is the GitHub issue tracker.

  • use the "New issue" button to open the creation form
  • in the "Add a title" field give a synthetic description of the issue, if it affects a specific part of the application you can add in square brackets a prefix containing that information, e.g. [post list] or post detail or [create report].
  • in the "Add a description" field provide a more detailed explanation of the issue, you can use Markdown syntax (in the GitHub flavour) to format text here. An ideal bug report contains:
    • the precondition (sequence of steps that lead to the faulty condition, if it is deterministic)
    • a description of the error condition (with the expected result and the actual result, where the difference between the two is clear)
    • (if you can, optionally) some screenshots that make it clear where the error is
  • you can add the "bug" label to the issue or otherwise leave it blank. Do not use the other fields (assignee, milestone, etc.) because they are used internally by the team members who will take the issue in charge during resolution.

3.3 Request features

As for bug reports, the preferred way for feature or change requests is the GitHub issue tracker. In this case, you should:

  • insert a short description in the title field, if the feature is specific to some app domain you can use the same square bracket prefix as in bug reports
  • insert a more detailed explanation of your idea in the description field, if your feature is present in other apps you can provide screenshots or make references to make the request clearer
  • you can use the "feature" label to tag the issue, or otherwise leave it blank and let the maintainers decide how to triage the issue.

3.4 Add a new localization

The preferred way for localizations (l10ns) is to submit a pull request (PR) as detailed in the next section. The project uses the moko-resources library for multiplatform access to resource files, which for l10n implies having to deal with XMLs in the Android style.

You will have to create a new folder under the resources/src/commonMain/resources/MR directory named after the locale you want to add (following IANA conventions for locales) and create a strings.xml file in it, copying the contents of this file except for the items that are marked as translatable="false" (you can safely remove them).

Modify the base l10n to add the name of your language in the untranslatable set

<string name="language_XXX" translatable="false">YYY</string>

where XXX is the IANA locale code and YYY if the name you want to appear in the UI (use the endonym of your language, i.e. the name of that language in the language itself).

After which you'll have to make sure the project compiles, because there are some syntax rules that must be enforced in string files, e.g.:

  • apostrophes (') must be escaped with a backslash (\')
  • some special characters must be represented as XML entities so > becomes &gt;, < becomes &lt;, & becomes &amps; and so on… (talking of ellipsis, use preferably instead of three dots)

If you want you can change the code in the following spots:

  • add your flag and language name (mapping your language code to the values) in Extensions.kt
  • add an option (using your language code) in the values array in LanguageBottomSheet.kt

That's it. You can test that everything works by launching the development app.

A note for translators: the strings that correspond to theme names (settings_color_aquamarine, settings_color_banana, settings_color_blue, settings_color_gray, settings_color_green, settings_color_orange, settings_color_pink, settings_color_purple, settings_color_red and settings_color_white) contain the name of an animal accompanied by an adjective. If would be nice if the adjective and the noun would start in every language with the same sound in order to create humorous pairs like in English ("hilarious hedgehog", "frolicsome frog", etc.).

However, if you are not a developer and do not feel confident with GitHub's PR mechanism, you can just download the base l10n to your local machine, edit the file and send an email to the maintainers with the attachment, we will take care of the rest.

If you have proposals, want to submit l10n fixes/improvements to existing ones, you can use this discussion and post an answer, we will reach out to you and solve the issue.

3.5 Submit a pull request

First of all, please fork the repository by using the "Create a new fork" button in the GitHub console, which will create a copy of the repository in your personal account.

Create a branch from the repository's master branch named after the feature/change you want to submit, use whatever convention you like (preferably underscores to separate words but as you wish) and make any commits you want on it.

Push the commits on the remote branch of your forked repository.

Use the "Compare and pull request" button to see a recap of the PR, after which use the "Create pull request" to submit the request to the upstream repository.

4. Setup 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.2.1 with the Android Gradle Plugin (AGP) version 8.2.1 you should use Android Studio Hedgehog 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 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=-Xmx8192M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx8192M"

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:

  • the Gradle wrapper is updated or some Gradle plugins are updated to a newer version;
  • a new external dependency is added or an existing library is updated to a newer version;
  • a new Gradle module is added to the project or whenever you edit the settings.gradle.kts file or any build.gradle.kts file in any module of the project hierarchy.

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.

5. 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).

5.1 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
The Moko resources library is used as part of the Moko project. This was a choice I never regretted, it works great (even in edge cases such as dynamic language configuration independent of device settings, which was a project must-have from the beginning) and makes it really easy to access strings, icons and fonts in a multiplatform environment.
Image loading
This was something that was expected to be simpler but unfortunately it wasn't. Popular kotlin libraries do not support multiplatform yet and the only library that advertised it was Kamel which had a major bug while rendering large images, which took a long time to be considered (and possibly has not been fixed yet). 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. In the end, the iOS part of the project continues using it (Raccoon for iOS has by far bigger problems than image rendering, being a virtually inexistent platform) while the Android counterpart was migrated to Coil. Things will change when Coil 3.x will be release, because it will go multiplatform as well.
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 is still lacking and things are far more complicated than it should be. The first approach that was used in the project, and which still survives in the iOS platform (being it "no man's land" currently) involved using JetBrain's Markdown library for parsing in conjunction with custom Compose rendering inspired by Multiplatform Markdown Renderer. This approach was promising in the beginning but it has proven to grow more and more difficult to support custom Markdown features, such as Lemmy spoilers. For this reason, the Android counterpart has been completely refactored and migrated to the Markwon library which is more flexible/extensible albeit more complicated to use, especially if called from a multiplatform environment with expect/actual functions (and image opening/URL opening/custom links like Lemmy URL references have to be managed). The big star here is MarkwonProvider and its implementation DefaultMarkwonProvider.kt. Parts of the Markwon configuration and usage is inspired by Jerboa for Lemmy.
Video playback
This had to be native, the Android implementation relies on Exoplayer whereas the iOS implementation on AVPlayer as usual.
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!

5.2 Module overview and dependencies

The project is organized as a multi-module Gradle projects, meaning that it is split into a series of interdependent Gradle subprojects and that there is a module hierarchy in which multiple levels.

The modules can be grouped into five groups:

  • top-level and special modules: these modules are tied to the project setup and contain either the entry point of the app (include everything) or expose the application resources (are included by everyone and do not include anyone).
  • feature modules: these that correspond to the main functions of the application, i.e. the five tabs that can be found in the tab bar that live independently and made up the basic structure. Feature modules can be included by top-level modules but can not include each other. They can include domain modules, unit modules or core modules.
  • domain modules: a series of purely business logic modules containing code related to specific domains such as user identity management, all the Lemmy specific business logic (APIs and inbox). Domain modules can only include core modules and can be included by feature modules, unit modules or top-level ones.
  • unit modules: reusable parts of the application containing UI components and a thin layer of presentation logic that are used to render the various screens, bottom sheets and dialogs of the application, grouped by feature so that each block can be called from multiple points. Unit modules are used by feature modules and can include domain modules, core modules or in some limited cases other unit modules (but never cyclically!).
  • core modules are the foundational layer of the application. They are included by unit modules, feature modules, domain modules and top-level modules. They should not include anything except the :resources modules and, in some rare occasions, other core modules (but never cyclically!). A notable example of this is the :core:md module (Markdown rendering) that includes :core:commonui:components because Markdown requires some custom UI components to be rendered.

5.2.1 Special and 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).

Another special module is :resources, which in turn is a terminal module, included by every other module and not including anyone. It is the module that contains the app resources (drawables, fonts, l10ns) and uses the moko-resources library to generate the MR object that exposes resource descriptors to all other multiplatform subprojects.

5.2.2 Feature modules

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

  • :feature:home contains the post list tab;
  • :feature:search contains the Explore tab;
  • :feature:inbox contains the Inbox tab;
  • :feature:profile contains the Profile tab;
  • :feature:settings contains the Settings tab.

5.2.3 Domain modules

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

  • :domain:identity contains the repositories and use cases that are related to user identity, authorization and API configuration;
  • :domain:lemmy contains all the Lemmy API interaction logic and is divided into two submodules:
    • :data contains all the domain models for Lemmy entities (posts, comments, communities, users, etc);
    • :repository contains the repositories that access Lemmy APIs (through the :core:api module) and are used to manage the entities contained in the :data module;
  • :domain:inbox contains some uses cases needed to interact with the replies, mentions and private messages repositories and coordinate the interaction between inbox-related app components.

5.2.4 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:

  • :unit:about contains the About this app dialog
  • :unit:accountsettings contains the screen of the remote account settings (web preferences)
  • :unit:ban contains the modal bottom sheet used to ban a user from a community
  • :unit:chat contains the chat conversation screen
  • :unit:choosecolor contains the dialogs and bottom sheets used for theme/color selection
  • :unit:choosefont contains the bottom sheets used for font selection
  • :unit:communitydetail contains the community detail screen
  • :unit:communityinfo contains the community info bottom sheet accessible from community detail
  • :unit:configureswipeactions contains the screen and bottom sheets to configure swipe actions
  • :unit:createcomment contains the create comment form
  • :unit:createpost contains the create post form
  • :unit:drawer contains the navigation drawer
  • :unit:instanceinfo contains the instance info bottom sheet with the list of communities
  • :unit:login contains the login modal bottom sheet
  • :unit:manageaccounts contains the modal bottom sheet used to change account
  • :unit:manageban contains the ban management screen
  • :unit:managesubscriptions contains the subscription management screen
  • :unit:mentions contains the mentions section of the Inbox tab
  • :unit:messages contains the private messages section of the Inbox tab
  • :unit:modlog contains the moderation log screen
  • :unit:multicommunity contains the multi-community detail and multi-community editor screens
  • :unit:myaccount contains the profile logged section of the Profile tab
  • :unit:postdetail contains the post detail screen
  • :unit:postlist contains the post list (home) screen
  • :unit:rawcontent contains the "Raw content" dialog with the custom (Android) text toolbar
  • :unit:remove contains the modal bottom sheet to remove a content (for moderators)
  • :unit:replies contains the replies section of the Inbox tab
  • :unit:reportlist contains the report list screen (for moderators)
  • :unit:saveditems contains the saved items screen
  • :unit:selectcommunity contains the dialog used to select communities (for cross-posts)
  • :unit:selectinstance contains the bottom sheet used to change instance in anonymous mode
  • :unit:userdetail contains the user detail screen
  • :unit:userinfo contains the user information bottom sheet accessible from user detail
  • :unit:web contains the internal WebView screen
  • :unit:zoomableimage contains the image detail screen used to display full-screen images.

5.2.5 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:

  • :core:api contains the Ktorfit services used to interact with Lemmy APIs and all the data transfer objects (DTOs) used to send and receive data from the APIs;
  • :core:appearance contains the look and feel repository which exposes the information about the current theme as observable states and allows to change them;
  • :core:architecture contains the building blocks for the Model-View-Intent architecture used in all the screens of the application (see the section about architectural patterns);
  • :core:commonui contains a series of sub-modules that are used to define UI components used in the app and reusable UI blocks:
    • :components: a collection of components that represent graphical widgets
    • :detailopener-api : a utility module used to expose an API to centralize content opening ( post detail, community, detail, user detail, comment creation and post creation)
    • :detailopener-impl: implementation of the detail opener, this is an exception to the module architecture because it is a core module which includes unit modules so the important thing is that no one ever include this module except for :shared;
    • :lemmyui: graphical components used to represent Lemmy UI (posts, comments, inbox items, etc.) and reusable sub-components such as different types of headers, footers, cards, etc.
    • :modals: definition of modal bottom sheets and dialogs that have no presentation logic. This module was historically much bigger and over time components were migrated to separate units modules;
  • :core:md contains Markdown rendering components;
  • :core:navigation contains the navigation manager used for stack navigation, bottom sheet navigation and a coordinator for the events originated by the navigation drawer;
  • :core:notifications contains the NotificationCenter contract and implementation as well as the event definition, this is used as an event bus throughout the whole project;
  • :core:persistence contains the local database (primary storage) management logic as well as SQLDelight definitions of entities and migrations, plus all the local data sources that are used to access the database;
  • :core:preferences contains the shared preferences/user defaults (secondary storage) and relies on the multiplatform-settings library to offer a temporary key-value store;
  • :core:utils: contains a series of helper and utility functions/classes that are used in the project but were not big enough to be converted to separate domain/core modules on their own.

On second thoughts:

  • the :resources module could have been a :core:xxx module but I followed the guide of the moko-resources library and thought it was wiser not to divert too much from the standard configuration;
  • :core:commonui has still too much in it, especially :modals packages should become unit modules;
  • :core:persistence belongs more to domain modules, e.g. :domain:accounts/:domain:settings but it is implemented as a core module because is is strongly tied to SQLDelight and its generated code which provides the named queries to fetch/save data to the local DB.

6. Coding conventions

6.1 Architectural patterns

6.1.1 General principles

The project’s architecture is inspired from the Clean architecture principles. Modules are organized in tiers and dependency flows goes from higher modules (top-level and feature modules) towards lower order modules (unit modules for UI, domain modules for business logic) which in turn rely on core modules for low-level operations; and that is a hierarchical structure on the vertical axis. Apart from that, there is a split on the horizontal axis (i.e. different features for different sections of the app). The intersection between these two axes determines the modularization strategy.

To summarize, if you are looking for something related to the app UI, have a look at :feature:xxx or :unit:xxx (and possibly you will have to drill down to :core:commonui:xxx). If you are searching for some piece of interaction with Lemmy APIs, have a look at :domain:lemmy:xxx (and you’ll probably end up in :core:api for more low-level minutia), if you look for data access on the local database have a look at :core:persistence, and so on. More on this here (Modularization strategy).

6.1.2 Model-View-Intent

Every part of the app which has some non trivial user-interaction follows the Model-View-Intent architectural pattern. This means that there are two different components interacting with each other:

  • the View, represented by a Screen implementation
  • the ViewModel, represented by a ScreenModel implementation

where the interfaces (Screen and ScreenModel) both come from the Voyager navigation library.

The View has the responsibility of drawing to screen the UI components that are needed to represent a particular state, which is provided by the ViewModel they have a reference to (the ViewModel being a state holder for this respect). The View has also the ability to collect user input (or, rather, intentions as intents) and dispatch them to the ViewModel.

On its side, the ViewModel holds an observable state and has the ability to react to user intents coming from the View, each of which implies some business logic operations which, in turn, determine ultimately a state change, observed by the view. In some less frequent case, the ViewModel can emit one-time events, that are unrelated to persistent state and can determine volatile * effects* in the View.

These concepts of a ViewModel having to deal with Intent, State and Effect are modelled in Kotlin in the :core:architecture module which is included as a building block by all UI related features and unit modules of the project. The MviModel interface defined here is a common supertype of all the ScreenModel implementations.

In case some event needs to be propagated across different ViewModels, the event bus defined in :core:notifications is used.

6.2 Modularization strategy

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:

  • :androidApp which is the KMP equivalent of :app module in Android-only projects) include :shared and can include :core modules (e.g. for navigation);
  • :shared is the heart of the KMP application and it virtually includes every other Gradle module as a dependency (it contains in the DiHelper.kt files the setup of the DI so it basically needs to see all Koin modules);
  • the :resources module is at the bottom of the hierarchy and is included by any other module (but does not depend on any);
  • :feature modules are included by :shared and include :domain, :core and :unit modules but they DO not include other each other nor any top level module; some unit modules are used just by one feature (e.g. :unit:postlist is used only by :feature:home) in some other cases multiple features use the same unit (e.g. :unit:zoomableimage is used by both :feature:home, :feature:search, :feature:profile and :feature:inbox):
  • :domain modules can be used by feature and unit modules and can only include core modules; only exception is :domain:inbox which is a thin layer on top of :domain:lemmy so it depends on it ( for inbox related functions);
  • :unit modules are included by feature modules (and :shared) and sometimes by other unit modules in case of highly reusable parts of the app; the only notable violation to this rule is :core:commonui:detailopener-impl which is a special module because it is only included by :shared (which does the binding between :detailopener-api and :detailopener-impl) and it includes some unit modules but the fact of a unit module included by a core module in general should never happen (instead, the reverse is perfectly ok);
  • :core modules can sometimes include each other (but without cycles, e.g. :core:md includes :core:commonui:components / :core:utils because it is a mid-level module and something similar happens with :core:persistence which uses :core:preferences / :core:appearance) and nothing else; they are in turn used by all the other types of modules.

For more detailed information about dependencies, please refer to the diagrams you can find in this page.

6.3 General Kotlin rules

Please refer to this page for the conventions to apply to Kotlin code, most of which are already enforced by Android Studio inspections.

6.4 Compose rules

As far as Compose code is concerned, we take Google’s indications as a baseline:

6.5 Test structure

TBD depending on experiment result.