Guidelines
July 17, 2026 ยท View on GitHub
Collecting design decisions. New and updated code and resources should follow them.
Coding patterns
Kotlin
For function calls, specify names of function parameters when the name of the passed value does not make it obvious:
// DO
setVisible(true)
doSomething(avoidWork = true)
// AVOID
doSomething(true)
Application dependency injection
Existing code is using Dagger and a ServicesComponent.
New code should avoid relying on Dagger (and its annotation processor) and use the SgAppContainer instead.
Room database
The @Entity data classes should use nullable types for all columns (besides the ID). Validation,
like null or empty checks, should always happen in code as the app has no control over modifications
to the database.
Prefer to define table and column names using constants (like @ColumnInfo(name = CONSTANT)).
This makes them safer to re-use.
There isn't a need to handle android.database.sqlite.SQLiteDatabaseCorruptException. If an
operation fails due to this, the app will crash. And when next started, Room will delete the corrupt
database files (androidx.room.RoomConnectionManager.SupportOpenHelperCallback extends
androidx.sqlite.db.SupportSQLiteOpenHelper.Callback where in onCorruption the file is deleted).
Lifecycle
For fragments, use viewLifecycleOwner to tie to the lifecycle of the view.
Note that dialog fragments don't have a viewLifecycleOwner.
Obtain a coroutine scope with lifecycleScope.
Click listeners
The interface class is owned by the class that owns the views that trigger the click events, for example the item view holder.
The interface class is named based on what the listener is for.
Example: ItemClickListener
The methods are named based on what is clicked.
Example: onMoreOptionsClick.
RecyclerView
Use the following pattern for ViewHolder classes:
class LinkViewHolder(
private val binding: ItemDiscoverLinkBinding,
itemClickListener: ItemClickListener
) : RecyclerView.ViewHolder(binding.root) {
interface ItemClickListener {
fun onItemClick()
}
init {
binding.button.setOnClickListener {
itemClickListener.onItemClick()
}
}
fun bindTo(text: String) {
binding.textView.text = text
}
companion object {
fun inflate(parent: ViewGroup, itemClickListener: ItemClickListener) =
LinkViewHolder(
ItemDiscoverLinkBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
itemClickListener
)
}
}
- Keeps the view binding class imports inside the
ViewHolderclass. - Keeps binding logic inside the
ViewHolderclass.
SharedPreferences and settings
SharedPreferences should be edited by a dedicated settings class. Other code should get and set settings only through functions, not through the SharedPreferences APIs.
This will hide the actual APIs used to store settings so it's easier to replace and helps keep track of code that modifies settings.
User interface
Some relevant documentation:
Adding or removing a language
- Add or remove the
values-<tags>directory. When adding, take the strings file from the translation provider. - Update locales_config.xml to support per-app language preferences. See the link for supported language codes. They differ from the resource directory name!
Layout resources
View IDs should be unique across the project to support refactoring using Android Studio.
Example: textViewItemEpisodeTitle in item_episode.xml
Use dimension resources (like @dimen/default_padding) for margin and padding to avoid looking them up.
Icons
Use Material Symbols with
- Rounded style
- weight 400
- no grade
- typically 24dp size
- to auto-mirror vector drawables in RTL layouts, add
android:autoMirrored="true"
Some existing icons may still use the old Filled or the old non-rounded Outlined style.
Name icon resource files like ic_<name>_<tint>_<size>dp.xml,
for example ic_event_control_24dp.xml.
Load vector drawables using compat loading so they work (tinting) and do not crash (gradients) on all supported releases:
Button: useViewTools.setVectorDrawableTop, ... (usesAppCompatResources.getDrawable()) orapp:iconImageView,ImageButton: useapp:srcCompatTextView: useapp:drawableStartCompat,app:drawableTopCompat, ...- Optionally if the drawable is just a color
- Not for app widget layouts as the system initializes them
When using Picasso, make sure to not pass a drawable resource ID but a drawable loaded using
AppCompatResources.getDrawable() instead. Otherwise, the drawable will not be tinted correctly.
Dialogs
Using AppCompatDialogFragment and overriding onCreateView will use dialogTheme of the theme.
If possible, use an alert dialog with a custom layout instead for improved sizing, easy adding of title and buttons.
When it makes sense, use a colored button for the primary and especially destructive action:
?attr/sgButtonDialogPrimaryand?attr/sgButtonDialogErrorforButtonThemeOverlay.SeriesGuide.Dialog.PrimaryButtonWarnforMaterialAlertDialogBuilder
Alert dialogs (recommended)
Using AppCompatDialogFragment and overriding onCreateDialog with MaterialAlertDialogBuilder.
The dialog theme is materialAlertDialogTheme, which only sets android:windowMinWidthMajor and android:windowMinWidthMinor.
The layout used is abc_alert_dialog_material.xml defined via alertDialogStyle of the theme.
When using a custom layout via setView():
layout_widthandlayout_heightof the root view are overwritten tomatch_parent(seeandroidx.appcompat.app.AlertController#setupCustomContent)- its parents use
layout_width="match_parent",layout_height="wrap_content",minHeight="48dp"(seeabc_alert_dialog_material.xml) - its
customPanelparent is resized to take only as much space as is available (seeAlertDialogLayout)
OptionsMenu
Register a MenuProvider using ComponentActivity.addMenuProvider() instead of overriding onCreateOptionsMenu().
An example is documented in the androidx.activity Version 1.4.0-alpha01 release notes.
PopupMenu
Use androidx.appcompat.widget.PopupMenu so Material 3 styles are correctly applied.
(Could also use the platform one and set Widget.Material3.PopupMenu with android:popupMenuStyle,
but rather use the same implementation on all versions.)
TextInputLayout
When trying to make TextInputLayout (grow to) fill available height, if the contained TextInputEditText is too tall the counter or error text can get pushed outside of its bounds.
As it is a LinearLayout, until this is fixed resolve by using android:layout_weight="1" on the contained TextInputEditText:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldEditNote"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:counterEnabled="true"
app:counterMaxLength="500">
<!-- This can grow up to the height of the TextInputLayout by default and will push the
counter out of bounds. As TextInputLayout is a LinearLayout, set layout_weight="1"
to resolve. -->
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
</com.google.android.material.textfield.TextInputLayout>
Dependencies
To inspect dependencies with Gradle use
./gradlew :app:dependencies --configuration pureDebugCompileClasspath
./gradlew :app:dependencyInsight --configuration pureDebugAndroidTestRuntimeClasspath --dependency kotlinx-serialization-core-jvm
When adding a new dependency, list it and its license in credits.