# Android Idioms

> Idiomatic Kotlin for Android in this codebase — decomposing oversized lifecycle functions, scope functions (run/apply/with), when and partition instead of switch, extension functions and AndroidX KTX over verbose Java utilities, null safety instead of platform types, and companion-object constants. Use when writing any new Kotlin or converting a Java class to Kotlin.

- Skill: `nextcloud/android-idioms` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nextcloud/android-idioms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nextcloud/android-idioms/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nextcloud (https://skillmd.com/u/nextcloud)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nextcloud/android-idioms

---


<!--
 ~ SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
 ~ SPDX-License-Identifier: GPL-3.0-or-later
-->

# Android + Kotlin Idioms

The shape new Kotlin should take here, and the transformations to apply when converting
Java. Every one is behaviour-preserving. Examples are drawn from a real fragment conversion
in the wider nextcloud/android codebase — the principles transfer, but the class names
(`FileDetailSharingFragment`, `OCFile`, `fileActivity`) are from that project, not this one.

## 1. Decompose Oversized Functions

The IDE keeps the Java structure: one enormous `onViewCreated`/`setupView` that inflates,
themes, wires listeners, and kicks off loading in a single 80-line block. Split by
intent into small private functions. The lifecycle callback becomes a readable table of
contents.

```kotlin
// BEFORE: onViewCreated does everything inline (adapters, layout managers, listeners, fetch)

// AFTER
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    fileActivity ?: return
    fileDataStorageManager = fileActivity?.storageManager
    fileOperationsHelper = fileActivity?.fileOperationsHelper

    startAnimation()
    val userId = getUserId()
    setupInternalShares(userId)
    setupExternalShares(userId)
    binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() }
    fetchSharees()
    setupView()
}
```

Rules:
- One function = one reason to change. Name it for *what it accomplishes*
  (`setupInternalShares`, `themeView`, `disableE2EEShareForV1`), not *how*.
- Factor duplicated blocks into a parameterized helper
  (`createShareListAdapter(userId, SharesType.INTERNAL)`).
- Keep files ≤300 lines (project rule). Heavy decomposition sometimes means splitting a
  god-class into collaborators — raise that with the developer rather than exceeding 300.

## 2. Scope Functions Over Repetition

Replace repeated `binding.x` / `viewThemeUtils.material.y` chains with `run`/`apply`/`with`.

```kotlin
// BEFORE
viewThemeUtils.material.themeSearchCardView(binding.searchCardWrapper);
viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.sendCopyBtn);
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListInternalShowAll);

// AFTER
binding.run {
    viewThemeUtils.material.run {
        themeSearchCardView(searchCardWrapper)
        colorMaterialButtonPrimaryOutlined(sendCopyBtn)
        colorMaterialButtonPrimaryBorderless(sharesListInternalShowAll)
    }
}
```

Use `apply {}` when configuring and returning the receiver:

```kotlin
ShareeListAdapter(fileActivity!!, ArrayList(), this, userId, user, viewThemeUtils, encrypted, type)
    .apply { setHasStableIds(true) }
```

## 3. `switch` → `when` / `filter` + `partition`

Collapse a `switch` that sorts items into buckets into a declarative pipeline with a
constant `Set`.

```kotlin
// BEFORE: for-loop with switch(shareType) adding to internalShares / externalShares

// AFTER
private val externalShareTypes = setOf(
    ShareType.PUBLIC_LINK, ShareType.FEDERATED_GROUP, ShareType.FEDERATED, ShareType.EMAIL
)

val (external, internal) = shares
    .filter { it.shareType != null }
    .partition { it.shareType in externalShareTypes }
```

## 4. Extension Functions & KTX

Import members directly and lean on AndroidX KTX instead of verbose Java utilities.

| Java / verbose | Idiomatic Kotlin |
|---|---|
| `TextUtils.isEmpty(s)` | `s.isNullOrEmpty()` |
| `BundleExtensionsKt.getParcelableArgument(b, k, T.class)` | `b.getParcelableArgument(k, T::class.java)` |
| `for (int i = 0; i < vg.getChildCount(); i++)` | `for (i in 0..<view.size)` (`androidx.core.view.size`) |
| manual getter/setter methods | Kotlin property access (`view.visibility = View.GONE`) |
| `private int x; public int getX()` (read-only to callers) | `var columnsCount = 0; private set` |
| empty override method body | `= Unit` single-expression body |
| free-standing util call | receiver extension (`externalShares.mergeDistinctByToken(publicShares)`) |

Domain-specific extensions read best as receivers on the relevant type:

```kotlin
private fun OCCapability?.isPasswordEnforced(): Boolean =
    this?.filesSharingPublicPasswordEnforced?.isTrue == true &&
        filesSharingPublicAskForOptionalPassword.isTrue
```

## 5. Null Safety Instead of Platform Types

The IDE leaves `!` platform types and defensive Java null-checks. Replace with `?.`,
`?:`, and Kotlin's `require`/`requireNotNull`. A nullable `binding` (cleared in
`onDestroyView`) is the canonical Android case — guard it with `?.` / `?: return`.

```kotlin
// BEFORE
if (binding == null) return;
final LinearLayout shimmer = binding.shimmerLayout.getRoot();
shimmer.clearAnimation();

// AFTER
binding?.run {
    shimmerLayout.root.run {
        clearAnimation()
        visibility = View.GONE
    }
    shareContainer.visibility = View.VISIBLE
}
```

## 6. Constants & Companion Object

Move `static final` and magic literals into a `companion object`; use `const val` for
compile-time constants. Add `@JvmStatic` to factory methods still called from Java.

```kotlin
companion object {
    private const val TAG = "FileDetailSharingFragment"
    private const val ARG_FILE = "FILE"
    private const val MIN_SHOW_ALL_VISIBLE_ITEM_COUNT = 3
    private const val INTERNAL_LINK_PATH_PRETTY = "/f/"

    @JvmStatic
    fun newInstance(file: OCFile?, user: User?) = FileDetailSharingFragment().apply {
        arguments = Bundle().apply {
            putParcelable(ARG_FILE, file)
            putParcelable(ARG_USER, user)
        }
    }
}
```

## 7. Decompose; Do Not Suppress

If a legacy god-class cannot be split within the change's scope, say so and propose the
split — do not paper over it with `@Suppress("TooManyFunctions", "LargeClass", ...)`. Those
are detekt rule names and this repository has no detekt, so the annotation suppresses
nothing; it only tells the next reader that someone knew the file was too big and left it.

## 8. `// region` Organization

For large classes, grouping members under `// region <name>` / `// endregion` (lifecycle,
private methods, overrides, companion) aids IDE folding. This is IDE structure, not a
decorative divider. Match the surrounding file's existing style; do not introduce ASCII
banner comments (`// ==== ====`), which the project forbids.

