REDI-92: Views/XML renderer for the characters list (same MVI ViewModel)

Add :feature:characters:presentation-views — a classic Fragment + ViewBinding +
RecyclerView/DiffUtil renderer driving the SAME CharacterListViewModel as the Compose
screen (obtained via Koin's by viewModel()), proving the presentation logic is truly
UI-agnostic. State is observed with viewLifecycleOwner.repeatOnLifecycle(STARTED),
one-time Events are collected, UiText is resolved via Context, and the binding is nulled
in onDestroyView. Coil loads avatars into ImageView with a circle-crop transform; the
module has no Compose dependency.

Paging scroll listener guards the empty-list case (lastVisible >= 0), uses a safe
layoutManager cast, and is removed in onDestroyView.
This commit is contained in:
2026-06-10 13:44:53 +02:00
parent 5f2792002b
commit e230aa77d8
8 changed files with 376 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Solid white oval, tinted per status at bind time via setBackgroundTintList. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white" />
</shape>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationContentDescription="@string/cd_back"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/characters_views_title" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:scrollbars="vertical" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<LinearLayout
android:id="@+id/errorContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp"
android:visibility="gone">
<TextView
android:id="@+id/errorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<Button
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/characters_views_retry" />
</LinearLayout>
<ProgressBar
android:id="@+id/nextPageProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="16dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="4dp"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/avatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<View
android:id="@+id/statusDot"
android:layout_width="8dp"
android:layout_height="8dp"
android:background="@drawable/bg_status_dot" />
<TextView
android:id="@+id/statusSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<resources>
<string name="characters_views_title">Characters (Views)</string>
<string name="characters_views_retry">Retry</string>
<string name="cd_back">Back</string>
<string name="cd_character_avatar">Avatar of %1$s</string>
<string name="characters_views_status_alive">Alive</string>
<string name="characters_views_status_dead">Dead</string>
<string name="characters_views_status_unknown">Unknown</string>
</resources>