| name | android-idioms |
| description | 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. |
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.
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.
viewThemeUtils.material.themeSearchCardView(binding.searchCardWrapper);
viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.sendCopyBtn);
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListInternalShowAll);
binding.run {
viewThemeUtils.material.run {
themeSearchCardView(searchCardWrapper)
colorMaterialButtonPrimaryOutlined(sendCopyBtn)
colorMaterialButtonPrimaryBorderless(sharesListInternalShowAll)
}
}
Use apply {} when configuring and returning the receiver:
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.
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:
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.
if (binding == null) return;
final LinearLayout shimmer = binding.shimmerLayout.getRoot();
shimmer.clearAnimation();
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.
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.