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