Skip to main content
Wire Android is a multi-module Gradle project that separates concerns across an app module, shared core libraries, and self-contained features. The UI layer is built entirely with Jetpack Compose following the MVVM pattern. Protocol-level logic — including end-to-end encryption, API communication, and session management — is delegated to Kalium, a Kotlin Multiplatform library included as a Git submodule.

Module structure

wire-android/
├── app/                   # Main application module
├── core/
│   ├── analytics/         # Analytics abstraction (enabled/disabled variants)
│   ├── analytics-enabled/ # Analytics-enabled implementation
│   ├── analytics-disabled/# No-op analytics implementation
│   ├── di/                # Shared DI utilities
│   ├── media/             # Media playback and capture helpers
│   ├── navigation/        # Navigation wrappers (TabletDialogWrapper, etc.)
│   ├── notification/      # Notification infrastructure
│   ├── ui-common/         # Shared Compose components (Android)
│   └── ui-common-kmp/     # Shared Compose components (KMP)
├── features/
│   ├── cells/             # Wire Cells (file storage)
│   ├── meetings/          # Meetings / scheduling
│   ├── sketch/            # Drawing canvas
│   ├── sync/              # Background sync feature
│   └── template/          # Scaffolding for new feature modules
├── kalium/                # Git submodule — Kotlin Multiplatform core library
├── build-logic/           # Gradle convention plugins
└── buildSrc/              # Build-time constants and version catalog helpers

app

Entry point of the Android application. Contains WireApplication, all screen composables, ViewModels, navigation graph definition, and the top-level DI setup.

core/*

Reusable modules consumed by both app and features. Each module has a single responsibility (analytics, media, navigation wrappers, notifications, shared UI components).

features/*

Independently developed feature modules (cells, meetings, sketch, sync). Each module owns its own UI, ViewModels, and navigation destinations.

kalium

Kotlin Multiplatform library providing all protocol logic: MLS and Proteus encryption, API client, session management, and use-case definitions.

Clean architecture layers

Wire Android follows clean architecture with a strict dependency direction:
UI (Compose) → ViewModel → UseCase (Kalium) → Repository (Kalium)
1

UI layer — Jetpack Compose

All screens are @Composable functions. They observe StateFlows exposed by their corresponding ViewModel and dispatch events via method calls or Channels. No business logic lives in composables.
2

ViewModel layer

Each screen has a Hilt-injected ViewModel (or a @ViewModelScoped variant for sub-screens). ViewModels call use-case lambdas/functions provided by Kalium and transform results into UI state.
3

UseCase layer — Kalium

Kalium exposes plain Kotlin callable objects (use cases) through scoped containers (GlobalScope, SessionScope, sub-scopes such as ConversationScope, CallsScope). They encapsulate all business and protocol logic.
4

Repository / data layer — Kalium

Kalium repositories own persistence (SQLite via SQLDelight), network calls (Ktor), and local caches. The Android app never touches these layers directly.
The Android codebase never imports Kalium repository or data-source classes directly. All access goes through the use-case objects vended by CoreLogic.getGlobalScope() or CoreLogic.getSessionScope(userId).

Kalium submodule

Kalium is included under kalium/ as a Git submodule. After cloning the repository you must initialise it:
git submodule update --init --recursive
Kalium provides:
  • MLS (Messaging Layer Security) and Proteus end-to-end encryption
  • Wire API client (Ktor-based)
  • CoreLogic — the single entry point used by the Android app
  • GlobalScope — session-independent use cases (login, account list, server config)
  • SessionScope — per-user use cases (messaging, calls, files, sync)
The kalium/local.properties file is not created automatically by Android Studio. Copy the root local.properties (which sets sdk.dir) to kalium/local.properties before building.

MVVM with Jetpack Compose

Each screen follows this pattern:
// ViewModel exposes immutable state
@HiltViewModel
class ConversationListViewModel @Inject constructor(
    private val observeConversations: ObserveConversationListDetailsUseCase,
) : ViewModel() {

    var state by mutableStateOf(ConversationListState())
        private set

    init {
        viewModelScope.launch {
            observeConversations().collect { conversations ->
                state = state.copy(conversations = conversations)
            }
        }
    }
}

// Composable observes state
@Composable
fun ConversationListScreen(
    viewModel: ConversationListViewModel = hiltViewModel()
) {
    val state = viewModel.state
    ConversationListContent(conversations = state.conversations)
}

AssistedInject for parameterised ViewModels

ViewModels that require runtime arguments (e.g. a specific ConversationsSource or a search query Flow) use Hilt’s AssistedInject. This avoids passing data through LaunchedEffect calls and makes each ViewModel dedicated to exactly one configuration, which simplifies testing and debugging (see ADR-0005).
@HiltViewModel(assistedFactory = ConversationListViewModel.Factory::class)
class ConversationListViewModel @AssistedInject constructor(
    @Assisted val source: ConversationsSource,
    @Assisted val searchQuery: Flow<String>,
    private val observeConversations: ObserveConversationListDetailsUseCase,
) : ViewModel(), ConversationListViewModelInterface {

    @AssistedFactory
    interface Factory {
        fun create(source: ConversationsSource, searchQuery: Flow<String>): ConversationListViewModel
    }
}

Calling architecture

Calling uses two separate Activities to isolate disposable incoming/outgoing call UI from the long-lived ongoing call UI (ADR-0002):
ActivityPurpose
StartingActivityHandles incoming and outgoing call UI. Disposable — recreated for each new call.
OngoingCallActivityHandles the ongoing call. Kept alive for the full call duration.

Build system

build-logic — convention plugins

Reusable Gradle plugins live in build-logic/plugins/. Modules apply them by id instead of copy-pasting configuration:
PluginEffect
wire.android.libraryAndroid library defaults (compile SDK, Java 17, etc.)
wire.android.applicationAndroid application defaults + APK renaming
wire.hiltApplies dagger.hilt.android.plugin + com.google.devtools.ksp, adds Hilt/KSP dependencies
wire.android.navigationAdds core:navigation + Compose Destinations + KSP
wire.kmp.libraryKotlin Multiplatform library defaults

buildSrc — build-time constants

buildSrc/ provides Kotlin objects consumed during configuration time:
  • BuildTypes — debug / release build type names
  • Dependencies — shared dependency version strings (before the version catalog covers everything)
  • flavor/ — app flavour definitions (Dev, Staging, Internal, Beta, Prod, F-Droid)
  • Custom Gradle tasks (IncludeGitBuildTask, WriteKeyValuesToFileTask, RenameApkTask)

KSP for annotation processing

All annotation processing — Hilt code generation and Compose Destinations destination/nav-graph generation — uses KSP (Kotlin Symbol Processing) rather than KAPT, giving faster incremental builds.

App flavours

Dev (red)

Bleeding-edge builds targeting the Wire Staging backend. All experimental features enabled. Logging on.

Staging (orange)

Release-like builds for QA against the Staging backend. Includes extra dev tools. Logging on.

Beta (blue)

Internal dogfood builds targeting Wire Prod. Some pre-release features. Logging on.

Prod / F-Droid (white)

Public production builds. Logging is disabled by default and never uploaded. F-Droid variant contains no closed-source dependencies.

Build docs developers (and LLMs) love