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 ViewHolder class.
  • Keeps binding logic inside the ViewHolder class.

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

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: use ViewTools.setVectorDrawableTop, ... (uses AppCompatResources.getDrawable()) or app:icon
  • ImageView, ImageButton: use app:srcCompat
  • TextView: use app: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/sgButtonDialogPrimary and ?attr/sgButtonDialogError for Button
  • ThemeOverlay.SeriesGuide.Dialog.PrimaryButtonWarn for MaterialAlertDialogBuilder

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_width and layout_height of the root view are overwritten to match_parent (see androidx.appcompat.app.AlertController#setupCustomContent)
  • its parents use layout_width="match_parent", layout_height="wrap_content", minHeight="48dp" (see abc_alert_dialog_material.xml)
  • its customPanel parent is resized to take only as much space as is available (see AlertDialogLayout)

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.