From 674423f94ab2679f3ff40ff708c3fe44346c737d Mon Sep 17 00:00:00 2001
From: "Kevin T. Coughlin" <706967+KevinTCoughlin@users.noreply.github.com>
Date: Sat, 14 Dec 2024 21:45:40 -0800
Subject: [PATCH] Refactor
---
...brains_kotlin_kotlin_stdlib_jdk7_1_8_0.xml | 6 +-
...rains_kotlin_kotlin_stdlib_jdk7_1_8_20.xml | 6 +-
...brains_kotlin_kotlin_stdlib_jdk8_1_8_0.xml | 6 +-
...rains_kotlin_kotlin_stdlib_jdk8_1_8_20.xml | 6 +-
...rains_kotlin_kotlin_stdlib_jdk8_1_8_22.xml | 6 +-
.../smodr/viewholders/EpisodeView.kt | 7 +-
.../smodr/views/fragments/EpisodesFragment.kt | 69 ++-----
.../jamoka/adapter/BinderRecyclerAdapter.kt | 176 +++++++++++++-----
.../jamoka/fragment/BinderRecyclerFragment.kt | 33 +++-
9 files changed, 196 insertions(+), 119 deletions(-)
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_0.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_0.xml
index d6475d00..f0f5f00b 100644
--- a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_0.xml
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_0.xml
@@ -2,13 +2,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_20.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_20.xml
index 6e7d2680..5a155400 100644
--- a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_20.xml
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk7_1_8_20.xml
@@ -2,13 +2,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_0.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_0.xml
index 1418060d..e3b8b586 100644
--- a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_0.xml
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_0.xml
@@ -2,13 +2,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_20.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_20.xml
index f668b84f..b10c9a68 100644
--- a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_20.xml
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_20.xml
@@ -2,13 +2,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_22.xml b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_22.xml
index e89a374c..1ff1e245 100644
--- a/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_22.xml
+++ b/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_jdk8_1_8_22.xml
@@ -2,13 +2,13 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/Smodr/src/main/java/com/kevintcoughlin/smodr/viewholders/EpisodeView.kt b/Smodr/src/main/java/com/kevintcoughlin/smodr/viewholders/EpisodeView.kt
index 959e1e00..4e76755f 100644
--- a/Smodr/src/main/java/com/kevintcoughlin/smodr/viewholders/EpisodeView.kt
+++ b/Smodr/src/main/java/com/kevintcoughlin/smodr/viewholders/EpisodeView.kt
@@ -1,5 +1,6 @@
package com.kevintcoughlin.smodr.viewholders
+import BinderRecyclerAdapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
@@ -10,9 +11,9 @@ import java.text.SimpleDateFormat
import java.util.Locale
/**
- * Implementation of ItemBinder for binding Episode data to EpisodeViewHolder.
+ * Implementation of ViewHolderBinder for binding Episode data to EpisodeViewHolder.
*/
-class EpisodeView : BinderRecyclerAdapter.ItemBinder- {
+class EpisodeView : BinderRecyclerAdapter.ViewHolderBinder
- {
override fun bind(model: Item, viewHolder: EpisodeViewHolder) = with(viewHolder.binding) {
title.text = model.title
@@ -24,7 +25,7 @@ class EpisodeView : BinderRecyclerAdapter.ItemBinder
- {
)
}
- override fun createViewHolder(parent: ViewGroup, viewType: Int) =
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder =
EpisodeViewHolder(ItemListEpisodeLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false))
companion object {
diff --git a/Smodr/src/main/java/com/kevintcoughlin/smodr/views/fragments/EpisodesFragment.kt b/Smodr/src/main/java/com/kevintcoughlin/smodr/views/fragments/EpisodesFragment.kt
index ba29e8ad..df6b9e6c 100644
--- a/Smodr/src/main/java/com/kevintcoughlin/smodr/views/fragments/EpisodesFragment.kt
+++ b/Smodr/src/main/java/com/kevintcoughlin/smodr/views/fragments/EpisodesFragment.kt
@@ -1,12 +1,10 @@
package com.kevintcoughlin.smodr.views.fragments
-import BinderRecyclerAdapter
-import BinderRecyclerAdapterConfig
import android.os.Bundle
import android.view.View
import android.widget.Toast
-import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.cascadiacollections.jamoka.fragment.BinderRecyclerFragment
import com.kevintcoughlin.smodr.models.Channel
import com.kevintcoughlin.smodr.models.Feed
@@ -20,78 +18,51 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
+import BinderRecyclerAdapter
+import BinderRecyclerAdapterConfig
class EpisodesFragment : BinderRecyclerFragment(), Callback {
private val feedService: FeedService by lazy { createFeedService() }
private val adapter: BinderRecyclerAdapter
- by lazy {
BinderRecyclerAdapter(
- binder = EpisodeView(),
- config = BinderRecyclerAdapterConfig(
- enableDiffUtil = false
- )
+ viewHolderBinder = EpisodeView(),
+ config = BinderRecyclerAdapterConfig.Builder
- ()
+ .enableDiffUtil(false)
+ .build()
)
}
+ override fun configureRecyclerView(recyclerView: RecyclerView) {
+ recyclerView.apply {
+ setHasFixedSize(true)
+ layoutManager = LinearLayoutManager(context)
+ adapter = this@EpisodesFragment.adapter
+ }
+ }
+
override fun onRefresh() {
fetchEpisodes()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
-
- recyclerView.apply {
- setHasFixedSize(true)
- layoutManager = LinearLayoutManager(context)
- adapter = adapter
- }
-
- val dummyData = listOf(
- Item(
- guid = "1",
- title = "Episode 1",
- pubDate = "2024-11-01",
- description = "This is a description for Episode 1",
- duration = "25:00",
- summary = "Summary of Episode 1",
- origEnclosureLink = "https://example.com/episode1.mp3",
- completed = false
- ),
- Item(
- guid = "2",
- title = "Episode 2",
- pubDate = "2024-11-02",
- description = "This is a description for Episode 2",
- duration = "30:00",
- summary = "Summary of Episode 2",
- origEnclosureLink = "https://example.com/episode2.mp3",
- completed = false
- ),
- Item(
- guid = "3",
- title = "Episode 3",
- pubDate = "2024-11-03",
- description = "This is a description for Episode 3",
- duration = "40:00",
- summary = "Summary of Episode 3",
- origEnclosureLink = "https://example.com/episode3.mp3",
- completed = false
- )
- )
- adapter.updateItems(dummyData)
-// fetchEpisodes()
+ fetchEpisodes()
}
override fun onResponse(call: Call, response: Response) {
+ swipeRefreshLayout.isRefreshing = false
response.body()?.channel?.items?.let {
adapter.updateItems(it)
}
}
override fun onFailure(call: Call, t: Throwable) {
+ swipeRefreshLayout.isRefreshing = false
Toast.makeText(context, t.message, Toast.LENGTH_SHORT).show()
}
private fun fetchEpisodes() {
+ swipeRefreshLayout.isRefreshing = true
arguments?.getString(EPISODE_FEED_URL)?.let { feedUrl ->
feedService.feed(feedUrl).enqueue(this)
}
@@ -114,7 +85,7 @@ class EpisodesFragment : BinderRecyclerFragment(), Callback {
val TAG: String = EpisodesFragment::class.java.simpleName
@JvmStatic
- fun create(channel: Channel): Fragment {
+ fun create(channel: Channel): EpisodesFragment {
return EpisodesFragment().apply {
arguments = Bundle().apply {
putString(EPISODE_FEED_URL, channel.link)
diff --git a/common-android/src/main/java/com/cascadiacollections/jamoka/adapter/BinderRecyclerAdapter.kt b/common-android/src/main/java/com/cascadiacollections/jamoka/adapter/BinderRecyclerAdapter.kt
index 791508f8..21288d62 100644
--- a/common-android/src/main/java/com/cascadiacollections/jamoka/adapter/BinderRecyclerAdapter.kt
+++ b/common-android/src/main/java/com/cascadiacollections/jamoka/adapter/BinderRecyclerAdapter.kt
@@ -11,19 +11,33 @@ import androidx.recyclerview.widget.RecyclerView
*
* @param T The type of the data model.
* @param VH The ViewHolder type.
- * @param binder The ItemBinder responsible for binding data and creating view holders.
+ * @param viewHolderBinder The ViewHolderBinder responsible for binding data and creating view holders.
* @param config Optional configuration object to customize adapter behavior.
*/
class BinderRecyclerAdapter(
- private val binder: ItemBinder,
- private val config: BinderRecyclerAdapterConfig = BinderRecyclerAdapterConfig()
+ private val viewHolderBinder: ViewHolderBinder,
+ private val config: BinderRecyclerAdapterConfig = BinderRecyclerAdapterConfig.Builder().build()
) : RecyclerView.Adapter() {
/**
* Interface for binding items and creating ViewHolders.
*/
- interface ItemBinder {
+ interface ViewHolderBinder {
+ /**
+ * Binds the data model to the ViewHolder.
+ *
+ * @param model The data model item.
+ * @param viewHolder The ViewHolder to bind the data to.
+ */
fun bind(model: T, viewHolder: VH)
+
+ /**
+ * Creates a new ViewHolder instance.
+ *
+ * @param parent The parent ViewGroup.
+ * @param viewType The view type of the new View.
+ * @return A new ViewHolder instance.
+ */
fun createViewHolder(parent: ViewGroup, viewType: Int): VH
}
@@ -31,14 +45,28 @@ class BinderRecyclerAdapter(
* Optional callbacks for lifecycle events.
*/
interface AdapterCallback {
+ /**
+ * Called when an item has been bound to a ViewHolder.
+ *
+ * @param model The data model item.
+ * @param viewHolder The ViewHolder that was bound.
+ */
fun onItemBound(model: T, viewHolder: VH) {}
+
+ /**
+ * Called when a new ViewHolder has been created.
+ *
+ * @param viewHolder The newly created ViewHolder.
+ */
fun onViewHolderCreated(viewHolder: VH) {}
}
- private var items: List = EMPTY_LIST as List
+ private var items: List = emptyList()
/**
- * Updates items using DiffUtil for efficient rendering if enabled in the config.
+ * Updates the adapter's data set and uses DiffUtil for efficient rendering if enabled.
+ *
+ * @param newItems The new list of items.
*/
fun updateItems(newItems: List) {
if (config.enableDiffUtil) {
@@ -54,47 +82,53 @@ class BinderRecyclerAdapter(
/**
* Adds a single item to the list and notifies the adapter.
+ *
+ * @param item The item to add.
*/
fun addItem(item: T) {
- items = items + item
- notifyItemInserted(items.size - 1)
+ val updatedItems = items + item
+ updateItems(updatedItems)
}
/**
* Adds multiple items to the list and notifies the adapter.
+ *
+ * @param newItems The new items to add.
*/
fun addItems(newItems: List) {
- items = items + newItems
- notifyItemRangeInserted(items.size - newItems.size, newItems.size)
+ val updatedItems = items + newItems
+ updateItems(updatedItems)
}
/**
* Clears all items from the adapter.
*/
fun clearItems() {
- items = EMPTY_LIST as List
- notifyDataSetChanged()
+ updateItems(emptyList())
}
/**
* Removes an item at the specified position.
+ *
+ * @param position The position of the item to remove.
+ * @throws IndexOutOfBoundsException if the position is out of range.
*/
fun removeItemAt(position: Int) {
if (position in items.indices) {
- items = items.toMutableList().apply { removeAt(position) }
- notifyItemRemoved(position)
+ val updatedItems = items.toMutableList().also { it.removeAt(position) }
+ updateItems(updatedItems)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
- val viewHolder = binder.createViewHolder(parent, viewType)
+ val viewHolder = viewHolderBinder.createViewHolder(parent, viewType)
config.adapterCallback?.onViewHolderCreated(viewHolder)
return viewHolder
}
override fun onBindViewHolder(viewHolder: VH, position: Int) {
val item = items[position]
- binder.bind(item, viewHolder)
+ viewHolderBinder.bind(item, viewHolder)
config.adapterCallback?.onItemBound(item, viewHolder)
}
@@ -115,52 +149,108 @@ class BinderRecyclerAdapter(
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return oldList[oldItemPosition] == newList[newItemPosition]
+ val oldItem = oldList.getOrNull(oldItemPosition)
+ val newItem = newList.getOrNull(newItemPosition)
+ return if (oldItem != null && newItem != null) {
+ oldItem == newItem
+ } else {
+ oldItem == null && newItem == null
+ }
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return oldList[oldItemPosition] == newList[newItemPosition]
+ val oldItem = oldList.getOrNull(oldItemPosition)
+ val newItem = newList.getOrNull(newItemPosition)
+ return if (oldItem != null && newItem != null) {
+ oldItem == newItem // Rely on equals() for content comparison
+ } else {
+ oldItem == null && newItem == null
+ }
}
}
-
- companion object {
- @Suppress("UNCHECKED_CAST")
- private val EMPTY_LIST = listOf()
- }
}
/**
* Configuration class for BinderRecyclerAdapter to customize behavior.
+ * Uses the Builder pattern for better readability with multiple options.
*
- * @param enableDiffUtil Enables or disables the use of DiffUtil.
- * @param diffUtilCallback Custom implementation of DiffUtil.Callback.
- * @param adapterCallback Optional callback for lifecycle events.
- * @param viewTypeResolver Lambda for resolving item view types.
+ * @param T The type of the data model.
*/
-class BinderRecyclerAdapterConfig(
- var enableDiffUtil: Boolean = true,
- var diffUtilCallback: DiffUtil.Callback? = null,
- var adapterCallback: BinderRecyclerAdapter.AdapterCallback? = null,
- var viewTypeResolver: ((T) -> Int)? = null
-)
+class BinderRecyclerAdapterConfig private constructor(
+ val enableDiffUtil: Boolean,
+ val diffUtilCallback: DiffUtil.Callback?,
+ val adapterCallback: BinderRecyclerAdapter.AdapterCallback?,
+ val viewTypeResolver: ((T) -> Int)?
+) {
+ /**
+ * Builder class for creating BinderRecyclerAdapterConfig instances.
+ */
+ class Builder(
+ private var enableDiffUtil: Boolean = true,
+ private var diffUtilCallback: DiffUtil.Callback? = null,
+ private var adapterCallback: BinderRecyclerAdapter.AdapterCallback? = null,
+ private var viewTypeResolver: ((T) -> Int)? = null
+ ) {
+ /**
+ * Enables or disables the use of DiffUtil for item updates.
+ *
+ * @param enable True to enable DiffUtil, false otherwise.
+ */
+ fun enableDiffUtil(enable: Boolean) = apply { this.enableDiffUtil = enable }
+
+ /**
+ * Sets a custom DiffUtil.Callback implementation.
+ *
+ * @param callback The custom DiffUtil.Callback.
+ */
+ fun diffUtilCallback(callback: DiffUtil.Callback?) = apply { this.diffUtilCallback = callback }
+
+ /**
+ * Sets an optional AdapterCallback for lifecycle events.
+ *
+ * @param callback The AdapterCallback.
+ */
+ fun adapterCallback(callback: BinderRecyclerAdapter.AdapterCallback?) =
+ apply { this.adapterCallback = callback }
+
+ /**
+ * Sets a lambda function to resolve item view types based on the data model.
+ *
+ * @param resolver The view type resolver lambda.
+ */
+ fun viewTypeResolver(resolver: ((T) -> Int)?) = apply { this.viewTypeResolver = resolver }
+
+ /**
+ * Builds the BinderRecyclerAdapterConfig instance.
+ *
+ * @return The created BinderRecyclerAdapterConfig.
+ */
+ fun build(): BinderRecyclerAdapterConfig =
+ BinderRecyclerAdapterConfig(enableDiffUtil, diffUtilCallback, adapterCallback, viewTypeResolver)
+ }
+}
/**
- * Default implementation of ItemBinder for simple data-binding use cases.
+ * Default implementation of ViewHolderBinder for simple data-binding use cases.
*
- * @param layoutResId Resource ID of the layout to inflate for each item.
- * @param bindFunction Function to bind data to the ViewHolder's views.
+ * @param T The type of the data model.
+ * @param VH The type of the ViewHolder.
+ * @param layoutResId The resource ID of the layout to inflate for each item.
+ * @param onBindViewHolder Function to bind data to the ViewHolder's views.
+ * @param viewHolderCreator Lambda function to create a ViewHolder instance from a View.
*/
-class DefaultBinder(
+class DefaultBinder(
@LayoutRes private val layoutResId: Int,
- private val bindFunction: (T, View) -> Unit
-) : BinderRecyclerAdapter.ItemBinder {
+ private val onBindViewHolder: (T, VH) -> Unit,
+ private val viewHolderCreator: (View) -> VH
+) : BinderRecyclerAdapter.ViewHolderBinder {
- override fun bind(model: T, viewHolder: RecyclerView.ViewHolder) {
- bindFunction(model, viewHolder.itemView)
+ override fun bind(model: T, viewHolder: VH) {
+ onBindViewHolder(model, viewHolder)
}
- override fun createViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
- return object : RecyclerView.ViewHolder(view) {}
+ return viewHolderCreator(view)
}
}
\ No newline at end of file
diff --git a/common-android/src/main/java/com/cascadiacollections/jamoka/fragment/BinderRecyclerFragment.kt b/common-android/src/main/java/com/cascadiacollections/jamoka/fragment/BinderRecyclerFragment.kt
index 99c38104..9ff91da8 100644
--- a/common-android/src/main/java/com/cascadiacollections/jamoka/fragment/BinderRecyclerFragment.kt
+++ b/common-android/src/main/java/com/cascadiacollections/jamoka/fragment/BinderRecyclerFragment.kt
@@ -18,11 +18,8 @@ abstract class BinderRecyclerFragment(
@LayoutRes private val layoutResId: Int = R.layout.fragment_recycler_layout
) : Fragment(), SwipeRefreshLayout.OnRefreshListener {
- protected val swipeRefreshLayout: SwipeRefreshLayout
- get() = requireView().findViewById(R.id.swipeContainer)
-
- protected val recyclerView: RecyclerView
- get() = requireView().findViewById(R.id.list)
+ protected lateinit var swipeRefreshLayout: SwipeRefreshLayout
+ protected lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater,
@@ -33,10 +30,11 @@ abstract class BinderRecyclerFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ swipeRefreshLayout = view.findViewById(R.id.swipeContainer)
+ recyclerView = view.findViewById(R.id.list)
+
recyclerView.apply {
setHasFixedSize(true)
- layoutManager = layoutManager
- adapter = adapter
configureRecyclerView(this)
}
@@ -46,11 +44,28 @@ abstract class BinderRecyclerFragment(
}
}
+ /**
+ * Provides a hook for subclasses to configure the RecyclerView.
+ * This is where the LayoutManager and Adapter should be set.
+ *
+ * @param recyclerView The RecyclerView instance.
+ */
protected open fun configureRecyclerView(recyclerView: RecyclerView) {
- // Optional for subclasses
+ // Subclasses should set the LayoutManager and Adapter here
}
+ /**
+ * Provides a hook for subclasses to customize the SwipeRefreshLayout.
+ *
+ * @param swipeRefreshLayout The SwipeRefreshLayout instance.
+ */
protected open fun configureSwipeRefresh(swipeRefreshLayout: SwipeRefreshLayout) {
- // Optional for subclasses
+ // Optional for subclasses to customize further
}
+
+ /**
+ * Called when a swipe gesture triggers a refresh.
+ * Subclasses should implement their refresh logic here.
+ */
+ abstract override fun onRefresh()
}
\ No newline at end of file