Skip to the content.

FastMediaSorter v2: Architecture & Flow

Framework: Android Native (Kotlin 1.9+, Java 17). Pattern: Clean Architecture + MVVM + Hilt DI.

Module Structure

Data Flow

UIViewModelUseCaseRepositoryDataSource

Three-Layer Structure

Key Patterns

UI Patterns - Trigger Row (MANDATORY)

Every toggle/switch or checkbox control that carries a description must follow one of the two canonical row patterns below. Mixing the patterns or using ad-hoc sizes is prohibited.

Pattern A - Switch/Toggle row (settings fragments)

Canonical row layout is title + helper inline on the top line, with the subtitle directly under the title. Prefer the reusable SettingsToggleRow compound view (see “Reusable component” below) over hand-built LinearLayouts - the raw XML below is included for reference and one-off exceptions only.

<LinearLayout
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:minHeight="@dimen/button_height">

    <!-- 1. Trigger control (leftmost) - canonical on/off class is Material3 MaterialSwitch -->
    <com.google.android.material.materialswitch.MaterialSwitch
        android:layout_marginEnd="@dimen/settings_switch_margin_end" />

    <!-- 2. Text group (fills remaining width) -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_weight="1"
        android:orientation="vertical">

        <!-- 2a. Title line: title + helper inline (helper sits next to the title) -->
        <LinearLayout
            android:orientation="horizontal"
            android:gravity="center_vertical">

            <!-- Main label: always toggler_title_text_size (14sp) -->
            <TextView
                android:layout_width="wrap_content"
                android:textSize="@dimen/toggler_title_text_size" />

            <!-- Help icon button: inline next to the title (NOT rightmost) -->
            <ImageButton
                android:layout_width="@dimen/settings_help_icon_size"
                android:layout_height="@dimen/settings_help_icon_size"
                android:layout_marginStart="@dimen/settings_help_icon_margin"
                android:src="@drawable/ic_help_outline_24" />
        </LinearLayout>

        <!-- 2b. Subtitle: always toggler_desc_text_size (12sp) = title − 2sp -->
        <TextView
            android:textSize="@dimen/toggler_desc_text_size"
            android:textColor="@color/text_color_secondary" />
    </LinearLayout>

    <!-- 3. Optional trailing action slot (rare; e.g. an extra action button
         that belongs to the row). Empty/hidden by default. -->
</LinearLayout>

Rules:

Reusable component

The canonical implementation is com.sza.fastmediasorter.ui.common.widget.SettingsToggleRow (compound view) backed by view_settings_toggle_row.xml. It embeds the canonical Material3 MaterialSwitch, so wrapping a control in this component is the single recommended form for every new on/off toggle - in settings fragments, forms, AND dialogs. New switch rows MUST use this component instead of hand-rolled MaterialSwitch + TextView + ImageButton triplets. The component encapsulates title, subtitle, helper visibility, tooltip wiring, and the optional trailing action slot. Hand-built rows are technical debt and must be migrated when adjacent code is touched.

Any on/off switch that must stay outside SettingsToggleRow (e.g. a dense list-item row where the full toggle row would break the layout) MUST be a com.google.android.material.materialswitch.MaterialSwitch; it inherits the project materialSwitchStyle (themes.xml) so it matches the switch rendered inside the component.

Selection/value row (SettingsSelectionRow)

Pattern B - Checkbox row (add-resource, cloud folder pickers)

<LinearLayout android:orientation="vertical">

    <!-- 1. Trigger control -->
    <com.google.android.material.checkbox.MaterialCheckBox />
    <!-- MaterialCheckBox default text = 16sp (Material3 bodyLarge) -->

    <!-- 2. Help text: always text_size_small (14sp) = checkbox − 2sp -->
    <TextView
        android:layout_marginStart="@dimen/checkbox_subtitle_margin_start"
        android:textSize="@dimen/text_size_small"
        android:textColor="@color/text_color_secondary" />
</LinearLayout>

Rules:

Dimen reference

Dimen key Value Role
toggler_title_text_size 14sp Switch row main label
toggler_desc_text_size 12sp Switch row help text (title − 2sp)
text_size_small 14sp Checkbox row help text (checkbox − 2sp)
settings_switch_margin_end - Gap between switch and text group
settings_help_icon_size - Help icon button size
settings_help_icon_margin - Gap between text group and help icon
checkbox_subtitle_margin_start - Help text indent under checkbox

Button Taxonomy (MANDATORY)

One named Material3 style per semantic role, defined in values/themes.xml. The same role must look identical everywhere - do NOT introduce a plain <Button>, a raw Widget.MaterialComponents.*/Widget.Material3.* reference, or a one-off per-screen style for a role already covered below. Pick by the button’s role, not by how it should look.

Role Style When to use
Primary / confirm Widget.FastMediaSorter.Button.Filled The single main affirmative action of a screen or dialog (Save, OK, Grant, primary CTA). At most one per surface.
Secondary emphasis Widget.FastMediaSorter.Button.Tonal A secondary action that still needs weight next to the primary (alternative confirm, “Use anyway”).
Secondary Widget.FastMediaSorter.Button.Outlined Neutral secondary action paired with a Filled primary (Back, Choose, Browse).
Low-emphasis / cancel Widget.FastMediaSorter.Button.Text Link-like / inline dismiss (“Not now”, “Skip”) OUTSIDE a dialog action pair; anything that previously used ?android:attr/borderlessButtonStyle. For a dialog/bottom-sheet confirm-cancel pair use the S0538/S0684 DialogCancel slot below (soft-pink tonal), not this style.
Icon-only Widget.FastMediaSorter.Button.Icon Toolbar / inline icon actions that want a Material ripple and 48dp target.

Dialog action pair (S0538/S0684) - special-purpose, NOT the general role taxonomy. Use these (and only these) for the confirm/cancel pair of any non-system dialog, action-pair bottom sheet, or custom dialog layout. The pair is deliberately asymmetric so a blind finger tap (e.g. while driving) cannot miss or confuse the actions: the confirm/destructive slot is large (min dialog_action_button_min_height, ~56dp) and wide (dialog_confirm_button_min_width), while the cancel is intentionally shorter (dialog_cancel_button_min_height, 48dp) and narrower so the affirmative action dominates. A dialog_action_button_gap sits between them. Colour key: green = confirm, soft-pink tonal = cancel, saturated red = destructive confirm only. The “at most one Filled per surface” rule does not apply to this pair.

Slot Style Look
Confirm (OK / Save / Apply) Widget.FastMediaSorter.Button.DialogConfirm Green filled (@color/confirm_button_bg), wide (dialog_confirm_button_min_width) so it is the dominant “under-finger” action
Cancel Widget.FastMediaSorter.Button.DialogCancel Soft-pink tonal fill (@color/cancel_button_bg/cancel_button_on), deliberately SMALLER than the green confirm - shorter (dialog_cancel_button_min_height, 48dp touch floor) and narrower (content-sized vs the wide confirm) - so confirm dominates and cancel reads as the lighter escape. Saturated red is reserved for DialogDestructive only (S0684).
Destructive confirm (delete / remove / clear) Widget.FastMediaSorter.Button.DialogDestructive Red filled (@color/delete_button)

Seam: MaterialAlertDialogBuilder dialogs inherit this pair automatically via materialAlertDialogTheme on the app theme (positive -> DialogConfirm, negative/neutral -> DialogCancel) - no per-call edit. A destructive builder dialog opts into the red variant with the per-dialog overload MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_FastMediaSorter_MaterialAlertDialog_Destructive). Custom inflated layouts apply the named style directly on each MaterialButton. OS/system dialogs are exempt (we do not own their chrome).

Rules:

Standalone Player Toolbar Order (MANDATORY)

The four standalone hosts (PhotoVideoStandaloneActivity, TextStandaloneActivity, DocumentStandaloneActivity, AudioStandaloneActivity) share ONE top-toolbar button order so a file feels the same whichever host opened it (S0920). Each host declares its own activity_standalone_*.xml (portrait + layout-land/), so there is no single shared layout to enforce this - a new host or an edit must follow the order by hand.

Canonical order: Back -> [paging: Prev, Next, Random, Slideshow] -> Delete -> Favorite -> Share -> Info -> Rename -> [type-specific actions] -> Overflow.

Rules:

Internet Streams Subsystem

Dedicated screen for internet audio/video/RTSP sources. Architectural boundaries:

Performance & Resource Optimization

To maintain fast startup times (cold start), low memory consumption, and efficient CPU usage, the following patterns must be strictly enforced:

1. Lazy Dependency Injection (dagger.Lazy)

Heavy singletons, network managers, and protocol clients (e.g., SmbClient, SftpClient, DropboxClient) must NOT be eagerly injected into global scopes like Application or entry points like PlayerActivity.

2. Layout Optimization via ViewStub

Do not use android:visibility="gone" for complex, format-specific, or optional layout elements (e.g., search overlays, specific player controls, game modules) in main activity XML layouts.

3. On-Demand Media Lifecycle Management

Media players (ExoPlayer, MediaPlayer) and image loading caches (Glide) must only allocate system resources (decoders, native memory) when active playback is running.

4. Dynamic OS Component Gating

Optional background elements like widget receivers (AppWidgetProvider) should not consume system resources when disabled by user settings.

Collapsible Section Groups (MANDATORY)

New screens with collapsible/expandable sections MUST use the unified pattern (S0535) - do not build a bespoke header or persistence mechanism.