diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt index d6c15fc..44bf598 100644 --- a/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/src/main/kotlin/Deps.kt @@ -3,19 +3,19 @@ import org.gradle.api.JavaVersion object Config { object Build { - const val kotlinVersion = "1.6.0" + const val kotlinVersion = "1.6.10" const val compileSdk = 31 const val minSdk = 17 const val sampleMinSdk = 21 const val targetSdk = compileSdk - val javaVersion = JavaVersion.VERSION_1_8 + val javaVersion = JavaVersion.VERSION_11 const val packageNameDev = "io.github.netcosports.compositeadapter.sample" const val packageNameProd = "io.github.netcosports.compositeadapter.sample" - const val versionName = "1.0.1" + const val versionName = "1.0.2" const val versionOffset = 0 } @@ -37,16 +37,23 @@ object Config { object Deps { object AndroidX { - const val appcompat = "androidx.appcompat:appcompat:1.4.0" + const val appcompat = "androidx.appcompat:appcompat:1.4.1" const val swipeRefresh = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val recycler = "androidx.recyclerview:recyclerview:1.2.1" - const val lifecycleVersion = "2.4.0" + const val core = "androidx.core:core-ktx:1.7.0" + private const val lifecycleVersion = "2.4.0" + const val lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" const val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" const val lifecycleLiveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" } + object Material { + const val material = "com.google.android.material:material:1.5.0" + } + object Coroutines { - const val version = "1.5.2" + private const val version = "1.6.0" + const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" } @@ -56,12 +63,33 @@ object Config { } } + object Koin { + private const val version = "3.1.5" + const val core = "io.insert-koin:koin-core:$version" + const val android = "io.insert-koin:koin-android:$version" + } + object Kotlin { const val kotlinJdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Build.kotlinVersion}" } object Libs { const val compositeAdapter = ":composite-adapter" + + const val decorations = ":samples:decorations" + const val differentBindings = ":samples:different-bindings" + const val innerRecyclerview = ":samples:inner-recyclerview" + + const val stateAsCells = ":samples:state-as-cells:app" + const val baseCore = ":samples:state-as-cells:features:base:core" + const val baseUI = ":samples:state-as-cells:features:base:ui" + const val homeData = ":samples:state-as-cells:features:home:data" + const val homeCore = ":samples:state-as-cells:features:home:core" + const val homeUI = ":samples:state-as-cells:features:home:ui" + const val storiesCore = ":samples:state-as-cells:features:stories:core" + const val storiesUI = ":samples:state-as-cells:features:stories:ui" + const val newsCore = ":samples:state-as-cells:features:news:core" + const val newsUI = ":samples:state-as-cells:features:news:ui" } } } \ No newline at end of file diff --git a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/adapter/BaseCompositeAdapter.kt b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/adapter/BaseCompositeAdapter.kt index b18b5a1..837cbaa 100644 --- a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/adapter/BaseCompositeAdapter.kt +++ b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/adapter/BaseCompositeAdapter.kt @@ -93,21 +93,21 @@ abstract class BaseCompositeAdapter>( } protected open fun storeCellInHolder(holder: RecyclerView.ViewHolder, position: Int) { - holder.itemView.setCompositeAdapterItem(getItem(position)) + holder.setCompositeAdapterItem(getItem(position)) } override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { super.onViewAttachedToWindow(holder) - holder.itemView.getCompositeAdapterItem().onViewAttachedToWindow(holder) + holder.getCompositeAdapterItem().onViewAttachedToWindow(holder) } override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { super.onViewDetachedFromWindow(holder) - holder.itemView.getCompositeAdapterItem().onViewDetachedFromWindow(holder) + holder.getCompositeAdapterItem().onViewDetachedFromWindow(holder) } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { super.onViewRecycled(holder) - holder.itemView.getCompositeAdapterItem().onViewRecycled(holder) + holder.getCompositeAdapterItem().onViewRecycled(holder) } } \ No newline at end of file diff --git a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/decoration/SpaceItemDecoration.kt b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/decoration/SpaceItemDecoration.kt index 84332cd..73501dc 100644 --- a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/decoration/SpaceItemDecoration.kt +++ b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/decoration/SpaceItemDecoration.kt @@ -9,10 +9,10 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView open class SpaceItemDecoration( - val top: Int = 0, - val bottom: Int = 0, - val start: Int = 0, - val end: Int = 0 + open val top: Int = 0, + open val bottom: Int = 0, + open val start: Int = 0, + open val end: Int = 0 ) : ItemDecoration { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, item: ITEM) { diff --git a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/utils/CompositeAdapterExtensions.kt b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/utils/CompositeAdapterExtensions.kt index a8005b7..014bf77 100644 --- a/composite-adapter/src/main/java/com/originsdigital/compositeadapter/utils/CompositeAdapterExtensions.kt +++ b/composite-adapter/src/main/java/com/originsdigital/compositeadapter/utils/CompositeAdapterExtensions.kt @@ -8,6 +8,7 @@ import android.content.Context import android.view.View import androidx.recyclerview.widget.RecyclerView import com.originsdigital.compositeadapter.R +import com.originsdigital.compositeadapter.cell.Cell val RecyclerView.ViewHolder.context: Context get() = itemView.context @@ -17,6 +18,22 @@ var View.compositeAdapterViewHolder: RecyclerView.ViewHolder setTag(R.id.composite_adapter_view_holder_tag, value) } +fun > CELL.getCompositeAdapterItem(viewHolder: RecyclerView.ViewHolder): CELL { + return viewHolder.itemView.getCompositeAdapterItem() +} + +fun > CELL.getCompositeAdapterItem(root: View): CELL { + return root.getCompositeAdapterItem() +} + +fun RecyclerView.ViewHolder.getCompositeAdapterItem(): ITEM { + return itemView.getCompositeAdapterItem() +} + +fun RecyclerView.ViewHolder.setCompositeAdapterItem(item: ITEM) { + return itemView.setCompositeAdapterItem(item) +} + fun View.getCompositeAdapterItem(): ITEM { @Suppress("UNCHECKED_CAST") return getTag(R.id.composite_adapter_item_tag) as ITEM @@ -26,12 +43,17 @@ fun View.setCompositeAdapterItem(item: ITEM) { setTag(R.id.composite_adapter_item_tag, item) } +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Get 'ViewBinding' from the 'ViewHolder'" +) inline fun RecyclerView.ViewHolder.getViewBinding( crossinline delegate: (View) -> VIEW_BINDING ): VIEW_BINDING { val binding = itemView.getTag(R.id.composite_adapter_viewbinding_tag) return if (binding == null) { delegate(itemView).also { + @Suppress("DEPRECATION_ERROR") setViewBinding(it) } } else { @@ -39,6 +61,10 @@ inline fun RecyclerView.ViewHolder.getViewBinding( } } +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Save 'ViewBinding' in the 'ViewHolder'" +) fun RecyclerView.ViewHolder.setViewBinding(binding: VIEW_BINDING) { itemView.setTag(R.id.composite_adapter_viewbinding_tag, binding) } \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml deleted file mode 100644 index ad878eb..0000000 --- a/sample/src/main/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/domain/entity/MessageEntity.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/domain/entity/MessageEntity.kt deleted file mode 100644 index ed0d840..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/domain/entity/MessageEntity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.originsdigital.compositeadapter.sample.domain.entity - -data class MessageEntity( - val id: String, - val type: Type, - val text: String? -) { - enum class Type { - VIEW, VIEW_BINDING, DATA_BINDING - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/SampleApplication.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/SampleApplication.kt deleted file mode 100644 index 268661c..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/SampleApplication.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui - -import android.app.Application -import com.originsdigital.compositeadapter.sample.ui.di.SampleDI - -class SampleApplication : Application() { - - override fun onCreate() { - super.onCreate() - - SampleDI.init(this) - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/di/SampleDI.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/di/SampleDI.kt deleted file mode 100644 index 451c4c9..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/di/SampleDI.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.di - -import com.originsdigital.compositeadapter.sample.ui.SampleApplication - -class SampleDI { - - companion object { - - private lateinit var app: SampleApplication - - fun init(app: SampleApplication) { - this.app = app - } - - fun provideApp(): SampleApplication = app - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleActivity.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleActivity.kt deleted file mode 100644 index db877c5..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleActivity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.adapter.CompositeAdapter -import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration -import com.originsdigital.compositeadapter.sample.databinding.SampleActivityBinding -import com.originsdigital.compositeadapter.sample.ui.sample.vm.SampleViewModel - -class SampleActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val viewModel = ViewModelProvider(this)[SampleViewModel::class.java] - - val binding = SampleActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - - binding.apply { - swipeRefreshLayout.setOnRefreshListener { viewModel.onRefresh() } - - val adapter = CompositeAdapter() - val layoutManager = LinearLayoutManager(this@SampleActivity) - recyclerView.adapter = adapter - recyclerView.layoutManager = layoutManager - recyclerView.addItemDecoration(CompositeItemDecoration()) - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - viewModel.onLastItemVisibleChanged(layoutManager.findLastVisibleItemPosition()) - } - }) - - viewModel.state.refreshingData.observe(this@SampleActivity, Observer { isRefreshing -> - swipeRefreshLayout.isRefreshing = isRefreshing - }) - viewModel.state.cellsData.observe(this@SampleActivity, Observer { items -> - adapter.submitList(items) - }) - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleMapper.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleMapper.kt deleted file mode 100644 index bb32e81..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/SampleMapper.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample - -import android.app.Application -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import android.util.TypedValue -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.decoration.SpaceItemDecoration -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity -import com.originsdigital.compositeadapter.sample.ui.sample.cell.common.SampleErrorCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.common.SampleFullScreenErrorCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.common.SampleFullScreenLoaderCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.common.SampleLoaderCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.databinding.SampleDataBindingMessageCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.view.SampleViewMessageCell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.viewbinding.SampleViewBindingMessageCell -import java.util.* - -class SampleMapper( - app: Application, - private val itemsPerPage: Int, - private val viewMessageClickListener: (ClickItem) -> Unit, - private val viewBindingMessageClickListener: (ClickItem) -> Unit, - private val dataBindingMessageClickListener: (ClickItem) -> Unit, -) { - - private val firstItemDecoration: SpaceItemDecoration> - private val middleSmallItemDecoration: SpaceItemDecoration> - private val middleBigItemDecoration: SpaceItemDecoration> - private val lastItemDecoration: SpaceItemDecoration> - - init { - val space = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, app.resources.displayMetrics).toInt() - firstItemDecoration = SpaceItemDecoration(top = 8 * space, bottom = space, start = 8 * space, end = 8 * space) - middleSmallItemDecoration = object : SpaceItemDecoration>( - top = 2 * space, bottom = 2 * space, start = 4 * space, end = 4 * space - ) { - private val itemBounds = Rect() - private val dividerPaint: Paint = Paint().apply { - this.color = Color.GREEN - this.style = Paint.Style.FILL - this.isAntiAlias = true - } - - override fun onDraw(canvas: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, item: Cell<*>) { - super.onDraw(canvas, view, parent, state, item) - parent.layoutManager?.getDecoratedBoundsWithMargins(view, itemBounds) - canvas.drawRect( - itemBounds.left.toFloat(), - itemBounds.top.toFloat(), - itemBounds.right.toFloat(), - itemBounds.bottom.toFloat(), - dividerPaint - ) - } - } - middleBigItemDecoration = SpaceItemDecoration(top = space, bottom = space, start = space, end = space) - lastItemDecoration = SpaceItemDecoration(top = space, bottom = 8 * space, start = 8 * space, end = 8 * space) - } - - fun mapToCells(messages: List, extraCell: Cell<*>? = null): List> { - val result = mutableListOf>() - - messages.mapIndexedTo(result) { i, message -> - val decoration = when { - i == 0 -> firstItemDecoration - i == messages.size - 1 -> lastItemDecoration - else -> { - if (i % ((itemsPerPage + messages.size) / itemsPerPage) == 0) { - middleSmallItemDecoration - } else { - middleBigItemDecoration - } - } - } - when (message.type) { - MessageEntity.Type.VIEW -> SampleViewMessageCell( - message.copy(text = "ViewMessageCell ${message.text}"), - decoration = decoration, - onClickListener = viewMessageClickListener - ) - MessageEntity.Type.VIEW_BINDING -> SampleViewBindingMessageCell( - message.copy(text = "ViewBindingMessageCell ${message.text}"), - decoration = decoration, - onClickListener = viewBindingMessageClickListener - ) - MessageEntity.Type.DATA_BINDING -> SampleDataBindingMessageCell( - message.copy(text = "DataBindingMessageCell ${message.text}"), - decoration = decoration, - onClickListener = dataBindingMessageClickListener - ) - } - } - if (extraCell != null) { - result.add(extraCell) - } - - return result - } - - fun changeMessageType(messages: List, input: MessageEntity): List { - return messages.map { message -> - if (input.id == message.id) { - message.copy(type = MessageEntity.Type.values().toList().minus(input.type).random()) - } else { - message - } - } - } - - fun getErrorMessage(random: Random): String { - val diff = 'z' - 'a' - val errorMessage = (0..5).map { - ('a' + random.nextInt(diff)).let { char -> - if (random.nextBoolean()) { - char - } else { - char.uppercase() - } - } - }.joinToString("") - return "Error $errorMessage" - } - - fun getLoaderCell(fullscreen: Boolean): Cell<*> { - return if (fullscreen) { - SampleFullScreenLoaderCell() - } else { - SampleLoaderCell() - } - } - - fun getErrorCell(error: String, fullscreen: Boolean, onRetryClickListener: () -> Unit): Cell<*> { - return if (fullscreen) { - SampleFullScreenErrorCell(data = error, onClickListener = { onRetryClickListener() }) - } else { - SampleErrorCell(data = error, onClickListener = { onRetryClickListener() }) - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleCellViewHolder.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleCellViewHolder.kt deleted file mode 100644 index 33a3143..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleCellViewHolder.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.common - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -open class SampleCellViewHolder(view: View) : RecyclerView.ViewHolder(view) \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleErrorCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleErrorCell.kt deleted file mode 100644 index 0def71a..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleErrorCell.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.common - -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.sample.R -import com.originsdigital.compositeadapter.sample.databinding.CommonErrorListItemBinding -import com.originsdigital.compositeadapter.sample.ui.sample.cell.viewbinding.SampleViewBindingCell -import com.originsdigital.compositeadapter.utils.getViewBinding - -data class SampleErrorCell( - override val uniqueId: String = "ErrorCell", - override val data: String, - override val onClickListener: ((ClickItem) -> Unit) -) : SampleViewBindingCell { - - override val layoutId: Int = R.layout.common_error_list_item - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder.getViewBinding(CommonErrorListItemBinding::bind)).apply { - message.text = data - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenErrorCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenErrorCell.kt deleted file mode 100644 index 158939d..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenErrorCell.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.common - -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.sample.R -import com.originsdigital.compositeadapter.sample.databinding.CommonFullscreenErrorListItemBinding -import com.originsdigital.compositeadapter.sample.ui.sample.cell.viewbinding.SampleViewBindingCell -import com.originsdigital.compositeadapter.utils.getViewBinding - -data class SampleFullScreenErrorCell( - override val uniqueId: String = "FullScreenErrorCell", - override val data: String, - override val onClickListener: ((ClickItem) -> Unit) -) : SampleViewBindingCell { - - override val layoutId: Int = R.layout.common_fullscreen_error_list_item - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder.getViewBinding(CommonFullscreenErrorListItemBinding::bind)).apply { - message.text = data - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenLoaderCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenLoaderCell.kt deleted file mode 100644 index e449372..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleFullScreenLoaderCell.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.common - -import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ProgressBar -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.R - -data class SampleFullScreenLoaderCell( - override val uniqueId: String = "FullScreenLoaderCell" -) : Cell { - - override val data: Unit = Unit - override val layoutId: Int = R.layout.common_fullscreen_loader_list_item - - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return SampleCellViewHolder(FrameLayout(inflater.context).apply { - layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, - RecyclerView.LayoutParams.MATCH_PARENT - ) - addView(ProgressBar(inflater.context, null, android.R.attr.progressBarStyleLarge).apply { - layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.CENTER - ) - }) - }) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleLoaderCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleLoaderCell.kt deleted file mode 100644 index beefcca..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/common/SampleLoaderCell.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.common - -import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ProgressBar -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.R - -data class SampleLoaderCell( - override val uniqueId: String = "LoaderCell" -) : Cell { - - override val data: Unit = Unit - override val layoutId: Int = R.layout.common_loader_list_item - - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return SampleCellViewHolder(FrameLayout(inflater.context).apply { - layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, - RecyclerView.LayoutParams.WRAP_CONTENT - ) - addView(ProgressBar(inflater.context, null, android.R.attr.progressBarStyleLarge).apply { - layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.CENTER_HORIZONTAL - ) - }) - }) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingCell.kt deleted file mode 100644 index 4414a50..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingCell.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.databinding - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.BR - -interface SampleDataBindingCell : Cell { - - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DataBindingViewHolder.create(inflater, layoutId, parent) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as DataBindingViewHolder).apply { - bindings.setVariable(BR.item, data) - bindings.executePendingBindings() - } - } - - class DataBindingViewHolder(val bindings: ViewDataBinding) : RecyclerView.ViewHolder(bindings.root) { - - companion object { - fun create(inflater: LayoutInflater, layoutResId: Int, parent: ViewGroup): DataBindingViewHolder { - return DataBindingViewHolder(DataBindingUtil.inflate(inflater, layoutResId, parent, false)) - } - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingMessageCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingMessageCell.kt deleted file mode 100644 index ac07467..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/databinding/SampleDataBindingMessageCell.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.databinding - -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.decoration.ItemDecoration -import com.originsdigital.compositeadapter.sample.R -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity - -data class SampleDataBindingMessageCell( - override val data: MessageEntity, - override val decoration: ItemDecoration>?, - override val onClickListener: ((ClickItem) -> Unit) -) : SampleDataBindingCell { - - override val uniqueId: String = data.id - override val layoutId: Int = R.layout.sample_databinding_message_list_item -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/view/SampleViewMessageCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/view/SampleViewMessageCell.kt deleted file mode 100644 index f634c05..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/view/SampleViewMessageCell.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.view - -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.widget.TextViewCompat -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.decoration.ItemDecoration -import com.originsdigital.compositeadapter.sample.R -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity - -data class SampleViewMessageCell( - override val data: MessageEntity, - override val decoration: ItemDecoration>?, - override val onClickListener: ((ClickItem) -> Unit) -) : Cell { - - override val uniqueId: String = data.id - override val layoutId: Int = R.layout.sample_view_message_list_item - - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20f, parent.resources.displayMetrics).toInt() - return ViewMessageViewHolder( - AppCompatTextView(inflater.context).apply { - id = R.id.text - layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, - RecyclerView.LayoutParams.WRAP_CONTENT - ) - setPadding(padding, padding, padding, padding) - val outValue = TypedValue() - context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) - setBackgroundResource(outValue.resourceId) - TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat_Medium) - } - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as ViewMessageViewHolder).apply { - text.text = data.text - } - } - - private class ViewMessageViewHolder(val text: TextView) : RecyclerView.ViewHolder(text) -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingCell.kt deleted file mode 100644 index 74cdd19..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingCell.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.viewbinding - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.ui.sample.cell.common.SampleCellViewHolder - -interface SampleViewBindingCell : Cell { - - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return SampleCellViewHolder(inflater.inflate(layoutId, parent, false)) - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingMessageCell.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingMessageCell.kt deleted file mode 100644 index 5712d9e..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/cell/viewbinding/SampleViewBindingMessageCell.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.cell.viewbinding - -import androidx.recyclerview.widget.RecyclerView -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.cell.ClickItem -import com.originsdigital.compositeadapter.decoration.ItemDecoration -import com.originsdigital.compositeadapter.sample.R -import com.originsdigital.compositeadapter.sample.databinding.SampleViewbindingMessageListItemBinding -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity -import com.originsdigital.compositeadapter.utils.getViewBinding - -data class SampleViewBindingMessageCell( - override val data: MessageEntity, - override val decoration: ItemDecoration>?, - override val onClickListener: ((ClickItem) -> Unit)? -) : SampleViewBindingCell { - - override val uniqueId: String = data.id - override val layoutId: Int = R.layout.sample_viewbinding_message_list_item - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder.getViewBinding(SampleViewbindingMessageListItemBinding::bind)).apply { - text.text = data.text - } - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleState.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleState.kt deleted file mode 100644 index 9e628cf..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.vm - -import androidx.lifecycle.MutableLiveData -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity - -class SampleState { - - var messages: List = emptyList() - val cellsData: MutableLiveData>> = MutableLiveData(emptyList()) - val refreshingData: MutableLiveData = MutableLiveData(false) - - fun setData(messages: List, cells: List>) { - this.messages = messages - this.cellsData.value = cells - setRefreshing(false) - } - - fun setRefreshing(isRefreshing: Boolean) { - refreshingData.value = isRefreshing - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleViewModel.kt b/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleViewModel.kt deleted file mode 100644 index f252f87..0000000 --- a/sample/src/main/java/com/originsdigital/compositeadapter/sample/ui/sample/vm/SampleViewModel.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.originsdigital.compositeadapter.sample.ui.sample.vm - -import android.app.Application -import android.widget.Toast -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.originsdigital.compositeadapter.cell.Cell -import com.originsdigital.compositeadapter.sample.domain.entity.MessageEntity -import com.originsdigital.compositeadapter.sample.ui.di.SampleDI -import com.originsdigital.compositeadapter.sample.ui.sample.SampleMapper -import kotlinx.coroutines.* -import java.util.* - -class SampleViewModel : ViewModel() { - - val state: SampleState = SampleState() - - private val app: Application = SampleDI.provideApp() - private var toast: Toast? = null - private val mapper = SampleMapper( - app = app, - itemsPerPage = ITEMS_PER_PAGE, - viewMessageClickListener = { - toast?.cancel() - toast = Toast.makeText(app, "viewMessageClickListener", Toast.LENGTH_SHORT).also { it.show() } - }, - viewBindingMessageClickListener = { click -> - changeMessageType(click.item) - }, - dataBindingMessageClickListener = { click -> - changeMessageType(click.item) - } - ) - - private var nextPage: Int = FIRST_PAGE - private var isPaginationByScrollAllowed = true - - private var loadJob: Job? = null - private var clickJob: Job? = null - - init { - loadData(nextPage) - } - - fun onRefresh() { - loadData(FIRST_PAGE) - } - - fun onLastItemVisibleChanged(position: Int) { - val currentItemsSize = state.cellsData.value.orEmpty().size - isPaginationByScrollAllowed = position < currentItemsSize - 1 - if (isPaginationByScrollAllowed && position > currentItemsSize - ITEMS_PER_PAGE / 2) { - if (loadJob?.isActive != true) { - isPaginationByScrollAllowed = false - loadData(nextPage) - } - } - } - - private fun loadData(page: Int) { - loadJob?.cancel() - if (page >= LAST_PAGE) { - state.setRefreshing(false) - return - } - loadJob = viewModelScope.launch { - val isReload = page == FIRST_PAGE - val messagesOnLoading: List - val cellsOnLoading: List> - withContext(Dispatchers.IO) { - messagesOnLoading = if (isReload) { - emptyList() - } else { - state.messages - } - cellsOnLoading = mapper.mapToCells( - messages = messagesOnLoading, - extraCell = mapper.getLoaderCell(fullscreen = messagesOnLoading.isEmpty()) - ) - } - state.setData(messagesOnLoading, cellsOnLoading) - - val newMessages: List - val newCells: List> - withContext(Dispatchers.IO) { - val random = Random() - delay(3000) - val isSuccess = random.nextInt(100) < 75 - - if (isSuccess) { - nextPage = getNextPage(page) - isPaginationByScrollAllowed = true - val messages = (0..ITEMS_PER_PAGE).map { index -> - val id = page * ITEMS_PER_PAGE + index - val type = when (random.nextInt(3)) { - 0 -> MessageEntity.Type.VIEW - 1 -> MessageEntity.Type.VIEW_BINDING - else -> MessageEntity.Type.DATA_BINDING - } - MessageEntity(id = id.toString(), type = type, text = "Item $id") - } - newMessages = if (isReload) { - messages - } else { - state.messages + messages - } - newCells = mapper.mapToCells(newMessages) - } else { - newMessages = state.messages - newCells = mapper.mapToCells( - messages = newMessages, - extraCell = mapper.getErrorCell( - mapper.getErrorMessage(random), - fullscreen = newMessages.isEmpty(), - onRetryClickListener = { loadData(page) } - ) - ) - } - } - state.setData(newMessages, newCells) - } - } - - private fun changeMessageType(input: MessageEntity) { - clickJob?.cancel() - clickJob = viewModelScope.launch { - val newMessages = withContext(Dispatchers.IO) { - mapper.changeMessageType(state.messages, input) - } - state.setData(newMessages, mapper.mapToCells(newMessages)) - } - } - - private fun getNextPage(currentPage: Int): Int = currentPage + 1 - - override fun onCleared() { - super.onCleared() - clickJob?.cancel() - loadJob?.cancel() - toast?.cancel() - toast = null - } - - companion object { - private const val FIRST_PAGE = 0 - private const val LAST_PAGE = 3 - private const val ITEMS_PER_PAGE = 10 - } -} \ No newline at end of file diff --git a/sample/src/main/res/drawable/common_ic_big_refresh.xml b/sample/src/main/res/drawable/common_ic_big_refresh.xml deleted file mode 100644 index fe6defb..0000000 --- a/sample/src/main/res/drawable/common_ic_big_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/sample/src/main/res/drawable/common_ic_refresh.xml b/sample/src/main/res/drawable/common_ic_refresh.xml deleted file mode 100644 index f9eade0..0000000 --- a/sample/src/main/res/drawable/common_ic_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/sample/src/main/res/layout/common_error_list_item.xml b/sample/src/main/res/layout/common_error_list_item.xml deleted file mode 100644 index 250a16b..0000000 --- a/sample/src/main/res/layout/common_error_list_item.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sample/src/main/res/layout/common_fullscreen_error_list_item.xml b/sample/src/main/res/layout/common_fullscreen_error_list_item.xml deleted file mode 100644 index 5d2fcb6..0000000 --- a/sample/src/main/res/layout/common_fullscreen_error_list_item.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml deleted file mode 100644 index 4faecfa..0000000 --- a/sample/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #6200EE - #3700B3 - #03DAC5 - \ No newline at end of file diff --git a/sample/src/main/res/values/ids.xml b/sample/src/main/res/values/ids.xml deleted file mode 100644 index 9592d31..0000000 --- a/sample/src/main/res/values/ids.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml deleted file mode 100644 index 84c5b88..0000000 --- a/sample/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - CompositeAdapter Sample - \ No newline at end of file diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml deleted file mode 100644 index 7fb1e2d..0000000 --- a/sample/src/main/res/values/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/sample/.gitignore b/samples/basic/.gitignore similarity index 100% rename from sample/.gitignore rename to samples/basic/.gitignore diff --git a/sample/build.gradle.kts b/samples/basic/build.gradle.kts similarity index 77% rename from sample/build.gradle.kts rename to samples/basic/build.gradle.kts index 693f378..2a5adc1 100644 --- a/sample/build.gradle.kts +++ b/samples/basic/build.gradle.kts @@ -1,14 +1,13 @@ plugins { id(Config.Plugins.androidApp) kotlin(Config.Plugins.android) - kotlin(Config.Plugins.kapt) } android { compileSdk = Config.Build.compileSdk defaultConfig { - minSdk = Config.Build.minSdk + minSdk = Config.Build.sampleMinSdk targetSdk = Config.Build.targetSdk versionCode = getCustomVersionCode() @@ -47,21 +46,24 @@ android { } buildFeatures { - dataBinding = true viewBinding = true + //only for the `different-bindings` module and only if dataBinding is used + dataBinding = true } } dependencies { implementation(Config.Deps.Kotlin.kotlinJdk8) - implementation(Config.Deps.Coroutines.android) implementation(Config.Deps.AndroidX.appcompat) implementation(Config.Deps.AndroidX.recycler) - implementation(Config.Deps.AndroidX.swipeRefresh) - implementation(Config.Deps.AndroidX.lifecycleViewModelKtx) - implementation(Config.Deps.AndroidX.lifecycleLiveDataKtx) + implementation(Config.Deps.Material.material) // implementation(Config.Deps.CompositeAdapter.compositeAdapter) implementation(project(Config.Deps.Libs.compositeAdapter)) + + implementation(project(Config.Deps.Libs.decorations)) + implementation(project(Config.Deps.Libs.differentBindings)) + implementation(project(Config.Deps.Libs.innerRecyclerview)) + implementation(project(Config.Deps.Libs.stateAsCells)) } \ No newline at end of file diff --git a/sample/proguard-rules.pro b/samples/basic/proguard-rules.pro similarity index 100% rename from sample/proguard-rules.pro rename to samples/basic/proguard-rules.pro diff --git a/samples/basic/src/main/AndroidManifest.xml b/samples/basic/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6571584 --- /dev/null +++ b/samples/basic/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesActivity.kt b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesActivity.kt new file mode 100644 index 0000000..9477a05 --- /dev/null +++ b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesActivity.kt @@ -0,0 +1,80 @@ +package com.originsdigital.compositeadapter.sample.basic.ui + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.sample.basic.ui.cell.SampleCell +import com.originsdigital.compositeadapter.sample.basic.ui.entity.SampleUI +import com.originsdigital.compositeadapter.sample.decorations.ui.DecorationsActivity +import com.originsdigital.compositeadapter.sample.differentbindings.ui.DifferentBindingsActivity +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.InnerRecyclerActivity +import com.originsdigital.compositeadapter.stateascells.home.ui.HomeActivity + +class SamplesActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(generateView()) + } + + private fun generateView(): View { + val compositeAdapter = CompositeAdapter().apply { + submitList(generateData()) + } + val recyclerView = RecyclerView(this).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + adapter = compositeAdapter + layoutManager = LinearLayoutManager(context) + addItemDecoration(CompositeItemDecoration()) + } + return recyclerView + } + + //should be inside ViewModel + private fun generateData(): List> { + return SampleUI.Type.values().map { type -> + val name = when (type) { + SampleUI.Type.DECORATIONS -> "Decorations" + SampleUI.Type.DIFFERENT_BINDINGS -> "ViewBindings/DataBindings/Programmatically View" + SampleUI.Type.INNER_RECYCLER -> "Inner RecyclerView/ViewPager/Etc and how to save scroll state" + SampleUI.Type.STATE_AS_CELLS -> "State as List of Cells" + } + val onClickListener: (ClickItem) -> Unit = { item -> + startActivity( + when (item.item.type) { + SampleUI.Type.DECORATIONS -> { + DecorationsActivity.getLaunchIntent(this) + } + SampleUI.Type.DIFFERENT_BINDINGS -> { + DifferentBindingsActivity.getLaunchIntent(this) + } + SampleUI.Type.INNER_RECYCLER -> { + InnerRecyclerActivity.getLaunchIntent(this) + } + SampleUI.Type.STATE_AS_CELLS -> { + HomeActivity.getLaunchIntent(this) + } + } + ) + } + SampleCell( + data = SampleUI( + name = name, + type = type + ), + onClickListener = onClickListener + ) + } + } +} \ No newline at end of file diff --git a/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesApplication.kt b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesApplication.kt new file mode 100644 index 0000000..a2b4661 --- /dev/null +++ b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/SamplesApplication.kt @@ -0,0 +1,13 @@ +package com.originsdigital.compositeadapter.sample.basic.ui + +import android.app.Application +import com.originsdigital.compositeadapter.stateascells.app.StateAsCellsDI + +class SamplesApplication : Application() { + + override fun onCreate() { + super.onCreate() + + StateAsCellsDI.init(this) + } +} \ No newline at end of file diff --git a/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/cell/SampleCell.kt b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/cell/SampleCell.kt new file mode 100644 index 0000000..36858a3 --- /dev/null +++ b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/cell/SampleCell.kt @@ -0,0 +1,39 @@ +package com.originsdigital.compositeadapter.sample.basic.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.basic.R +import com.originsdigital.compositeadapter.sample.basic.databinding.SampleCellBinding +import com.originsdigital.compositeadapter.sample.basic.ui.entity.SampleUI + +data class SampleCell( + override val data: SampleUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : Cell { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.sample_cell + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return SampleViewHolder(SampleCellBinding.inflate(inflater, parent, false)) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as SampleViewHolder).binding.apply { + text.text = data.name + } + } + + private class SampleViewHolder( + val binding: SampleCellBinding + ) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/entity/SampleUI.kt b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/entity/SampleUI.kt new file mode 100644 index 0000000..35f28d6 --- /dev/null +++ b/samples/basic/src/main/java/com/originsdigital/compositeadapter/sample/basic/ui/entity/SampleUI.kt @@ -0,0 +1,10 @@ +package com.originsdigital.compositeadapter.sample.basic.ui.entity + +data class SampleUI( + val name: String, + val type: Type +) { + enum class Type { + DECORATIONS, DIFFERENT_BINDINGS, INNER_RECYCLER, STATE_AS_CELLS + } +} \ No newline at end of file diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from sample/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to samples/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/samples/basic/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from sample/src/main/res/drawable/ic_launcher_background.xml rename to samples/basic/src/main/res/drawable/ic_launcher_background.xml diff --git a/samples/basic/src/main/res/layout/sample_cell.xml b/samples/basic/src/main/res/layout/sample_cell.xml new file mode 100644 index 0000000..04cff49 --- /dev/null +++ b/samples/basic/src/main/res/layout/sample_cell.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to samples/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to samples/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/samples/basic/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/mipmap-hdpi/ic_launcher.png rename to samples/basic/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/samples/basic/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from sample/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to samples/basic/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/samples/basic/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/mipmap-mdpi/ic_launcher.png rename to samples/basic/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/samples/basic/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from sample/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to samples/basic/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/samples/basic/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/mipmap-xhdpi/ic_launcher.png rename to samples/basic/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/samples/basic/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to samples/basic/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/samples/basic/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to samples/basic/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/samples/basic/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to samples/basic/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/samples/basic/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to samples/basic/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/samples/basic/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to samples/basic/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/samples/basic/src/main/res/values/strings.xml b/samples/basic/src/main/res/values/strings.xml new file mode 100644 index 0000000..5564fc3 --- /dev/null +++ b/samples/basic/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CompositeAdapter Samples + \ No newline at end of file diff --git a/samples/basic/src/main/res/values/themes.xml b/samples/basic/src/main/res/values/themes.xml new file mode 100644 index 0000000..bf972ad --- /dev/null +++ b/samples/basic/src/main/res/values/themes.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/samples/decorations/.gitignore b/samples/decorations/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/decorations/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/decorations/build.gradle.kts b/samples/decorations/build.gradle.kts new file mode 100644 index 0000000..ee95cc0 --- /dev/null +++ b/samples/decorations/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + buildTypes { + getByName("debug") { + } + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(Config.Deps.Kotlin.kotlinJdk8) + + implementation(Config.Deps.AndroidX.appcompat) + implementation(Config.Deps.AndroidX.swipeRefresh) + implementation(Config.Deps.AndroidX.recycler) + +// implementation(Config.Deps.CompositeAdapter.compositeAdapter) + implementation(project(Config.Deps.Libs.compositeAdapter)) +} \ No newline at end of file diff --git a/samples/decorations/proguard-rules.pro b/samples/decorations/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/samples/decorations/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/decorations/src/main/AndroidManifest.xml b/samples/decorations/src/main/AndroidManifest.xml new file mode 100644 index 0000000..30dccc7 --- /dev/null +++ b/samples/decorations/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/DecorationsActivity.kt b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/DecorationsActivity.kt new file mode 100644 index 0000000..ecef19c --- /dev/null +++ b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/DecorationsActivity.kt @@ -0,0 +1,176 @@ +package com.originsdigital.compositeadapter.sample.decorations.ui + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.decorations.ui.cell.DecorationsCell +import com.originsdigital.compositeadapter.sample.decorations.ui.decorations.SampleItemDecoration +import com.originsdigital.compositeadapter.sample.decorations.ui.entity.DecorationsUI +import kotlin.random.Random + +class DecorationsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val dataHolder = DataHolder( + space = dpToPx(20f).toInt(), + radius = dpToPx(8f), + strokeWidth = dpToPx(2f), + dividerHeight = dpToPx(1f).toInt(), + dividerColorInt = Color.GRAY, + backgroundColorInt = Color.DKGRAY + ) + setContentView(generateView(dataHolder)) + } + + private fun Context.dpToPx(dp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) + } + + private fun generateView(dataHolder: DataHolder): View { + val compositeAdapter = CompositeAdapter().apply { + submitList(dataHolder.generateData()) + } + val swipeRefreshLayout = SwipeRefreshLayout(this).apply { + setOnRefreshListener { + compositeAdapter.submitList(dataHolder.generateData()) { + isRefreshing = false + } + } + } + val recyclerView = RecyclerView(this).apply { + adapter = compositeAdapter + layoutManager = LinearLayoutManager(context) + addItemDecoration(CompositeItemDecoration()) + } + swipeRefreshLayout.addView( + recyclerView, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + return swipeRefreshLayout + } + + + companion object { + fun getLaunchIntent(context: Context): Intent { + return Intent(context, DecorationsActivity::class.java) + } + } + + //should be inside ViewModel/UIMapper + private class DataHolder( + space: Int, + radius: Float, + strokeWidth: Float, + dividerHeight: Int, + @ColorInt dividerColorInt: Int, + @ColorInt backgroundColorInt: Int + ) { + + private val singleItemDecoration: ItemDecoration> + private val topItemDecoration: ItemDecoration> + private val middleItemDecoration: ItemDecoration> + private val bottomItemDecoration: ItemDecoration> + + init { + singleItemDecoration = SampleItemDecoration( + type = SampleItemDecoration.Type.SINGLE, + radius = radius, + strokeWidth = strokeWidth, + dividerHeight = dividerHeight, + dividerColorInt = dividerColorInt, + backgroundColorInt = backgroundColorInt, + top = space, + bottom = space, + start = space, + end = space + ) + topItemDecoration = SampleItemDecoration( + type = SampleItemDecoration.Type.TOP, + radius = radius, + strokeWidth = strokeWidth, + dividerHeight = dividerHeight, + dividerColorInt = dividerColorInt, + backgroundColorInt = backgroundColorInt, + top = space, + start = space, + end = space, + bottom = dividerHeight + ) + middleItemDecoration = SampleItemDecoration( + type = SampleItemDecoration.Type.MIDDLE, + radius = radius, + strokeWidth = strokeWidth, + dividerHeight = dividerHeight, + dividerColorInt = dividerColorInt, + backgroundColorInt = backgroundColorInt, + start = space, + end = space, + bottom = dividerHeight + ) + bottomItemDecoration = SampleItemDecoration( + type = SampleItemDecoration.Type.BOTTOM, + radius = radius, + strokeWidth = strokeWidth, + dividerHeight = dividerHeight, + dividerColorInt = dividerColorInt, + backgroundColorInt = backgroundColorInt, + bottom = space, + start = space, + end = space + ) + } + + //should be inside ViewModel/UIMapper + fun generateData(): List> { + val ids = (0..(Random.nextInt(10))).filterIndexed { index, _ -> + index == 0 || Random.nextBoolean() + } + val size = ids.size + return ids.mapIndexed { index, id -> + val name: String + val decoration: ItemDecoration> + when { + size == 1 -> { + name = "Single Cell id=$id" + decoration = singleItemDecoration + } + index == 0 -> { + name = "Top Cell id=$id" + decoration = topItemDecoration + } + index == size - 1 -> { + name = "Bottom Cell id=$id" + decoration = bottomItemDecoration + } + else -> { + name = "Middle Cell id=$id" + decoration = middleItemDecoration + } + } + DecorationsCell( + data = DecorationsUI( + id = id.toString(), + name = name + ), + decoration = decoration + ) + } + } + } +} \ No newline at end of file diff --git a/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/cell/DecorationsCell.kt b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/cell/DecorationsCell.kt new file mode 100644 index 0000000..94f491f --- /dev/null +++ b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/cell/DecorationsCell.kt @@ -0,0 +1,39 @@ +package com.originsdigital.compositeadapter.sample.decorations.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.decorations.R +import com.originsdigital.compositeadapter.sample.decorations.databinding.DecorationsCellBinding +import com.originsdigital.compositeadapter.sample.decorations.ui.entity.DecorationsUI + +data class DecorationsCell( + override val data: DecorationsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : Cell { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.decorations_cell + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return SampleViewHolder(DecorationsCellBinding.inflate(inflater, parent, false)) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as SampleViewHolder).binding.apply { + text.text = data.name + } + } + + private class SampleViewHolder( + val binding: DecorationsCellBinding + ) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/decorations/SampleItemDecoration.kt b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/decorations/SampleItemDecoration.kt new file mode 100644 index 0000000..804ce09 --- /dev/null +++ b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/decorations/SampleItemDecoration.kt @@ -0,0 +1,121 @@ +package com.originsdigital.compositeadapter.sample.decorations.ui.decorations + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.view.View +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.SpaceItemDecoration +import kotlin.math.roundToInt + +data class SampleItemDecoration( + private val type: Type, + private val radius: Float, + private val strokeWidth: Float, + private val dividerHeight: Int, + @ColorInt val dividerColorInt: Int, + @ColorInt val backgroundColorInt: Int, + override val top: Int = 0, + override val bottom: Int = 0, + override val start: Int = 0, + override val end: Int = 0 +) : SpaceItemDecoration>() { + + private val dividerPaint = Paint().apply { + color = dividerColorInt + } + private val backgroundPaint = Paint().apply { + style = Paint.Style.STROKE + isAntiAlias = true + strokeWidth = this@SampleItemDecoration.strokeWidth + color = backgroundColorInt + } + private val itemBounds = Rect() + + override fun onDraw( + canvas: Canvas, + view: View, + parent: RecyclerView, + state: RecyclerView.State, + item: Cell<*> + ) { + super.onDraw(canvas, view, parent, state, item) + parent.layoutManager?.getDecoratedBoundsWithMargins(view, itemBounds) + val translationY = view.translationY.roundToInt() + itemBounds.bottom = itemBounds.bottom + translationY + itemBounds.top = itemBounds.top + translationY + val drawBottomDivider: Boolean + val roundedTop: Boolean + val roundedBottom: Boolean + when (type) { + Type.SINGLE -> { + drawBottomDivider = false + roundedTop = true + roundedBottom = true + } + Type.TOP -> { + drawBottomDivider = true + roundedTop = true + roundedBottom = false + } + Type.MIDDLE -> { + drawBottomDivider = true + roundedTop = false + roundedBottom = false + } + Type.BOTTOM -> { + drawBottomDivider = false + roundedTop = false + roundedBottom = true + } + } + canvas.drawRoundedRect( + paint = backgroundPaint, + strokeWidth = strokeWidth / 2, + left = itemBounds.left.toFloat() + start, + top = itemBounds.top.toFloat() + top, + right = itemBounds.right.toFloat() - end, + bottom = itemBounds.bottom.toFloat() - bottom, + radius = radius, + withTop = roundedTop, + withBottom = roundedBottom + ) + if (drawBottomDivider) { + itemBounds.left = itemBounds.left + start + itemBounds.right = itemBounds.right - end + itemBounds.top = itemBounds.bottom - dividerHeight + canvas.drawRect(itemBounds, dividerPaint) + } + } + + private fun Canvas.drawRoundedRect( + paint: Paint, + strokeWidth: Float, + left: Float, + top: Float, + right: Float, + bottom: Float, + radius: Float, + withTop: Boolean, + withBottom: Boolean + ) { + save() + clipRect(left, top, right, bottom) + drawRoundRect( + left + strokeWidth, + if (withTop) top + strokeWidth else top - radius, + right - strokeWidth, + if (withBottom) bottom - strokeWidth else bottom + radius, + radius, + radius, + paint + ) + restore() + } + + enum class Type { + SINGLE, TOP, MIDDLE, BOTTOM + } +} \ No newline at end of file diff --git a/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/entity/DecorationsUI.kt b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/entity/DecorationsUI.kt new file mode 100644 index 0000000..e57970d --- /dev/null +++ b/samples/decorations/src/main/java/com/originsdigital/compositeadapter/sample/decorations/ui/entity/DecorationsUI.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.sample.decorations.ui.entity + +data class DecorationsUI( + val id: String, + val name: String +) \ No newline at end of file diff --git a/sample/src/main/res/layout/sample_viewbinding_message_list_item.xml b/samples/decorations/src/main/res/layout/decorations_cell.xml similarity index 100% rename from sample/src/main/res/layout/sample_viewbinding_message_list_item.xml rename to samples/decorations/src/main/res/layout/decorations_cell.xml diff --git a/samples/different-bindings/.gitignore b/samples/different-bindings/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/different-bindings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/different-bindings/build.gradle.kts b/samples/different-bindings/build.gradle.kts new file mode 100644 index 0000000..9f9020d --- /dev/null +++ b/samples/different-bindings/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) + kotlin(Config.Plugins.kapt) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + buildTypes { + getByName("debug") { + } + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + dataBinding = true + } +} + +dependencies { + implementation(Config.Deps.Kotlin.kotlinJdk8) + + implementation(Config.Deps.AndroidX.appcompat) + implementation(Config.Deps.AndroidX.swipeRefresh) + implementation(Config.Deps.AndroidX.recycler) + +// implementation(Config.Deps.CompositeAdapter.compositeAdapter) + implementation(project(Config.Deps.Libs.compositeAdapter)) +} \ No newline at end of file diff --git a/samples/different-bindings/proguard-rules.pro b/samples/different-bindings/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/samples/different-bindings/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/different-bindings/src/main/AndroidManifest.xml b/samples/different-bindings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5e2f3c2 --- /dev/null +++ b/samples/different-bindings/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/DifferentBindingsActivity.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/DifferentBindingsActivity.kt new file mode 100644 index 0000000..97105d5 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/DifferentBindingsActivity.kt @@ -0,0 +1,111 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding.DifferentBindingsDataBinding1MessageCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding.DifferentBindingsDataBinding2MessageCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.view.DifferentBindingsViewMessageCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.DifferentBindingsViewBinding1MessageCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.DifferentBindingsViewBinding2MessageCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +class DifferentBindingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(generateView()) + } + + private fun generateView(): View { + val compositeAdapter = CompositeAdapter().apply { + submitList(generateData()) + } + val swipeRefreshLayout = SwipeRefreshLayout(this).apply { + setOnRefreshListener { + compositeAdapter.submitList(generateData()) { + isRefreshing = false + } + } + } + val recyclerView = RecyclerView(this).apply { + adapter = compositeAdapter + layoutManager = LinearLayoutManager(context) + addItemDecoration(CompositeItemDecoration()) + } + swipeRefreshLayout.addView( + recyclerView, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + return swipeRefreshLayout + } + + //should be inside ViewModel + private fun generateData(): List> { + return DifferentBindingsUI.Type.values().map { type -> + when (type) { + DifferentBindingsUI.Type.VIEW -> { + listOf( + DifferentBindingsViewMessageCell( + data = DifferentBindingsUI( + name = "Cell with a programmatically generated View", + type = type + ) + ) + ) + } + DifferentBindingsUI.Type.VIEW_BINDING -> { + listOf( + DifferentBindingsViewBinding1MessageCell( + data = DifferentBindingsUI( + name = "Cell with default ViewHolder", + type = type + ) + ), + DifferentBindingsViewBinding2MessageCell( + data = DifferentBindingsUI( + name = "Cell with custom ViewHolder", + type = type + ) + ) + ) + } + DifferentBindingsUI.Type.DATA_BINDING -> { + listOf( + DifferentBindingsDataBinding1MessageCell( + data = DifferentBindingsUI( + name = "Cell with DataBindings inside xml", + type = type + ) + ), + DifferentBindingsDataBinding2MessageCell( + data = DifferentBindingsUI( + name = "Cell with DataBindings inside Cell", + type = type + ) + ) + ) + } + } + } + .flatten() + .shuffled() + } + + companion object { + fun getLaunchIntent(context: Context): Intent { + return Intent(context, DifferentBindingsActivity::class.java) + } + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseCell.kt new file mode 100644 index 0000000..eb03992 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseCell.kt @@ -0,0 +1,66 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell + +@Suppress("UNUSED_PARAMETER") +abstract class BaseCell : Cell { + + abstract fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): VIEW_HOLDER + + fun onBindViewHolder( + holder: VIEW_HOLDER, + position: Int, + payloads: List + ): Boolean = false + + abstract fun onBindViewHolder(holder: VIEW_HOLDER, position: Int) + + fun onViewAttachedToWindow(holder: VIEW_HOLDER) = Unit + fun onViewDetachedFromWindow(holder: VIEW_HOLDER) = Unit + + fun onViewRecycled(holder: VIEW_HOLDER) = Unit + + final override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return createViewHolder(inflater, parent, viewType) + } + + final override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: List + ): Boolean { + return onBindViewHolder(castHolder(holder), position, payloads) + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + onBindViewHolder(castHolder(holder), position) + } + + final override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + onViewAttachedToWindow(castHolder(holder)) + } + + final override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + onViewDetachedFromWindow(castHolder(holder)) + } + + final override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + onViewRecycled(castHolder(holder)) + } + + protected fun castHolder(holder: RecyclerView.ViewHolder): VIEW_HOLDER { + @Suppress("UNCHECKED_CAST") + return holder as VIEW_HOLDER + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseViewHolder.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseViewHolder.kt new file mode 100644 index 0000000..9a5694f --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/base/BaseViewHolder.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder(root: View) : RecyclerView.ViewHolder(root) \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding1MessageCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding1MessageCell.kt new file mode 100644 index 0000000..d247b0d --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding1MessageCell.kt @@ -0,0 +1,28 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding + +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.R +import com.originsdigital.compositeadapter.sample.differentbindings.databinding.DifferentBindingsDataBinding1CellBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding.base.DataBindingCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +// ViewBinding is better anyway +data class DifferentBindingsDataBinding1MessageCell( + override val data: DifferentBindingsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : DataBindingCell() { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.different_bindings_data_binding_1_cell + + // We do not need `onBindViewHolder` because everything is done inside the DataBindingCell +// override fun onBindViewHolder( +// holder: DataBindingViewHolder, +// position: Int +// ) { +// super.onBindViewHolder(holder, position) +// } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding2MessageCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding2MessageCell.kt new file mode 100644 index 0000000..dea41c9 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/DifferentBindingsDataBinding2MessageCell.kt @@ -0,0 +1,49 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.R +import com.originsdigital.compositeadapter.sample.differentbindings.databinding.DifferentBindingsDataBinding2CellBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseViewHolder +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +// ViewBinding is better anyway +data class DifferentBindingsDataBinding2MessageCell( + override val data: DifferentBindingsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : BaseCell() { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.different_bindings_data_binding_2_cell + + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CustomViewHolder { + return CustomViewHolder( + DifferentBindingsDataBinding2CellBinding.inflate( + inflater, + parent, + false + ) + ) + } + + override fun onBindViewHolder( + holder: CustomViewHolder, + position: Int + ) { + // But we can have a custom onBindViewHolder instead of custom BindingAdapter functions + holder.binding.text.text = data.name + } + + class CustomViewHolder( + val binding: DifferentBindingsDataBinding2CellBinding + ) : BaseViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingCell.kt new file mode 100644 index 0000000..1feb35e --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingCell.kt @@ -0,0 +1,27 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.ViewDataBinding +import com.originsdigital.compositeadapter.sample.differentbindings.BR +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseCell + +// ViewBinding is better anyway +abstract class DataBindingCell + : BaseCell>() { + + final override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): DataBindingViewHolder { + return DataBindingViewHolder.create(inflater, layoutId, parent) + } + + override fun onBindViewHolder(holder: DataBindingViewHolder, position: Int) { + (holder.binding).apply { + setVariable(BR.item, data) + executePendingBindings() + } + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingViewHolder.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingViewHolder.kt new file mode 100644 index 0000000..e8ebfbb --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/databinding/base/DataBindingViewHolder.kt @@ -0,0 +1,30 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.databinding.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseViewHolder + +// ViewBinding is better anyway +class DataBindingViewHolder( + val binding: DATA_BINDING +) : BaseViewHolder(binding.root) { + + companion object { + fun create( + inflater: LayoutInflater, + layoutResId: Int, + parent: ViewGroup + ): DataBindingViewHolder { + return DataBindingViewHolder( + DataBindingUtil.inflate( + inflater, + layoutResId, + parent, + false + ) as DATA_BINDING + ) + } + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/view/DifferentBindingsViewMessageCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/view/DifferentBindingsViewMessageCell.kt new file mode 100644 index 0000000..71707e1 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/view/DifferentBindingsViewMessageCell.kt @@ -0,0 +1,58 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.view + +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.widget.TextViewCompat +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.R +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseViewHolder +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +data class DifferentBindingsViewMessageCell( + override val data: DifferentBindingsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : BaseCell() { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.different_bindings_view_cell + + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CustomViewHolder { + val padding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 20f, + parent.resources.displayMetrics + ).toInt() + return CustomViewHolder( + AppCompatTextView(inflater.context).apply { + id = R.id.text + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) + setPadding(padding, padding, padding, padding) + val outValue = TypedValue() + context.theme.resolveAttribute(R.attr.selectableItemBackground, outValue, true) + setBackgroundResource(outValue.resourceId) + TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat_Medium) + } + ) + } + + override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { + holder.text.text = data.name + } + + class CustomViewHolder(val text: TextView) : BaseViewHolder(text) +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding1MessageCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding1MessageCell.kt new file mode 100644 index 0000000..418a885 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding1MessageCell.kt @@ -0,0 +1,37 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.R +import com.originsdigital.compositeadapter.sample.differentbindings.databinding.DifferentBindingsViewBinding1CellBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.base.ViewBindingCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.base.ViewBindingViewHolder +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +data class DifferentBindingsViewBinding1MessageCell( + override val data: DifferentBindingsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : ViewBindingCell() { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.different_bindings_view_binding_1_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): DifferentBindingsViewBinding1CellBinding { + return DifferentBindingsViewBinding1CellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.text.text = data.name + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding2MessageCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding2MessageCell.kt new file mode 100644 index 0000000..85a0ea2 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/DifferentBindingsViewBinding2MessageCell.kt @@ -0,0 +1,44 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.differentbindings.R +import com.originsdigital.compositeadapter.sample.differentbindings.databinding.DifferentBindingsViewBinding2CellBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseCell +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseViewHolder +import com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI + +data class DifferentBindingsViewBinding2MessageCell( + override val data: DifferentBindingsUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : BaseCell() { + + override val uniqueId: String = data.type.name + override val layoutId: Int = R.layout.different_bindings_view_binding_2_cell + + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CustomViewHolder { + return CustomViewHolder( + DifferentBindingsViewBinding2CellBinding.inflate( + inflater, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { + holder.binding.text.text = data.name + } + + class CustomViewHolder( + val binding: DifferentBindingsViewBinding2CellBinding + ) : BaseViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingCell.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingCell.kt new file mode 100644 index 0000000..f102e7d --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingCell.kt @@ -0,0 +1,24 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseCell + +abstract class ViewBindingCell + : BaseCell>() { + + abstract fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): VIEW_BINDING + + final override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): ViewBindingViewHolder { + return ViewBindingViewHolder(createViewBinding(inflater, parent, viewType)) + } +} \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingViewHolder.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingViewHolder.kt new file mode 100644 index 0000000..fc787d8 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/cell/viewbinding/base/ViewBindingViewHolder.kt @@ -0,0 +1,8 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.viewbinding.base + +import androidx.viewbinding.ViewBinding +import com.originsdigital.compositeadapter.sample.differentbindings.ui.cell.base.BaseViewHolder + +class ViewBindingViewHolder( + val binding: VIEW_BINDING +) : BaseViewHolder(binding.root) \ No newline at end of file diff --git a/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/entity/DifferentBindingsUI.kt b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/entity/DifferentBindingsUI.kt new file mode 100644 index 0000000..f75c782 --- /dev/null +++ b/samples/different-bindings/src/main/java/com/originsdigital/compositeadapter/sample/differentbindings/ui/entity/DifferentBindingsUI.kt @@ -0,0 +1,10 @@ +package com.originsdigital.compositeadapter.sample.differentbindings.ui.entity + +data class DifferentBindingsUI( + val name: String, + val type: Type +) { + enum class Type { + VIEW, VIEW_BINDING, DATA_BINDING + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/sample_databinding_message_list_item.xml b/samples/different-bindings/src/main/res/layout/different_bindings_data_binding_1_cell.xml similarity index 78% rename from sample/src/main/res/layout/sample_databinding_message_list_item.xml rename to samples/different-bindings/src/main/res/layout/different_bindings_data_binding_1_cell.xml index 187faff..026c4dd 100644 --- a/sample/src/main/res/layout/sample_databinding_message_list_item.xml +++ b/samples/different-bindings/src/main/res/layout/different_bindings_data_binding_1_cell.xml @@ -6,7 +6,7 @@ + type="com.originsdigital.compositeadapter.sample.differentbindings.ui.entity.DifferentBindingsUI" /> \ No newline at end of file diff --git a/samples/different-bindings/src/main/res/layout/different_bindings_data_binding_2_cell.xml b/samples/different-bindings/src/main/res/layout/different_bindings_data_binding_2_cell.xml new file mode 100644 index 0000000..f5fddcc --- /dev/null +++ b/samples/different-bindings/src/main/res/layout/different_bindings_data_binding_2_cell.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_1_cell.xml b/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_1_cell.xml new file mode 100644 index 0000000..a383a7c --- /dev/null +++ b/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_1_cell.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_2_cell.xml b/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_2_cell.xml new file mode 100644 index 0000000..a383a7c --- /dev/null +++ b/samples/different-bindings/src/main/res/layout/different_bindings_view_binding_2_cell.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/samples/different-bindings/src/main/res/values/ids.xml b/samples/different-bindings/src/main/res/values/ids.xml new file mode 100644 index 0000000..a3ad6fc --- /dev/null +++ b/samples/different-bindings/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/inner-recyclerview/.gitignore b/samples/inner-recyclerview/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/inner-recyclerview/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/inner-recyclerview/build.gradle.kts b/samples/inner-recyclerview/build.gradle.kts new file mode 100644 index 0000000..ee95cc0 --- /dev/null +++ b/samples/inner-recyclerview/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + buildTypes { + getByName("debug") { + } + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(Config.Deps.Kotlin.kotlinJdk8) + + implementation(Config.Deps.AndroidX.appcompat) + implementation(Config.Deps.AndroidX.swipeRefresh) + implementation(Config.Deps.AndroidX.recycler) + +// implementation(Config.Deps.CompositeAdapter.compositeAdapter) + implementation(project(Config.Deps.Libs.compositeAdapter)) +} \ No newline at end of file diff --git a/samples/inner-recyclerview/proguard-rules.pro b/samples/inner-recyclerview/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/samples/inner-recyclerview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/AndroidManifest.xml b/samples/inner-recyclerview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1612cac --- /dev/null +++ b/samples/inner-recyclerview/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/InnerRecyclerActivity.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/InnerRecyclerActivity.kt new file mode 100644 index 0000000..572a37e --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/InnerRecyclerActivity.kt @@ -0,0 +1,147 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.decoration.SpaceItemDecoration +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell.InnerRecycler1Cell +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell.InnerRecycler2Cell +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell.InnerRecyclerItemCell +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity.InnerRecyclerItemUI +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity.InnerRecyclerUI +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.stateholder.ScrollStatesHolder +import kotlin.random.Random + +class InnerRecyclerActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val dataHolder = DataHolder(dpToPx(8f).toInt()) + setContentView(generateView(dataHolder)) + } + + private fun Context.dpToPx(dp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) + } + + private fun generateView(dataHolder: DataHolder): View { + val compositeAdapter = CompositeAdapter().apply { + submitList(dataHolder.generateData()) + } + val swipeRefreshLayout = SwipeRefreshLayout(this).apply { + setOnRefreshListener { + compositeAdapter.submitList(dataHolder.generateData()) { + isRefreshing = false + } + } + } + val recyclerView = RecyclerView(this).apply { + adapter = compositeAdapter + layoutManager = LinearLayoutManager(context) + addItemDecoration(CompositeItemDecoration()) + } + swipeRefreshLayout.addView( + recyclerView, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + return swipeRefreshLayout + } + + + companion object { + fun getLaunchIntent(context: Context): Intent { + return Intent(context, InnerRecyclerActivity::class.java) + } + } + + //should be inside ViewModel/UIMapper + private class DataHolder(space: Int) { + + private val scrollStatesHolder = ScrollStatesHolder() + + private val singleItemDecoration: ItemDecoration> + private val firstItemDecoration: ItemDecoration> + private val middleItemDecoration: ItemDecoration> + private val lastItemDecoration: ItemDecoration> + + init { + singleItemDecoration = SpaceItemDecoration( + top = space, + bottom = space, + start = space, + end = space + ) + firstItemDecoration = SpaceItemDecoration( + top = space, + bottom = space, + start = space, + end = space / 2 + ) + middleItemDecoration = SpaceItemDecoration( + top = space, + bottom = space, + start = space / 2, + end = space / 2 + ) + lastItemDecoration = SpaceItemDecoration( + top = space, + bottom = space, + start = space / 2, + end = space + ) + } + + //should be inside ViewModel/UIMapper + fun generateData(): List> { + // Cells do the same, but + // InnerRecycler1Cell - more code but more clearer and `correct` + // InnerRecycler2Cell - less code (can be less with `ViewBindingCell`) + val useClearerVersion = Random.nextBoolean() + return (0..20).map { recyclerId -> + val size = 10 + val cells = (0..size).mapIndexed { index, itemId -> + val decoration = when { + size == 0 -> singleItemDecoration + index == 0 -> firstItemDecoration + index == size -> lastItemDecoration + else -> middleItemDecoration + } + InnerRecyclerItemCell( + data = InnerRecyclerItemUI( + id = itemId.toString(), + name = "Value $itemId is ${Random.nextBoolean()}" + ), + decoration = decoration + ) + } + val recyclerUI = InnerRecyclerUI( + id = recyclerId.toString(), + cells = cells, + scrollStatesHolder = scrollStatesHolder + ) + if (useClearerVersion) { + InnerRecycler1Cell( + data = recyclerUI + ) + } else { + InnerRecycler2Cell( + data = recyclerUI + ) + } + } + } + } +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler1Cell.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler1Cell.kt new file mode 100644 index 0000000..6fb47e0 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler1Cell.kt @@ -0,0 +1,86 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.innerrecyclerview.R +import com.originsdigital.compositeadapter.sample.innerrecyclerview.databinding.InnerRecycler1CellBinding +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity.InnerRecyclerUI +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.layoutmanager.PercentWidthLinearLayoutManager + +// More code but more clearer and `correct` than `InnerRecycler2Cell` +data class InnerRecycler1Cell( + override val data: InnerRecyclerUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : Cell { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.inner_recycler_1_cell + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return SampleViewHolder(InnerRecycler1CellBinding.inflate(inflater, parent, false)) + } + + // This is the correct way to optimize bindings with payloads + // But you can skip this overload, so `onBindViewHolder` with `payload` will call + // `onBindViewHolder` without `payload`, and result will be the same in this case + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: List + ): Boolean { + return if (payloads.isNotEmpty() && payloads.all { payload -> RECYCLER_VIEW_PAYLOAD == payload }) { + submitData(holder) + true + } else { + super.onBindViewHolder(holder, position, payloads) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + submitData(holder) + } + + private fun submitData(holder: RecyclerView.ViewHolder) { + (holder as SampleViewHolder).setData(data) + } + + // We do not need to animate the InnerRecyclerCell, because it has its own CompositeAdapter, + // which will calculate the diffs of his cells and animate these changes within itself. + // The same for the ViewPager1/ViewPager2/Webview/VideoPlayer/other complex view. + override fun getChangePayload(newItem: Cell<*>): Any = RECYCLER_VIEW_PAYLOAD + + companion object { + private const val RECYCLER_VIEW_PAYLOAD = "RECYCLER_VIEW" + } + + private class SampleViewHolder( + private val binding: InnerRecycler1CellBinding + ) : RecyclerView.ViewHolder(binding.root) { + + private val adapter = CompositeAdapter() + + init { + binding.apply { + recyclerView.adapter = adapter + recyclerView.layoutManager = PercentWidthLinearLayoutManager(binding.root.context) + recyclerView.addItemDecoration(CompositeItemDecoration()) + } + } + + fun setData(data: InnerRecyclerUI) { + adapter.submitList(data.cells) + data.scrollStatesHolder.setupRecyclerView(data.id, binding.recyclerView) + } + } +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler2Cell.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler2Cell.kt new file mode 100644 index 0000000..297401f --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecycler2Cell.kt @@ -0,0 +1,66 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.innerrecyclerview.R +import com.originsdigital.compositeadapter.sample.innerrecyclerview.databinding.InnerRecycler2CellBinding +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity.InnerRecyclerUI +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.layoutmanager.PercentWidthLinearLayoutManager + +// less code (can be less with `ViewBindingCell`) than `InnerRecycler1Cell` +data class InnerRecycler2Cell( + override val data: InnerRecyclerUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : Cell { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.inner_recycler_2_cell + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return SampleViewHolder( + InnerRecycler2CellBinding.inflate(inflater, parent, false).also { binding -> + // Don't forget that Cell can survive the configuration changes inside the ViewModel + // or in some other way. So you MUST NOT store any link to + // Adapter/View/ViewHolder/Fragment/Context/etc in the Cell + // otherwise it will be leaked. + binding.recyclerView.adapter = CompositeAdapter() + binding.recyclerView.layoutManager = + PercentWidthLinearLayoutManager(binding.root.context) + binding.recyclerView.addItemDecoration(CompositeItemDecoration()) + } + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as SampleViewHolder).binding.apply { + (recyclerView.adapter as CompositeAdapter).submitList(data.cells) + data.scrollStatesHolder.setupRecyclerView(uniqueId, holder.binding.recyclerView) + } + } + + // We do not need to animate the InnerRecyclerCell, because it has its own CompositeAdapter, + // which will calculate the diffs of his cells and animate these changes within itself. + // The same for the ViewPager1/ViewPager2/Webview/VideoPlayer/other complex view. + // `onBindViewHolder` with `payload` will be called. But its empty so it will call + // `onBindViewHolder` without `payload` where submitList is called + override fun getChangePayload(newItem: Cell<*>): Any = RECYCLER_VIEW_PAYLOAD + + companion object { + private const val RECYCLER_VIEW_PAYLOAD = "RECYCLER_VIEW" + } + + private class SampleViewHolder( + val binding: InnerRecycler2CellBinding + ) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecyclerItemCell.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecyclerItemCell.kt new file mode 100644 index 0000000..e3ea1f3 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/cell/InnerRecyclerItemCell.kt @@ -0,0 +1,67 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.cell + +import android.content.Context +import android.graphics.Outline +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.sample.innerrecyclerview.R +import com.originsdigital.compositeadapter.sample.innerrecyclerview.databinding.InnerRecyclerItemCellBinding +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity.InnerRecyclerItemUI + +data class InnerRecyclerItemCell( + override val data: InnerRecyclerItemUI, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : Cell { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.inner_recycler_item_cell + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return SampleViewHolder( + InnerRecyclerItemCellBinding.inflate(inflater, parent, false).also { holder -> + holder.root.applyRoundCorners(holder.root.context.dpToPx(6f)) + } + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as SampleViewHolder).binding.apply { + text.text = data.name + } + } + + private fun Context.dpToPx(dp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) + } + + private fun View.applyRoundCorners(radius: Float) { + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height, + radius + ) + } + } + clipToOutline = true + } + + private class SampleViewHolder( + val binding: InnerRecyclerItemCellBinding + ) : RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerItemUI.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerItemUI.kt new file mode 100644 index 0000000..7474955 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerItemUI.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity + +data class InnerRecyclerItemUI( + val id: String, + val name: String +) \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerUI.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerUI.kt new file mode 100644 index 0000000..9e50ab3 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/entity/InnerRecyclerUI.kt @@ -0,0 +1,10 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.entity + +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.stateholder.ScrollStatesHolder + +data class InnerRecyclerUI( + val id: String, + val cells: List>, + val scrollStatesHolder: ScrollStatesHolder +) \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/layoutmanager/PercentWidthLinearLayoutManager.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/layoutmanager/PercentWidthLinearLayoutManager.kt new file mode 100644 index 0000000..692a53c --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/layoutmanager/PercentWidthLinearLayoutManager.kt @@ -0,0 +1,37 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.layoutmanager + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class PercentWidthLinearLayoutManager( + context: Context +) : LinearLayoutManager(context, HORIZONTAL, false) { + + override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { + return super.generateDefaultLayoutParams().scale() + } + + override fun generateLayoutParams(lp: ViewGroup.LayoutParams?): RecyclerView.LayoutParams { + return super.generateLayoutParams(lp).scale() + } + + override fun generateLayoutParams( + c: Context?, + attrs: AttributeSet? + ): RecyclerView.LayoutParams { + return super.generateLayoutParams(c, attrs).scale() + } + + private fun RecyclerView.LayoutParams.scale(): RecyclerView.LayoutParams { + return this.apply { + width = calculateChildWidth(getWidth() - paddingStart - paddingEnd) + } + } + + companion object { + fun calculateChildWidth(parentWidth: Int): Int = parentWidth / 3 + } +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStateHolder.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStateHolder.kt new file mode 100644 index 0000000..e124730 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStateHolder.kt @@ -0,0 +1,49 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.stateholder + +import android.os.Parcelable +import androidx.recyclerview.widget.RecyclerView + +class ScrollStateHolder { + + private var pendingScroll: Boolean = false + private var currentState: Parcelable? = null + + private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + saveScrollState(recyclerView) + } + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (dx != 0) { + pendingScroll = true + } + } + } + + fun setupRecyclerView(recyclerView: RecyclerView) { + recyclerView.removeOnScrollListener(scrollListener) + restoreScrollState(recyclerView) + recyclerView.addOnScrollListener(scrollListener) + } + + private fun saveScrollState(recyclerView: RecyclerView) { + if (pendingScroll) { + pendingScroll = false + currentState = recyclerView.layoutManager!!.onSaveInstanceState() + } + } + + private fun restoreScrollState(recyclerView: RecyclerView) { + val layoutManager = recyclerView.layoutManager!! + val currentState = currentState + if (currentState == null) { + layoutManager.scrollToPosition(0) + } else { + layoutManager.onRestoreInstanceState(currentState) + } + } +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStatesHolder.kt b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStatesHolder.kt new file mode 100644 index 0000000..7688d89 --- /dev/null +++ b/samples/inner-recyclerview/src/main/java/com/originsdigital/compositeadapter/sample/innerrecyclerview/ui/stateholder/ScrollStatesHolder.kt @@ -0,0 +1,18 @@ +package com.originsdigital.compositeadapter.sample.innerrecyclerview.ui.stateholder + +import androidx.recyclerview.widget.RecyclerView + +class ScrollStatesHolder { + + private val stateHolders = hashMapOf() + + fun setupRecyclerView(uniqueId: String, recyclerView: RecyclerView) { + getStateHolder(uniqueId).setupRecyclerView(recyclerView) + } + + private fun getStateHolder(uniqueId: String): ScrollStateHolder { + return stateHolders[uniqueId] ?: ScrollStateHolder().also { stateHolder -> + stateHolders[uniqueId] = stateHolder + } + } +} \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/res/layout/inner_recycler_1_cell.xml b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_1_cell.xml new file mode 100644 index 0000000..a6be088 --- /dev/null +++ b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_1_cell.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/res/layout/inner_recycler_2_cell.xml b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_2_cell.xml new file mode 100644 index 0000000..a6be088 --- /dev/null +++ b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_2_cell.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/samples/inner-recyclerview/src/main/res/layout/inner_recycler_item_cell.xml b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_item_cell.xml new file mode 100644 index 0000000..fd6eead --- /dev/null +++ b/samples/inner-recyclerview/src/main/res/layout/inner_recycler_item_cell.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/app/.gitignore b/samples/state-as-cells/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/app/build.gradle.kts b/samples/state-as-cells/app/build.gradle.kts new file mode 100644 index 0000000..3262a93 --- /dev/null +++ b/samples/state-as-cells/app/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseUI)) + implementation(project(Config.Deps.Libs.homeData)) + implementation(project(Config.Deps.Libs.homeCore)) + api(project(Config.Deps.Libs.homeUI)) + implementation(project(Config.Deps.Libs.storiesCore)) + implementation(project(Config.Deps.Libs.storiesUI)) + implementation(project(Config.Deps.Libs.newsCore)) + implementation(project(Config.Deps.Libs.newsUI)) +} \ No newline at end of file diff --git a/samples/state-as-cells/app/consumer-rules.pro b/samples/state-as-cells/app/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/app/proguard-rules.pro b/samples/state-as-cells/app/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/app/src/main/AndroidManifest.xml b/samples/state-as-cells/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cf0daec --- /dev/null +++ b/samples/state-as-cells/app/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/AppLoggerImpl.kt b/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/AppLoggerImpl.kt new file mode 100644 index 0000000..60d7701 --- /dev/null +++ b/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/AppLoggerImpl.kt @@ -0,0 +1,31 @@ +package com.originsdigital.compositeadapter.stateascells.app + +import android.util.Log +import com.originsdigital.compositeadapter.core.AppLogger + +class AppLoggerImpl(private val isEnabled: Boolean) : AppLogger { + + override fun logD(tag: String, message: String) { + if (isEnabled) { + Log.d(tag, message) + } + } + + override fun logD(tag: String, message: String, error: Throwable) { + if (isEnabled) { + Log.d(tag, "$message $error") + error.printStackTrace() + } + } + + override fun trackNonFatal(tag: String, message: String) { + try { + throw IllegalStateException("$tag $message") + } catch (stackTrace: IllegalStateException) { +// FirebaseCrashlytics.getInstance().recordException(stackTrace) + if (isEnabled) { + throw stackTrace + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/StateAsCellsDI.kt b/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/StateAsCellsDI.kt new file mode 100644 index 0000000..64a86b8 --- /dev/null +++ b/samples/state-as-cells/app/src/main/java/com/originsdigital/compositeadapter/stateascells/app/StateAsCellsDI.kt @@ -0,0 +1,63 @@ +package com.originsdigital.compositeadapter.stateascells.app + +import android.app.Application +import com.originsdigital.compositeadapter.BuildConfig +import com.originsdigital.compositeadapter.core.AppLogger +import com.originsdigital.compositeadapter.core.di.IS_DEBUG_QUALIFIER +import com.originsdigital.compositeadapter.core.di.IS_INTEGRATION_QUALIFIER +import com.originsdigital.compositeadapter.core.di.IS_NOT_PRODUCTION_RELEASE +import com.originsdigital.compositeadapter.core.di.IS_NOT_PRODUCTION_RELEASE_QUALIFIER +import com.originsdigital.compositeadapter.core.di.IS_PRODUCTION_BETA_QUALIFIER +import com.originsdigital.compositeadapter.core.di.coreModule +import com.originsdigital.compositeadapter.stateascells.home.ui.di.homeUIModule +import com.originsdigital.compositeadapter.stories.data.di.homeDataModule +import com.originsdigital.compositeadapter.ui.di.uiModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.dsl.module + + +class StateAsCellsDI { + + companion object { + fun init(application: Application) { + startKoin { + androidContext(application) + modules(getModules()) + } + } + + private fun getModules(): List { + return listOf( + coreModule, + uiModule, + homeDataModule(), + homeUIModule(), + appModule() + ) + } + + private fun appModule(): Module { + return module { + single(IS_DEBUG_QUALIFIER) { + "debug".equals(BuildConfig.BUILD_TYPE, true) + } + single(IS_INTEGRATION_QUALIFIER) { +// "integration".equals(BuildConfig.FLAVOR, true) + true + } + single(IS_PRODUCTION_BETA_QUALIFIER) { + /*"production".equals(BuildConfig.FLAVOR, true) + && */"beta".equals(BuildConfig.BUILD_TYPE, true) + } + single(IS_NOT_PRODUCTION_RELEASE_QUALIFIER) { + BuildConfig.DEBUG +// || !"production".equals(BuildConfig.FLAVOR, true) + || !"release".equals(BuildConfig.BUILD_TYPE, true) + } + single { AppLoggerImpl(IS_NOT_PRODUCTION_RELEASE) } + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/.gitignore b/samples/state-as-cells/features/base/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/base/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/build.gradle.kts b/samples/state-as-cells/features/base/core/build.gradle.kts new file mode 100644 index 0000000..ed15a46 --- /dev/null +++ b/samples/state-as-cells/features/base/core/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") + `java-library` +} + +java.sourceCompatibility = Config.Build.javaVersion +java.targetCompatibility = Config.Build.javaVersion + +project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } +} + +dependencies { + api(Config.Deps.Kotlin.kotlinJdk8) + api(Config.Deps.Coroutines.core) + api(Config.Deps.Koin.core) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/AppLogger.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/AppLogger.kt new file mode 100644 index 0000000..29f8525 --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/AppLogger.kt @@ -0,0 +1,40 @@ +package com.originsdigital.compositeadapter.core + +import org.koin.core.context.GlobalContext + +interface AppLogger { + + fun logD(tag: String, message: String) + + fun logD(tag: String, message: String, error: Throwable) + + fun trackNonFatal(tag: String, message: String) +} + +fun log(tag: Any, message: String) { + log(getTag(tag), message) +} + +fun log(tag: String, message: String) { + GlobalContext.get().get().logD(tag, message) +} + +fun log(tag: Any, message: String, error: Throwable) { + log(getTag(tag), message) +} + +fun log(tag: String, message: String, error: Throwable) { + GlobalContext.get().get().logD(tag, message) +} + +fun trackNonFatal(tag: Any, message: String) { + trackNonFatal(getTag(tag), message) +} + +fun trackNonFatal(tag: String, message: String) { + GlobalContext.get().get().trackNonFatal(tag, message) +} + +private fun getTag(tag: Any): String { + return "${tag::class.java.simpleName}@${tag.hashCode()}" +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/di/CoreModule.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/di/CoreModule.kt new file mode 100644 index 0000000..c77c815 --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/di/CoreModule.kt @@ -0,0 +1,28 @@ +package com.originsdigital.compositeadapter.core.di + +import kotlinx.coroutines.Dispatchers +import org.koin.core.context.GlobalContext +import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val coreModule + get() = module { + factory(BG_DISPATCHER_QUALIFIER) { Dispatchers.IO } + factory(UI_DISPATCHER_QUALIFIER) { Dispatchers.Main } + factory(DEFAULT_DISPATCHER_QUALIFIER) { Dispatchers.Default } + } + +val UI_DISPATCHER_QUALIFIER: StringQualifier get() = named("UI_DISPATCHER") +val BG_DISPATCHER_QUALIFIER: StringQualifier get() = named("BG_DISPATCHER") +val DEFAULT_DISPATCHER_QUALIFIER: StringQualifier get() = named("DEFAULT_DISPATCHER") + +val IS_DEBUG_QUALIFIER: StringQualifier get() = named("IS_DEBUG") +val IS_INTEGRATION_QUALIFIER: StringQualifier get() = named("IS_INTEGRATION") +val IS_PRODUCTION_BETA_QUALIFIER: StringQualifier get() = named("IS_PRODUCTION_BETA") +val IS_NOT_PRODUCTION_RELEASE_QUALIFIER: StringQualifier get() = named("IS_NOT_PRODUCTION_RELEASE") + +val IS_DEBUG: Boolean get() = GlobalContext.get().get(IS_DEBUG_QUALIFIER) +val IS_INTEGRATION: Boolean get() = GlobalContext.get().get(IS_INTEGRATION_QUALIFIER) +val IS_PRODUCTION_BETA: Boolean get() = GlobalContext.get().get(IS_PRODUCTION_BETA_QUALIFIER) +val IS_NOT_PRODUCTION_RELEASE: Boolean get() = GlobalContext.get().get(IS_NOT_PRODUCTION_RELEASE_QUALIFIER) \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/entity/Scene.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/entity/Scene.kt new file mode 100644 index 0000000..9ba1b65 --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/entity/Scene.kt @@ -0,0 +1,20 @@ +package com.originsdigital.compositeadapter.core.entity + +sealed class Scene { + + abstract val isRefreshing: Boolean + + data class Loading( + override val isRefreshing: Boolean = false + ) : Scene() + + data class Error( + val throwable: Throwable, + override val isRefreshing: Boolean + ) : Scene() + + data class Data( + val data: DATA, + override val isRefreshing: Boolean + ) : Scene() +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/ExceptionsExtensions.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/ExceptionsExtensions.kt new file mode 100644 index 0000000..ffc97d1 --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/ExceptionsExtensions.kt @@ -0,0 +1,24 @@ +package com.originsdigital.compositeadapter.core.extensions + +import kotlinx.coroutines.TimeoutCancellationException +import java.io.InterruptedIOException +import java.net.SocketException +import java.net.UnknownHostException +import java.net.UnknownServiceException +import java.nio.channels.ClosedChannelException +import kotlin.coroutines.cancellation.CancellationException + +fun Throwable.rethrowIfNeeded() { + if (this !is TimeoutCancellationException && this is CancellationException) { + throw this + } +} + +val Throwable.isInternetConnectionException: Boolean + get() { + return this is UnknownHostException + || this is UnknownServiceException + || this is InterruptedIOException + || this is SocketException + || this is ClosedChannelException + } \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/SceneExtensions.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/SceneExtensions.kt new file mode 100644 index 0000000..5116e66 --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/extensions/SceneExtensions.kt @@ -0,0 +1,97 @@ +package com.originsdigital.compositeadapter.core.extensions + +import com.originsdigital.compositeadapter.core.entity.Scene + +val Scene.dataOrNull: DATA? + get() { + return when (this) { + is Scene.Loading, is Scene.Error -> null + is Scene.Data -> this.data + } + } + +val Scene.isEmpty: Boolean get() = dataOrNull.isEmpty + +val Any?.isEmpty: Boolean + get() { + return when (this) { + null -> true + is Collection<*> -> isEmpty() + else -> false + } + } + +fun Scene.toLoading(force: Boolean): Scene { + return when (this) { + is Scene.Loading -> this + is Scene.Error -> { + if (force) { + Scene.Loading() + } else { + this.copy(isRefreshing = true) + } + } + is Scene.Data -> { + if (force) { + Scene.Loading() + } else { + this.copy(isRefreshing = true) + } + } + } +} + +fun Scene.toError( + error: Throwable, + force: Boolean +): Scene { + return when (this) { + is Scene.Loading, is Scene.Error -> Scene.Error(throwable = error, isRefreshing = false) + is Scene.Data -> if (force) { + Scene.Error(throwable = error, isRefreshing = false) + } else { + this.copy(isRefreshing = false) + } + } +} + +fun Scene.toData(data: DATA): Scene { + return when (this) { + is Scene.Loading, is Scene.Error -> Scene.Data(data = data, isRefreshing = false) + is Scene.Data -> this.copy(data = data, isRefreshing = false) + } +} + +fun Scene.Loading.mapData(): Scene { + return Scene.Loading() +} + +fun Scene.Error.mapData(): Scene { + return Scene.Error(throwable = throwable, isRefreshing = isRefreshing) +} + +fun Scene.Data.mapData( + mapper: (DATA) -> NEW_DATA +): Scene { + return Scene.Data(data = mapper(data), isRefreshing = isRefreshing) +} + +fun Scene.mapData( + mapper: (DATA) -> NEW_DATA +): Scene { + return when (this) { + is Scene.Loading -> this.mapData() + is Scene.Error -> this.mapData() + is Scene.Data -> this.mapData(mapper = mapper) + } +} + +fun Scene.mapDataScene( + mapper: (Scene.Data) -> Scene +): Scene { + return when (this) { + is Scene.Loading -> this.mapData() + is Scene.Error -> this.mapData() + is Scene.Data -> mapper(this) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/utils/Loading.kt b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/utils/Loading.kt new file mode 100644 index 0000000..8cc2e0f --- /dev/null +++ b/samples/state-as-cells/features/base/core/src/main/java/com/originsdigital/compositeadapter/core/utils/Loading.kt @@ -0,0 +1,22 @@ +package com.originsdigital.compositeadapter.core.utils + +import com.originsdigital.compositeadapter.core.extensions.rethrowIfNeeded +import com.originsdigital.compositeadapter.core.log + +suspend inline fun safeLoad( + tag: Any, + text: String, + crossinline block: (suspend () -> T), + crossinline error: (suspend (Exception) -> T) +): T { + return try { + log(tag, "$text started") + block().also { + log(tag, "$text result = $it") + } + } catch (e: Exception) { + e.rethrowIfNeeded() + log(tag, "$text error =", e) + error(e) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/.gitignore b/samples/state-as-cells/features/base/ui/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/base/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/build.gradle.kts b/samples/state-as-cells/features/base/ui/build.gradle.kts new file mode 100644 index 0000000..cf3b4ad --- /dev/null +++ b/samples/state-as-cells/features/base/ui/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + api(project(Config.Deps.Libs.baseCore)) + api(Config.Deps.Coroutines.android) + + api(Config.Deps.AndroidX.appcompat) + api(Config.Deps.AndroidX.recycler) + api(Config.Deps.AndroidX.core) + api(Config.Deps.AndroidX.swipeRefresh) + api(Config.Deps.AndroidX.lifecycle) + api(Config.Deps.AndroidX.lifecycleViewModelKtx) + api(Config.Deps.AndroidX.lifecycleLiveDataKtx) + api(Config.Deps.Material.material) + + api(Config.Deps.Koin.android) + +// api(Config.Deps.CompositeAdapter.compositeAdapter) + api(project(Config.Deps.Libs.compositeAdapter)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/consumer-rules.pro b/samples/state-as-cells/features/base/ui/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/base/ui/proguard-rules.pro b/samples/state-as-cells/features/base/ui/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/AndroidManifest.xml b/samples/state-as-cells/features/base/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..37efe39 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonErrorCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonErrorCell.kt new file mode 100644 index 0000000..f934e2e --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonErrorCell.kt @@ -0,0 +1,42 @@ +package com.originsdigital.compositeadapter.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder +import com.originsdigital.compositeadapter.ui.databinding.CommonErrorCellBinding +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI + +data class CommonErrorCell( + override val data: CommonErrorUI, + override val decoration: ItemDecoration>?, + override val onClickListener: ((ClickItem) -> Unit)? +) : ViewBindingCell() { + + override val uniqueId = data.id + override val layoutId = R.layout.common_error_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CommonErrorCellBinding { + return CommonErrorCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.apply { + text.text = when (data.message) { + is CommonErrorUI.Message.Text -> data.message.text + is CommonErrorUI.Message.Resource -> text.context.getString(data.message.textId) + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullEmptyCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullEmptyCell.kt new file mode 100644 index 0000000..5c2adae --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullEmptyCell.kt @@ -0,0 +1,42 @@ +package com.originsdigital.compositeadapter.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder +import com.originsdigital.compositeadapter.ui.databinding.CommonFullEmptyCellBinding +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI + +data class CommonFullEmptyCell( + override val data: CommonErrorUI, + override val decoration: ItemDecoration>?, + override val onClickListener: ((ClickItem) -> Unit)? +) : ViewBindingCell() { + + override val uniqueId = data.id + override val layoutId = R.layout.common_full_empty_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CommonFullEmptyCellBinding { + return CommonFullEmptyCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.apply { + text.text = when (data.message) { + is CommonErrorUI.Message.Text -> data.message.text + is CommonErrorUI.Message.Resource -> text.context.getString(data.message.textId) + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullErrorCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullErrorCell.kt new file mode 100644 index 0000000..6b5848b --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullErrorCell.kt @@ -0,0 +1,42 @@ +package com.originsdigital.compositeadapter.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder +import com.originsdigital.compositeadapter.ui.databinding.CommonFullErrorCellBinding +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI + +data class CommonFullErrorCell( + override val data: CommonErrorUI, + override val decoration: ItemDecoration>?, + override val onClickListener: ((ClickItem) -> Unit)? +) : ViewBindingCell() { + + override val uniqueId = data.id + override val layoutId = R.layout.common_full_error_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CommonFullErrorCellBinding { + return CommonFullErrorCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.apply { + text.text = when (data.message) { + is CommonErrorUI.Message.Text -> data.message.text + is CommonErrorUI.Message.Resource -> text.context.getString(data.message.textId) + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullLoaderCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullLoaderCell.kt new file mode 100644 index 0000000..9d5f420 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonFullLoaderCell.kt @@ -0,0 +1,29 @@ +package com.originsdigital.compositeadapter.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder +import com.originsdigital.compositeadapter.ui.databinding.CommonFullLoaderCellBinding + +data class CommonFullLoaderCell( + override val data: Any = Unit +) : ViewBindingCell() { + + override val uniqueId = "CommonFullLoaderCell" + override val layoutId = R.layout.common_full_loader_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CommonFullLoaderCellBinding { + return CommonFullLoaderCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) = Unit +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonLoaderCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonLoaderCell.kt new file mode 100644 index 0000000..6b29be5 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/CommonLoaderCell.kt @@ -0,0 +1,46 @@ +package com.originsdigital.compositeadapter.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder +import com.originsdigital.compositeadapter.ui.databinding.CommonLoaderCellBinding +import com.originsdigital.compositeadapter.ui.entity.CommonLoaderUI +import com.originsdigital.compositeadapter.utils.context + +data class CommonLoaderCell( + override val data: CommonLoaderUI, + override val decoration: ItemDecoration>? +) : ViewBindingCell() { + + override val uniqueId = data.id + override val layoutId = R.layout.common_loader_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CommonLoaderCellBinding { + return CommonLoaderCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.apply { + val newHeight = if (data.heightId == null) { + ViewGroup.LayoutParams.WRAP_CONTENT + } else { + holder.context.resources.getDimension(data.heightId).toInt() + } + if (root.layoutParams.height != newHeight) { + root.updateLayoutParams { height = newHeight } + } + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseCell.kt new file mode 100644 index 0000000..943fae1 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseCell.kt @@ -0,0 +1,66 @@ +package com.originsdigital.compositeadapter.ui.cell.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.cell.Cell + +@Suppress("UNUSED_PARAMETER") +abstract class BaseCell : Cell { + + abstract fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): VIEW_HOLDER + + fun onBindViewHolder( + holder: VIEW_HOLDER, + position: Int, + payloads: List + ): Boolean = false + + abstract fun onBindViewHolder(holder: VIEW_HOLDER, position: Int) + + fun onViewAttachedToWindow(holder: VIEW_HOLDER) = Unit + fun onViewDetachedFromWindow(holder: VIEW_HOLDER) = Unit + + fun onViewRecycled(holder: VIEW_HOLDER) = Unit + + final override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return createViewHolder(inflater, parent, viewType) + } + + final override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: List + ): Boolean { + return onBindViewHolder(castHolder(holder), position, payloads) + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + onBindViewHolder(castHolder(holder), position) + } + + final override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + onViewAttachedToWindow(castHolder(holder)) + } + + final override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + onViewDetachedFromWindow(castHolder(holder)) + } + + final override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + onViewRecycled(castHolder(holder)) + } + + protected fun castHolder(holder: RecyclerView.ViewHolder): VIEW_HOLDER { + @Suppress("UNCHECKED_CAST") + return holder as VIEW_HOLDER + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseViewHolder.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseViewHolder.kt new file mode 100644 index 0000000..33d144f --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/base/BaseViewHolder.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.ui.cell.base + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder(root: View) : RecyclerView.ViewHolder(root) \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingCell.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingCell.kt new file mode 100644 index 0000000..1a171f4 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingCell.kt @@ -0,0 +1,24 @@ +package com.originsdigital.compositeadapter.ui.cell.viewbinding + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.originsdigital.compositeadapter.ui.cell.base.BaseCell + +abstract class ViewBindingCell + : BaseCell>() { + + abstract fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): VIEW_BINDING + + final override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): ViewBindingViewHolder { + return ViewBindingViewHolder(createViewBinding(inflater, parent, viewType)) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingViewHolder.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingViewHolder.kt new file mode 100644 index 0000000..a1a9025 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/cell/viewbinding/ViewBindingViewHolder.kt @@ -0,0 +1,8 @@ +package com.originsdigital.compositeadapter.ui.cell.viewbinding + +import androidx.viewbinding.ViewBinding +import com.originsdigital.compositeadapter.ui.cell.base.BaseViewHolder + +class ViewBindingViewHolder( + val binding: VIEW_BINDING +) : BaseViewHolder(binding.root) \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/di/UIModule.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/di/UIModule.kt new file mode 100644 index 0000000..655addc --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/di/UIModule.kt @@ -0,0 +1,20 @@ +package com.originsdigital.compositeadapter.ui.di + +import com.originsdigital.compositeadapter.ui.mapper.ErrorUIMapper +import com.originsdigital.compositeadapter.ui.mapper.LoaderUIMapper +import com.originsdigital.compositeadapter.ui.mapper.SceneMapper +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +val uiModule + get() = module { + factory { ErrorUIMapper() } + factory { LoaderUIMapper() } + factory { + SceneMapper( + app = androidApplication(), + loaderUIMapper = get(), + errorUIMapper = get() + ) + } + } \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonErrorUI.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonErrorUI.kt new file mode 100644 index 0000000..5914fae --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonErrorUI.kt @@ -0,0 +1,17 @@ +package com.originsdigital.compositeadapter.ui.entity + +import androidx.annotation.DimenRes +import androidx.annotation.StringRes + +data class CommonErrorUI( + val id: String, + val message: Message, + @DimenRes val heightId: Int? +) { + + sealed class Message { + + data class Text(val text: String?) : Message() + data class Resource(@StringRes val textId: Int) : Message() + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonLoaderUI.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonLoaderUI.kt new file mode 100644 index 0000000..5baf1b7 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/entity/CommonLoaderUI.kt @@ -0,0 +1,8 @@ +package com.originsdigital.compositeadapter.ui.entity + +import androidx.annotation.DimenRes + +data class CommonLoaderUI( + val id: String, + @DimenRes val heightId: Int? +) \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/extensions/CellExtensions.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/extensions/CellExtensions.kt new file mode 100644 index 0000000..44ec535 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/extensions/CellExtensions.kt @@ -0,0 +1,17 @@ +package com.originsdigital.compositeadapter.ui.extensions + +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.ui.cell.CommonErrorCell +import com.originsdigital.compositeadapter.ui.cell.CommonFullEmptyCell +import com.originsdigital.compositeadapter.ui.cell.CommonFullErrorCell +import com.originsdigital.compositeadapter.ui.cell.CommonFullLoaderCell +import com.originsdigital.compositeadapter.ui.cell.CommonLoaderCell + +val Cell<*>.isStateCell: Boolean + get() { + return this is CommonLoaderCell + || this is CommonFullLoaderCell + || this is CommonErrorCell + || this is CommonFullErrorCell + || this is CommonFullEmptyCell + } \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/BaseFragment.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/BaseFragment.kt new file mode 100644 index 0000000..4b8cc72 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/BaseFragment.kt @@ -0,0 +1,34 @@ +package com.originsdigital.compositeadapter.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding +import org.koin.core.component.KoinComponent + +abstract class BaseFragment : Fragment(), KoinComponent { + + protected open var viewBinding: VIEW_BINDING? = null + protected fun requireViewBinding(): VIEW_BINDING = requireNotNull(viewBinding) + protected abstract fun createViewBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): VIEW_BINDING + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return createViewBinding(inflater, container).also { + viewBinding = it + }.root + } + + override fun onDestroyView() { + super.onDestroyView() + viewBinding = null + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/SimpleRecyclerFragment.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/SimpleRecyclerFragment.kt new file mode 100644 index 0000000..d14cfb8 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/fragment/SimpleRecyclerFragment.kt @@ -0,0 +1,15 @@ +package com.originsdigital.compositeadapter.ui.fragment + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.ui.databinding.CommonRecyclerFragmentBinding + +abstract class SimpleRecyclerFragment : BaseFragment() { + + final override fun createViewBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): CommonRecyclerFragmentBinding { + return CommonRecyclerFragmentBinding.inflate(inflater, container, false) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/ErrorUIMapper.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/ErrorUIMapper.kt new file mode 100644 index 0000000..08f8322 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/ErrorUIMapper.kt @@ -0,0 +1,50 @@ +package com.originsdigital.compositeadapter.ui.mapper + +import androidx.annotation.DimenRes +import androidx.annotation.StringRes +import com.originsdigital.compositeadapter.core.extensions.isInternetConnectionException +import com.originsdigital.compositeadapter.ui.R +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI + +class ErrorUIMapper { + + fun mapError( + id: String, + text: String, + @DimenRes heightId: Int? = null + ): CommonErrorUI { + return CommonErrorUI( + id = id, + message = CommonErrorUI.Message.Text(text = text), + heightId = heightId + ) + } + + fun mapError( + id: String, + @StringRes textId: Int, + @DimenRes heightId: Int? = null + ): CommonErrorUI { + return CommonErrorUI( + id = id, + message = CommonErrorUI.Message.Resource(textId = textId), + heightId = heightId + ) + } + + fun mapError( + id: String, + throwable: Throwable, + @DimenRes heightId: Int? = null + ): CommonErrorUI { + return mapError( + id = id, + textId = if (throwable.isInternetConnectionException) { + R.string.common_no_connection_error + } else { + R.string.common_default_error + }, + heightId = heightId + ) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/LoaderUIMapper.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/LoaderUIMapper.kt new file mode 100644 index 0000000..baa07f2 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/LoaderUIMapper.kt @@ -0,0 +1,14 @@ +package com.originsdigital.compositeadapter.ui.mapper + +import androidx.annotation.DimenRes +import com.originsdigital.compositeadapter.ui.entity.CommonLoaderUI + +class LoaderUIMapper { + + fun mapLoader(id: String, @DimenRes heightId: Int?): CommonLoaderUI { + return CommonLoaderUI( + id = id, + heightId = heightId + ) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/SceneMapper.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/SceneMapper.kt new file mode 100644 index 0000000..55770b1 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/mapper/SceneMapper.kt @@ -0,0 +1,276 @@ +package com.originsdigital.compositeadapter.ui.mapper + +import android.app.Application +import android.util.TypedValue +import androidx.annotation.DimenRes +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.core.entity.Scene +import com.originsdigital.compositeadapter.core.log +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.decoration.SpaceItemDecoration +import com.originsdigital.compositeadapter.ui.cell.CommonErrorCell +import com.originsdigital.compositeadapter.ui.cell.CommonFullErrorCell +import com.originsdigital.compositeadapter.ui.cell.CommonFullLoaderCell +import com.originsdigital.compositeadapter.ui.cell.CommonLoaderCell +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI + +class SceneMapper( + app: Application, + val loaderUIMapper: LoaderUIMapper, + val errorUIMapper: ErrorUIMapper +) { + + private val stateItemDecoration: ItemDecoration> + + init { + val defaultSpace = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 40f, app.resources.displayMetrics + ).toInt() + stateItemDecoration = SpaceItemDecoration( + top = defaultSpace, + bottom = defaultSpace + ) + } + + fun isAllLoaders(vararg scenes: Scene<*>): Boolean { + return scenes.isNotEmpty() && scenes.all { scene -> scene is Scene.Loading } + } + + fun isAllErrors(vararg scenes: Scene<*>): Boolean { + return scenes.isNotEmpty() && scenes.all { scene -> scene is Scene.Error } + } + + fun isRefreshing(vararg scenes: Scene<*>): Boolean { + return scenes.isNotEmpty() && scenes.any { scene -> scene.isRefreshing } + } + + fun getErrorScene( + mainScene: Scene, + vararg scenes: Scene<*> + ): Scene.Error? { + return if (mainScene is Scene.Error && isAllErrors(*scenes)) { + mainScene + } else { + null + } + } + + fun mapScene( + scene: Scene, + loadingDelegate: (scene: Scene.Loading) -> CONTENT, + errorDelegate: (scene: Scene.Error) -> CONTENT, + dataDelegate: (Scene.Data) -> CONTENT + ): CONTENT { + log(this, "mapSceneToCells=${Thread.currentThread()}") + return when (scene) { + is Scene.Loading -> loadingDelegate(scene) + is Scene.Error -> errorDelegate(scene) + is Scene.Data -> dataDelegate(scene) + } + } + + fun mapSceneToCell( + scene: Scene, + loadingDelegate: (scene: Scene.Loading) -> Cell<*> = { getFullLoaderCell() }, + errorDelegate: (scene: Scene.Error) -> Cell<*>, + dataDelegate: (Scene.Data) -> Cell<*> + ): Cell<*> { + return mapScene( + scene = scene, + loadingDelegate = loadingDelegate, + errorDelegate = errorDelegate, + dataDelegate = dataDelegate + ) + } + + fun mapSceneToCellOrNull( + scene: Scene, + loadingDelegate: (scene: Scene.Loading) -> Cell<*>? = { getFullLoaderCell() }, + errorDelegate: (scene: Scene.Error) -> Cell<*>?, + dataDelegate: (Scene.Data) -> Cell<*>? + ): Cell<*>? { + return mapScene( + scene = scene, + loadingDelegate = loadingDelegate, + errorDelegate = errorDelegate, + dataDelegate = dataDelegate + ) + } + + fun mapSceneToCells( + scene: Scene, + loadingDelegate: (scene: Scene.Loading) -> List> = { + listOf(getFullLoaderCell()) + }, + errorDelegate: (scene: Scene.Error) -> List>, + dataDelegate: (Scene.Data) -> List> + ): List> { + return mapScene( + scene = scene, + loadingDelegate = loadingDelegate, + errorDelegate = errorDelegate, + dataDelegate = dataDelegate + ) + } + + fun mapSceneToCells( + scene: Scene, + onRetryClicked: (ClickItem) -> Unit, + dataDelegate: (Scene.Data) -> List> + ): List> { + return mapScene( + scene = scene, + loadingDelegate = { listOf(getFullLoaderCell()) }, + errorDelegate = { errorScene -> + listOf( + getFullErrorCell( + error = errorUIMapper.mapError( + id = "FullErrorCell", + throwable = errorScene.throwable + ), + decoration = null, + onRetryClicked = onRetryClicked + ) + ) + }, + dataDelegate = dataDelegate + ) + } + + fun mapSmallSceneToCell( + scene: Scene, + uniqueId: String, + onRetryClicked: ((ClickItem) -> Unit), + dataDelegate: (Scene.Data) -> Cell<*> + ): Cell<*> { + return mapSceneToCell( + scene = scene, + loadingDelegate = { getSmallLoaderCell(uniqueLoaderId = "loader=$uniqueId") }, + errorDelegate = { errorScene -> + getSmallErrorCell( + error = errorUIMapper.mapError( + id = "error=$uniqueId", + throwable = errorScene.throwable + ), + decoration = stateItemDecoration, + onRetryClicked = onRetryClicked + ) + }, + dataDelegate = dataDelegate + ) + } + + fun mapSmallSceneToCellOrNull( + scene: Scene, + uniqueId: String, + onRetryClicked: ((ClickItem) -> Unit), + dataDelegate: (Scene.Data) -> Cell<*>? + ): Cell<*>? { + return mapSceneToCellOrNull( + scene = scene, + loadingDelegate = { getSmallLoaderCell(uniqueLoaderId = uniqueId) }, + errorDelegate = { errorScene -> + getSmallErrorCell( + error = errorUIMapper.mapError( + id = "error=$uniqueId", + throwable = errorScene.throwable, + heightId = null + ), + decoration = stateItemDecoration, + onRetryClicked = onRetryClicked + ) + }, + dataDelegate = dataDelegate + ) + } + + fun mapSmallSceneToCells( + scene: Scene, + uniqueId: String, + onRetryClicked: ((ClickItem) -> Unit), + dataDelegate: (Scene.Data) -> List> + ): List> { + return mapSceneToCells( + scene = scene, + loadingDelegate = { listOf(getSmallLoaderCell(uniqueLoaderId = uniqueId)) }, + errorDelegate = { errorScene -> + listOf( + getSmallErrorCell( + error = errorUIMapper.mapError( + id = "error=$uniqueId", + throwable = errorScene.throwable, + heightId = null + ), + decoration = stateItemDecoration, + onRetryClicked = onRetryClicked + ) + ) + }, + dataDelegate = dataDelegate + ) + } + + fun getFullLoaderCell(): Cell<*> { + return CommonFullLoaderCell() + } + + fun getSmallLoaderCell( + uniqueLoaderId: String, + decoration: ItemDecoration>? = stateItemDecoration, + @DimenRes height: Int? = null + ): Cell<*> { + return CommonLoaderCell( + data = loaderUIMapper.mapLoader( + id = uniqueLoaderId, + heightId = height + ), + decoration = decoration + ) + } + + fun getErrorCell( + isFull: Boolean, + error: CommonErrorUI, + decoration: ItemDecoration>? = stateItemDecoration, + onRetryClicked: (ClickItem) -> Unit + ): Cell<*> { + return if (isFull) { + getFullErrorCell( + error = error, + decoration = null, + onRetryClicked = onRetryClicked + ) + } else { + getSmallErrorCell( + error = error, + decoration = decoration, + onRetryClicked = onRetryClicked + ) + } + } + + fun getFullErrorCell( + error: CommonErrorUI, + decoration: ItemDecoration>? = null, + onRetryClicked: (ClickItem) -> Unit + ): Cell<*> { + return CommonFullErrorCell( + data = error, + decoration = decoration, + onClickListener = onRetryClicked + ) + } + + fun getSmallErrorCell( + error: CommonErrorUI, + decoration: ItemDecoration>? = stateItemDecoration, + onRetryClicked: (ClickItem) -> Unit + ): Cell<*> { + return CommonErrorCell( + data = error, + decoration = decoration, + onClickListener = onRetryClicked + ) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/CompositeAdapterUtils.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/CompositeAdapterUtils.kt new file mode 100644 index 0000000..6d11aa0 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/CompositeAdapterUtils.kt @@ -0,0 +1,32 @@ +package com.originsdigital.compositeadapter.ui.utils + +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell + +fun submitAdapterData( + adapter: CompositeAdapter, + cells: List>, + commitCallback: Runnable? = null +) { + if (commitCallback == null) { + adapter.submitList(cells) + } else { + adapter.submitList(cells, commitCallback) + } +} + +fun submitAdapterData( + adapter: CompositeAdapter, + swipeRefreshLayout: SwipeRefreshLayout, + isRefreshing: Boolean, + cells: List>, + commitCallback: Runnable? = null +) { + swipeRefreshLayout.isRefreshing = isRefreshing + submitAdapterData( + adapter = adapter, + cells = cells, + commitCallback = commitCallback + ) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/FlowUtils.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/FlowUtils.kt new file mode 100644 index 0000000..e3c0f49 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/FlowUtils.kt @@ -0,0 +1,22 @@ +package com.originsdigital.compositeadapter.ui.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +fun observeFlow( + flow: Flow, + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.RESUMED, + delegate: suspend (T) -> Unit +) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.lifecycle.repeatOnLifecycle(lifecycleState) { + flow.collect { item -> delegate(item) } + } + } +} diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/RecyclerUtils.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/RecyclerUtils.kt new file mode 100644 index 0000000..99cfe67 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/RecyclerUtils.kt @@ -0,0 +1,87 @@ +package com.originsdigital.compositeadapter.ui.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.decoration.CompositeItemDecoration +import com.originsdigital.compositeadapter.ui.vm.BaseStateViewModel + +fun initRecyclerView( + recyclerView: RecyclerView, + adapter: CompositeAdapter = CompositeAdapter(), + layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(recyclerView.context) +) { + recyclerView.adapter = adapter + recyclerView.layoutManager = layoutManager + recyclerView.addItemDecoration(CompositeItemDecoration()) +} + +fun initRecyclerView( + recyclerView: RecyclerView, + adapter: CompositeAdapter = CompositeAdapter(), + layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(recyclerView.context), + swipeRefreshLayout: SwipeRefreshLayout, + onRefresh: () -> Unit +) { + initRecyclerView( + recyclerView = recyclerView, + adapter = adapter, + layoutManager = layoutManager + ) + initSwipeRefresh( + swipeRefreshLayout = swipeRefreshLayout, + onRefresh = onRefresh + ) +} + +fun > initRecyclerView( + recyclerView: RecyclerView, + adapter: CompositeAdapter = CompositeAdapter(), + layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(recyclerView.context), + swipeRefreshLayout: SwipeRefreshLayout, + viewModel: VIEW_MODEL, + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.RESUMED, + stateDelegate: (CompositeAdapter, state: STATE) -> Unit +) { + initRecyclerView( + recyclerView = recyclerView, + adapter = adapter, + layoutManager = layoutManager, + swipeRefreshLayout = swipeRefreshLayout, + onRefresh = viewModel::onRefresh + ) + + observeCellViewModel( + viewModel = viewModel, + lifecycleOwner = lifecycleOwner, + lifecycleState = lifecycleState, + stateDelegate = { state -> stateDelegate(adapter, state) } + ) +} + +fun > initRecyclerView( + recyclerView: RecyclerView, + adapter: CompositeAdapter = CompositeAdapter(), + layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(recyclerView.context), + viewModel: VIEW_MODEL, + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.RESUMED, + stateDelegate: (CompositeAdapter, state: STATE) -> Unit +) { + initRecyclerView( + recyclerView = recyclerView, + adapter = adapter, + layoutManager = layoutManager + ) + + observeCellViewModel( + viewModel = viewModel, + lifecycleOwner = lifecycleOwner, + lifecycleState = lifecycleState, + stateDelegate = { state -> stateDelegate(adapter, state) } + ) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/SwipeRefreshUtils.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/SwipeRefreshUtils.kt new file mode 100644 index 0000000..f995211 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/SwipeRefreshUtils.kt @@ -0,0 +1,29 @@ +package com.originsdigital.compositeadapter.ui.utils + +import android.graphics.Color +import androidx.core.content.res.use +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.originsdigital.compositeadapter.ui.R + +fun initSwipeRefresh( + swipeRefreshLayout: SwipeRefreshLayout, + onRefresh: () -> Unit +) { + val schemeColor = swipeRefreshLayout.context.obtainStyledAttributes( + intArrayOf(R.attr.colorSecondary) + ).use { + it.getColor(0, Color.BLACK) + } + swipeRefreshLayout.setColorSchemeColors(schemeColor, schemeColor, schemeColor) + swipeRefreshLayout.setOnRefreshListener { onRefresh() } +} + +fun setSwipeEnabled( + swipeRefreshLayout: SwipeRefreshLayout, + isEnabled: Boolean +) { + if (!isEnabled) { + swipeRefreshLayout.isRefreshing = false + } + swipeRefreshLayout.isEnabled = isEnabled +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/ViewModelUtils.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/ViewModelUtils.kt new file mode 100644 index 0000000..d0e33f3 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/utils/ViewModelUtils.kt @@ -0,0 +1,34 @@ +package com.originsdigital.compositeadapter.ui.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.originsdigital.compositeadapter.ui.vm.BaseStateViewModel +import com.originsdigital.compositeadapter.ui.vm.BaseViewModel + +fun observeLifecycle( + viewModel: BaseViewModel, + lifecycleOwner: LifecycleOwner +) { + lifecycleOwner.lifecycle.addObserver(viewModel) +} + +fun observeCellViewModel( + viewModel: VIEW_MODEL, + lifecycleOwner: LifecycleOwner, + lifecycleState: Lifecycle.State = Lifecycle.State.RESUMED, + withLifecycle: Boolean = true, + stateDelegate: (STATE) -> Unit +) where VIEW_MODEL : BaseStateViewModel { + if (withLifecycle) { + observeLifecycle( + viewModel = viewModel, + lifecycleOwner = lifecycleOwner + ) + } + observeFlow( + flow = viewModel.stateFlow, + lifecycleOwner = lifecycleOwner, + lifecycleState = lifecycleState, + delegate = stateDelegate + ) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseDataViewModel.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseDataViewModel.kt new file mode 100644 index 0000000..f2b598f --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseDataViewModel.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.ui.vm + +abstract class BaseDataViewModel : BaseViewModel() { + + abstract fun onRefresh() +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseStateViewModel.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseStateViewModel.kt new file mode 100644 index 0000000..723f3ab --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseStateViewModel.kt @@ -0,0 +1,102 @@ +package com.originsdigital.compositeadapter.ui.vm + +import androidx.lifecycle.viewModelScope +import com.originsdigital.compositeadapter.core.entity.Scene +import com.originsdigital.compositeadapter.core.extensions.toData +import com.originsdigital.compositeadapter.core.extensions.toError +import com.originsdigital.compositeadapter.core.extensions.toLoading +import com.originsdigital.compositeadapter.core.log +import com.originsdigital.compositeadapter.core.utils.safeLoad +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +abstract class BaseStateViewModel : BaseDataViewModel() { + + abstract val stateFlow: Flow + + protected fun loadBlockWithState( + text: String, + force: Boolean, + currentSceneDelegate: () -> Scene, + newSceneDelegate: (Scene) -> Unit, + block: (suspend CoroutineScope.() -> DATA) + ): Job { + return viewModelScope.launch { + withContext(bgDispatcher) { + safeLoad( + scope = this, + text = text, + force = force, + currentSceneDelegate = currentSceneDelegate, + newSceneDelegate = newSceneDelegate, + block = block + ) + } + } + } + + protected fun loadBlockWithState( + text: String, + force: Boolean, + sceneFlow: MutableStateFlow>, + block: (suspend CoroutineScope.() -> DATA) + ): Job { + return loadBlockWithState( + text = text, + force = force, + currentSceneDelegate = { sceneFlow.value }, + newSceneDelegate = { newScene -> sceneFlow.value = newScene }, + block = block + ) + } + + protected fun loadFlowWithState( + text: String, + force: Boolean, + currentSceneDelegate: () -> Scene, + newSceneDelegate: (Scene) -> Unit, + flow: Flow + ): Job { + val tag = this@BaseStateViewModel + return viewModelScope.launch { + flow + .catch { error -> + log(tag, "$text error = $error") + newSceneDelegate(currentSceneDelegate().toError(error = error, force = force)) + } + .onStart { + log(tag, "$text onStart") + newSceneDelegate(currentSceneDelegate().toLoading(force = force)) + } + .collect { data -> + log(tag, "$text collect = $data") + newSceneDelegate(currentSceneDelegate().toData(data = data)) + } + } + } + + private suspend fun safeLoad( + scope: CoroutineScope, + text: String, + force: Boolean, + currentSceneDelegate: () -> Scene, + newSceneDelegate: (Scene) -> Unit, + block: (suspend CoroutineScope.() -> DATA) + ) { + safeLoad>( + tag = this@BaseStateViewModel, + text = text, + block = { + newSceneDelegate(currentSceneDelegate().toLoading(force = force)) + currentSceneDelegate() + .toData(data = block(scope)) + .also { newScene -> newSceneDelegate(newScene) } + }, + error = { error -> + currentSceneDelegate() + .toError(error = error, force = force) + .also { newScene -> newSceneDelegate(newScene) } + } + ) + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseViewModel.kt b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseViewModel.kt new file mode 100644 index 0000000..0c88c14 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/java/com/originsdigital/compositeadapter/ui/vm/BaseViewModel.kt @@ -0,0 +1,26 @@ +package com.originsdigital.compositeadapter.ui.vm + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.ViewModel +import com.originsdigital.compositeadapter.core.di.BG_DISPATCHER_QUALIFIER +import com.originsdigital.compositeadapter.core.di.UI_DISPATCHER_QUALIFIER +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import org.koin.core.context.GlobalContext + +abstract class BaseViewModel : ViewModel(), DefaultLifecycleObserver { + + protected val bgDispatcher: CoroutineDispatcher by lazy { + GlobalContext.get().get(BG_DISPATCHER_QUALIFIER) + } + protected val uiDispatcher: CoroutineDispatcher by lazy { + GlobalContext.get().get(UI_DISPATCHER_QUALIFIER) + } + + protected var job: Job? = null + + override fun onCleared() { + job?.cancel() + super.onCleared() + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/res/layout/common_error_cell.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_error_cell.xml new file mode 100644 index 0000000..58e1936 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_error_cell.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_empty_cell.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_empty_cell.xml new file mode 100644 index 0000000..ddb05e3 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_empty_cell.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_error_cell.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_error_cell.xml new file mode 100644 index 0000000..ddb05e3 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_error_cell.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_loader_cell.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_loader_cell.xml new file mode 100644 index 0000000..c9ec6aa --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_full_loader_cell.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/samples/state-as-cells/features/base/ui/src/main/res/layout/common_loader_cell.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_loader_cell.xml new file mode 100644 index 0000000..58e8173 --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_loader_cell.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/sample_activity.xml b/samples/state-as-cells/features/base/ui/src/main/res/layout/common_recycler_fragment.xml similarity index 100% rename from sample/src/main/res/layout/sample_activity.xml rename to samples/state-as-cells/features/base/ui/src/main/res/layout/common_recycler_fragment.xml diff --git a/samples/state-as-cells/features/base/ui/src/main/res/values/colors.xml b/samples/state-as-cells/features/base/ui/src/main/res/values/colors.xml new file mode 100644 index 0000000..257344d --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/values/colors.xml @@ -0,0 +1,22 @@ + + + @color/dusty_orange + @color/dusty_orange + @color/compositeadapter_white + @color/dusty_orange + @color/dusty_orange + @color/compositeadapter_white + @color/pinkish_red + @color/compositeadapter_white + @color/compositeadapter_white + @color/compositeadapter_black + @color/compositeadapter_white + @color/compositeadapter_black + + #FFFFFF + #000000 + #00000000 + + #F47A31 + #EB2227 + diff --git a/samples/state-as-cells/features/base/ui/src/main/res/values/strings.xml b/samples/state-as-cells/features/base/ui/src/main/res/values/strings.xml new file mode 100644 index 0000000..445a77e --- /dev/null +++ b/samples/state-as-cells/features/base/ui/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + No connection + Error + \ No newline at end of file diff --git a/samples/state-as-cells/features/home/core/.gitignore b/samples/state-as-cells/features/home/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/home/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/home/core/build.gradle.kts b/samples/state-as-cells/features/home/core/build.gradle.kts new file mode 100644 index 0000000..27fe096 --- /dev/null +++ b/samples/state-as-cells/features/home/core/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") + `java-library` +} + +java.sourceCompatibility = Config.Build.javaVersion +java.targetCompatibility = Config.Build.javaVersion + +project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseCore)) + implementation(project(Config.Deps.Libs.storiesCore)) + implementation(project(Config.Deps.Libs.newsCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/core/consumer-rules.pro b/samples/state-as-cells/features/home/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/home/core/proguard-rules.pro b/samples/state-as-cells/features/home/core/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/home/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/home/core/src/main/java/com/originsdigital/compositeadapter/home/core/repository/HomeRepository.kt b/samples/state-as-cells/features/home/core/src/main/java/com/originsdigital/compositeadapter/home/core/repository/HomeRepository.kt new file mode 100644 index 0000000..f698c61 --- /dev/null +++ b/samples/state-as-cells/features/home/core/src/main/java/com/originsdigital/compositeadapter/home/core/repository/HomeRepository.kt @@ -0,0 +1,11 @@ +package com.originsdigital.compositeadapter.home.core.repository + +import com.originsdigital.compositeadapter.news.core.entity.NewsEntity +import com.originsdigital.compositeadapter.stories.core.entity.StoryEntity + +interface HomeRepository { + + suspend fun getStories(): List + + suspend fun getNews(): List +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/.gitignore b/samples/state-as-cells/features/home/data/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/home/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/build.gradle.kts b/samples/state-as-cells/features/home/data/build.gradle.kts new file mode 100644 index 0000000..4ac3e95 --- /dev/null +++ b/samples/state-as-cells/features/home/data/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseCore)) + implementation(project(Config.Deps.Libs.homeCore)) + implementation(project(Config.Deps.Libs.storiesCore)) + implementation(project(Config.Deps.Libs.newsCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/consumer-rules.pro b/samples/state-as-cells/features/home/data/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/home/data/proguard-rules.pro b/samples/state-as-cells/features/home/data/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/home/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/src/main/AndroidManifest.xml b/samples/state-as-cells/features/home/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000..86f34b9 --- /dev/null +++ b/samples/state-as-cells/features/home/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/di/HomeDataModule.kt b/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/di/HomeDataModule.kt new file mode 100644 index 0000000..7869950 --- /dev/null +++ b/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/di/HomeDataModule.kt @@ -0,0 +1,12 @@ +package com.originsdigital.compositeadapter.stories.data.di + +import com.originsdigital.compositeadapter.home.core.repository.HomeRepository +import com.originsdigital.compositeadapter.stories.data.repository.HomeRepositoryImpl +import org.koin.core.module.Module +import org.koin.dsl.module + +fun homeDataModule(): Module { + return module { + factory { HomeRepositoryImpl() } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/repository/HomeRepositoryImpl.kt b/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/repository/HomeRepositoryImpl.kt new file mode 100644 index 0000000..5f426b0 --- /dev/null +++ b/samples/state-as-cells/features/home/data/src/main/java/com/originsdigital/compositeadapter/stories/data/repository/HomeRepositoryImpl.kt @@ -0,0 +1,51 @@ +package com.originsdigital.compositeadapter.stories.data.repository + +import android.graphics.Color +import com.originsdigital.compositeadapter.home.core.repository.HomeRepository +import com.originsdigital.compositeadapter.news.core.entity.NewsEntity +import com.originsdigital.compositeadapter.stories.core.entity.StoryEntity +import kotlinx.coroutines.delay +import kotlin.random.Random + +class HomeRepositoryImpl : HomeRepository { + + override suspend fun getStories(): List { + loadData() + val colors = listOf( + Color.BLACK, + Color.BLUE, + Color.CYAN, + Color.DKGRAY, + Color.GRAY, + Color.GREEN, + Color.LTGRAY, + Color.MAGENTA, + Color.RED, + Color.YELLOW + ) + return (0..20).map { index -> + StoryEntity( + id = index.toString(), + colorInt = colors[index % colors.size], + name = "Story $index" + ) + } + .filter { Random.nextBoolean() } + } + + override suspend fun getNews(): List { + loadData() + return (0..50).map { index -> + NewsEntity(id = index.toString(), text = "News $index") + } + .filter { Random.nextBoolean() } + } + + private suspend fun loadData() { + val delay = 500 + Random.nextLong(500) + delay(delay) + if (delay > 800) { + throw NullPointerException() + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/.gitignore b/samples/state-as-cells/features/home/ui/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/home/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/build.gradle.kts b/samples/state-as-cells/features/home/ui/build.gradle.kts new file mode 100644 index 0000000..b9e2188 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseUI)) + implementation(project(Config.Deps.Libs.homeCore)) + implementation(project(Config.Deps.Libs.storiesCore)) + implementation(project(Config.Deps.Libs.storiesUI)) + implementation(project(Config.Deps.Libs.newsCore)) + implementation(project(Config.Deps.Libs.newsUI)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/consumer-rules.pro b/samples/state-as-cells/features/home/ui/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/home/ui/proguard-rules.pro b/samples/state-as-cells/features/home/ui/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/AndroidManifest.xml b/samples/state-as-cells/features/home/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ef37bb3 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeActivity.kt b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeActivity.kt new file mode 100644 index 0000000..b7a379c --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeActivity.kt @@ -0,0 +1,44 @@ +package com.originsdigital.compositeadapter.stateascells.home.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.originsdigital.compositeadapter.stateascells.home.ui.databinding.HomeActivityBinding +import com.originsdigital.compositeadapter.ui.utils.initRecyclerView +import com.originsdigital.compositeadapter.ui.utils.submitAdapterData +import org.koin.androidx.viewmodel.ext.android.getViewModel + +class HomeActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = HomeActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + val viewModel: HomeViewModel = getViewModel() + + binding.apply { + initRecyclerView( + recyclerView = recyclerView, + swipeRefreshLayout = swipeRefreshLayout, + viewModel = viewModel, + lifecycleOwner = this@HomeActivity, + stateDelegate = { adapter, state -> + submitAdapterData( + adapter = adapter, + swipeRefreshLayout = swipeRefreshLayout, + isRefreshing = state.isRefreshing, + cells = state.cells + ) + } + ) + } + } + + companion object { + fun getLaunchIntent(context: Context): Intent { + return Intent(context, HomeActivity::class.java) + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeUIMapper.kt b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeUIMapper.kt new file mode 100644 index 0000000..eda0b91 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeUIMapper.kt @@ -0,0 +1,131 @@ +package com.originsdigital.compositeadapter.stateascells.home.ui + +import android.app.Application +import android.content.Context +import android.util.TypedValue +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.core.entity.Scene +import com.originsdigital.compositeadapter.decoration.SpaceItemDecoration +import com.originsdigital.compositeadapter.news.core.entity.NewsEntity +import com.originsdigital.compositeadapter.news.ui.cell.NewsCell +import com.originsdigital.compositeadapter.stories.core.entity.StoryEntity +import com.originsdigital.compositeadapter.stories.ui.cell.StoriesCell +import com.originsdigital.compositeadapter.stories.ui.cell.StoryCell +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI +import com.originsdigital.compositeadapter.ui.mapper.SceneMapper + +class HomeUIMapper( + app: Application, + private val sceneMapper: SceneMapper +) { + + private val firstStoryItemDecoration: SpaceItemDecoration> + private val middleStoryBigItemDecoration: SpaceItemDecoration> + private val lastStoryItemDecoration: SpaceItemDecoration> + private val firstNewsItemDecoration: SpaceItemDecoration> + private val middleNewsBigItemDecoration: SpaceItemDecoration> + private val lastNewsItemDecoration: SpaceItemDecoration> + + init { + firstStoryItemDecoration = SpaceItemDecoration( + top = app.dpToPx(8f).toInt(), + bottom = app.dpToPx(8f).toInt(), + start = app.dpToPx(8f).toInt() + ) + middleStoryBigItemDecoration = firstStoryItemDecoration + lastStoryItemDecoration = SpaceItemDecoration( + top = firstStoryItemDecoration.top, + bottom = firstStoryItemDecoration.bottom, + start = firstStoryItemDecoration.start, + end = firstStoryItemDecoration.start + ) + firstNewsItemDecoration = SpaceItemDecoration( + top = app.dpToPx(8f).toInt(), + start = app.dpToPx(8f).toInt(), + end = app.dpToPx(8f).toInt() + ) + middleNewsBigItemDecoration = firstNewsItemDecoration + lastNewsItemDecoration = SpaceItemDecoration( + top = firstStoryItemDecoration.top, + bottom = firstStoryItemDecoration.top, + start = firstStoryItemDecoration.start, + end = firstStoryItemDecoration.end + ) + } + + private fun Context.dpToPx(dp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) + } + + fun mapState(): HomeViewModel.State { + return HomeViewModel.State( + isRefreshing = false, + cells = emptyList() + ) + } + + fun mapState( + storiesScene: Scene>, + newsScene: Scene>, + onStoriesRetryClicked: ((ClickItem) -> Unit), + onNewsRetryClicked: ((ClickItem) -> Unit), + onStoryClickListener: ((ClickItem) -> Unit), + onNewsClickListener: ((ClickItem) -> Unit) + ): HomeViewModel.State { + val storiesCell = sceneMapper.mapSmallSceneToCell( + scene = storiesScene, + uniqueId = "stories", + onRetryClicked = onStoriesRetryClicked, + dataDelegate = { dataScene -> mapStories(dataScene, onStoryClickListener) } + ) + val newsCells = sceneMapper.mapSmallSceneToCells( + scene = newsScene, + uniqueId = "news", + onRetryClicked = onNewsRetryClicked, + dataDelegate = { dataScene -> mapNews(dataScene, onNewsClickListener) } + ) + return HomeViewModel.State( + isRefreshing = sceneMapper.isRefreshing(storiesScene, newsScene), + cells = listOf(storiesCell) + newsCells + ) + } + + private fun mapStories( + storiesScene: Scene.Data>, + onStoryClickListener: ((ClickItem) -> Unit) + ): Cell<*> { + return StoriesCell( + data = storiesScene.data.mapIndexed { index, story -> + val decoration = when (index) { + 0 -> firstStoryItemDecoration + storiesScene.data.size - 1 -> lastStoryItemDecoration + else -> middleStoryBigItemDecoration + } + StoryCell( + data = story, + decoration = decoration, + onClickListener = onStoryClickListener + ) + } + ) + } + + private fun mapNews( + newsScene: Scene.Data>, + onNewsClickListener: ((ClickItem) -> Unit) + ): List> { + return newsScene.data.mapIndexed { index, story -> + val decoration = when (index) { + 0 -> firstNewsItemDecoration + newsScene.data.size - 1 -> lastNewsItemDecoration + else -> middleNewsBigItemDecoration + } + NewsCell( + data = story, + decoration = decoration, + onClickListener = onNewsClickListener + ) + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeViewModel.kt b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeViewModel.kt new file mode 100644 index 0000000..449e4fd --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/HomeViewModel.kt @@ -0,0 +1,88 @@ +package com.originsdigital.compositeadapter.stateascells.home.ui + +import androidx.lifecycle.viewModelScope +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.core.entity.Scene +import com.originsdigital.compositeadapter.core.extensions.isEmpty +import com.originsdigital.compositeadapter.home.core.repository.HomeRepository +import com.originsdigital.compositeadapter.news.core.entity.NewsEntity +import com.originsdigital.compositeadapter.stories.core.entity.StoryEntity +import com.originsdigital.compositeadapter.ui.entity.CommonErrorUI +import com.originsdigital.compositeadapter.ui.vm.BaseStateViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +class HomeViewModel( + private val homeRepository: HomeRepository, + private val homeUIMapper: HomeUIMapper +) : BaseStateViewModel() { + + private val onStoriesRetryClicked: ((ClickItem) -> Unit) = { onRefresh() } + private val onNewsRetryClicked: ((ClickItem) -> Unit) = { onRefresh() } + private val onStoryClickListener: ((ClickItem) -> Unit) = { click -> + } + private val onNewsClickListener: ((ClickItem) -> Unit) = { click -> + } + + private val storiesFlow = MutableStateFlow>>(Scene.Loading()) + private val newsFlow = MutableStateFlow>>(Scene.Loading()) + + override val stateFlow: StateFlow = combine( + storiesFlow, + newsFlow + ) { storiesScene, newsScene -> + homeUIMapper.mapState( + storiesScene = storiesScene, + newsScene = newsScene, + onStoriesRetryClicked = onStoriesRetryClicked, + onNewsRetryClicked = onNewsRetryClicked, + onStoryClickListener = onStoryClickListener, + onNewsClickListener = onNewsClickListener + ) + } + .flowOn(bgDispatcher) + .stateIn(viewModelScope, SharingStarted.Lazily, homeUIMapper.mapState()) + + private var storiesJob: Job? = null + private var newsJob: Job? = null + + init { + onRefresh() + } + + override fun onRefresh() { + loadStories() + loadNews() + } + + private fun loadStories(force: Boolean = storiesFlow.value.isEmpty) { + storiesJob?.cancel() + storiesJob = loadBlockWithState( + text = "loadStories", + force = force, + sceneFlow = storiesFlow, + block = { homeRepository.getStories() } + ) + } + + private fun loadNews(force: Boolean = newsFlow.value.isEmpty) { + newsJob?.cancel() + newsJob = loadBlockWithState( + text = "loadNews", + force = force, + sceneFlow = newsFlow, + block = { homeRepository.getNews() } + ) + } + + data class State( + val isRefreshing: Boolean, + val cells: List> + ) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/di/HomeUIModule.kt b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/di/HomeUIModule.kt new file mode 100644 index 0000000..8c01b52 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/java/com/originsdigital/compositeadapter/stateascells/home/ui/di/HomeUIModule.kt @@ -0,0 +1,25 @@ +package com.originsdigital.compositeadapter.stateascells.home.ui.di + +import com.originsdigital.compositeadapter.stateascells.home.ui.HomeUIMapper +import com.originsdigital.compositeadapter.stateascells.home.ui.HomeViewModel +import org.koin.android.ext.koin.androidApplication +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.Module +import org.koin.dsl.module + +fun homeUIModule(): Module { + return module { + factory { + HomeUIMapper( + app = androidApplication(), + sceneMapper = get() + ) + } + viewModel { + HomeViewModel( + homeRepository = get(), + homeUIMapper = get() + ) + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/home/ui/src/main/res/layout/home_activity.xml b/samples/state-as-cells/features/home/ui/src/main/res/layout/home_activity.xml new file mode 100644 index 0000000..ac92298 --- /dev/null +++ b/samples/state-as-cells/features/home/ui/src/main/res/layout/home_activity.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/samples/state-as-cells/features/news/core/.gitignore b/samples/state-as-cells/features/news/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/news/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/news/core/build.gradle.kts b/samples/state-as-cells/features/news/core/build.gradle.kts new file mode 100644 index 0000000..3765cf9 --- /dev/null +++ b/samples/state-as-cells/features/news/core/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") + `java-library` +} + +java.sourceCompatibility = Config.Build.javaVersion +java.targetCompatibility = Config.Build.javaVersion + +project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/news/core/consumer-rules.pro b/samples/state-as-cells/features/news/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/news/core/proguard-rules.pro b/samples/state-as-cells/features/news/core/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/news/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/news/core/src/main/java/com/originsdigital/compositeadapter/news/core/entity/NewsEntity.kt b/samples/state-as-cells/features/news/core/src/main/java/com/originsdigital/compositeadapter/news/core/entity/NewsEntity.kt new file mode 100644 index 0000000..6a9aba2 --- /dev/null +++ b/samples/state-as-cells/features/news/core/src/main/java/com/originsdigital/compositeadapter/news/core/entity/NewsEntity.kt @@ -0,0 +1,6 @@ +package com.originsdigital.compositeadapter.news.core.entity + +data class NewsEntity( + val id: String, + val text: String? +) \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/.gitignore b/samples/state-as-cells/features/news/ui/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/news/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/build.gradle.kts b/samples/state-as-cells/features/news/ui/build.gradle.kts new file mode 100644 index 0000000..c117c3c --- /dev/null +++ b/samples/state-as-cells/features/news/ui/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseUI)) + implementation(project(Config.Deps.Libs.newsCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/consumer-rules.pro b/samples/state-as-cells/features/news/ui/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/news/ui/proguard-rules.pro b/samples/state-as-cells/features/news/ui/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/news/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/src/main/AndroidManifest.xml b/samples/state-as-cells/features/news/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a46ba7d --- /dev/null +++ b/samples/state-as-cells/features/news/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/src/main/java/com/originsdigital/compositeadapter/news/ui/cell/NewsCell.kt b/samples/state-as-cells/features/news/ui/src/main/java/com/originsdigital/compositeadapter/news/ui/cell/NewsCell.kt new file mode 100644 index 0000000..f25e646 --- /dev/null +++ b/samples/state-as-cells/features/news/ui/src/main/java/com/originsdigital/compositeadapter/news/ui/cell/NewsCell.kt @@ -0,0 +1,37 @@ +package com.originsdigital.compositeadapter.news.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.news.core.entity.NewsEntity +import com.originsdigital.compositeadapter.news.ui.R +import com.originsdigital.compositeadapter.news.ui.databinding.NewsCellBinding +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder + +data class NewsCell( + override val data: NewsEntity, + override val decoration: ItemDecoration>?, + override val onClickListener: ((ClickItem) -> Unit)? +) : ViewBindingCell() { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.news_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): NewsCellBinding { + return NewsCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.text.text = data.text + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/news/ui/src/main/res/layout/news_cell.xml b/samples/state-as-cells/features/news/ui/src/main/res/layout/news_cell.xml new file mode 100644 index 0000000..b007c37 --- /dev/null +++ b/samples/state-as-cells/features/news/ui/src/main/res/layout/news_cell.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/core/.gitignore b/samples/state-as-cells/features/stories/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/stories/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/core/build.gradle.kts b/samples/state-as-cells/features/stories/core/build.gradle.kts new file mode 100644 index 0000000..3765cf9 --- /dev/null +++ b/samples/state-as-cells/features/stories/core/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") + `java-library` +} + +java.sourceCompatibility = Config.Build.javaVersion +java.targetCompatibility = Config.Build.javaVersion + +project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/core/consumer-rules.pro b/samples/state-as-cells/features/stories/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/stories/core/proguard-rules.pro b/samples/state-as-cells/features/stories/core/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/stories/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/core/src/main/java/com/originsdigital/compositeadapter/stories/core/entity/StoryEntity.kt b/samples/state-as-cells/features/stories/core/src/main/java/com/originsdigital/compositeadapter/stories/core/entity/StoryEntity.kt new file mode 100644 index 0000000..be3ea29 --- /dev/null +++ b/samples/state-as-cells/features/stories/core/src/main/java/com/originsdigital/compositeadapter/stories/core/entity/StoryEntity.kt @@ -0,0 +1,7 @@ +package com.originsdigital.compositeadapter.stories.core.entity + +data class StoryEntity( + val id: String, + val colorInt: Int, + val name: String? +) \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/.gitignore b/samples/state-as-cells/features/stories/ui/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/build.gradle.kts b/samples/state-as-cells/features/stories/ui/build.gradle.kts new file mode 100644 index 0000000..e515fe1 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id(Config.Plugins.androidLibrary) + kotlin(Config.Plugins.android) +} + +android { + compileSdk = Config.Build.compileSdk + + defaultConfig { + minSdk = Config.Build.sampleMinSdk + targetSdk = Config.Build.targetSdk + } + + compileOptions { + sourceCompatibility = Config.Build.javaVersion + targetCompatibility = Config.Build.javaVersion + } + kotlinOptions { + jvmTarget = Config.Build.javaVersion.toString() + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(Config.Deps.Libs.baseUI)) + implementation(project(Config.Deps.Libs.storiesCore)) +} \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/consumer-rules.pro b/samples/state-as-cells/features/stories/ui/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/samples/state-as-cells/features/stories/ui/proguard-rules.pro b/samples/state-as-cells/features/stories/ui/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/AndroidManifest.xml b/samples/state-as-cells/features/stories/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c170164 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoriesCell.kt b/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoriesCell.kt new file mode 100644 index 0000000..b8fccfb --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoriesCell.kt @@ -0,0 +1,63 @@ +package com.originsdigital.compositeadapter.stories.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.originsdigital.compositeadapter.adapter.CompositeAdapter +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.stories.ui.R +import com.originsdigital.compositeadapter.ui.cell.base.BaseCell +import com.originsdigital.compositeadapter.ui.cell.base.BaseViewHolder +import com.originsdigital.compositeadapter.ui.utils.initRecyclerView + +data class StoriesCell( + override val data: List, + override val decoration: ItemDecoration>? = null +) : BaseCell, StoriesCell.CustomViewHolder>() { + + override val uniqueId: String = "StoriesCell" + override val layoutId: Int = R.layout.stories_cell + + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): CustomViewHolder { + return CustomViewHolder( + recyclerView = RecyclerView(inflater.context).apply { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) + } + ) + } + + override fun onBindViewHolder( + holder: CustomViewHolder, + position: Int + ) { + holder.compositeAdapter.submitList(data) + } + + override fun getChangePayload(newItem: Cell<*>): Any = "RECYCLER_VIEW" + + class CustomViewHolder(recyclerView: RecyclerView) : BaseViewHolder(recyclerView) { + + val compositeAdapter = CompositeAdapter() + + init { + initRecyclerView( + recyclerView = recyclerView, + adapter = compositeAdapter, + layoutManager = LinearLayoutManager( + recyclerView.context, + LinearLayoutManager.HORIZONTAL, + false + ) + ) + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoryCell.kt b/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoryCell.kt new file mode 100644 index 0000000..6a146f8 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/java/com/originsdigital/compositeadapter/stories/ui/cell/StoryCell.kt @@ -0,0 +1,40 @@ +package com.originsdigital.compositeadapter.stories.ui.cell + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.originsdigital.compositeadapter.cell.Cell +import com.originsdigital.compositeadapter.cell.ClickItem +import com.originsdigital.compositeadapter.decoration.ItemDecoration +import com.originsdigital.compositeadapter.stories.core.entity.StoryEntity +import com.originsdigital.compositeadapter.stories.ui.R +import com.originsdigital.compositeadapter.stories.ui.databinding.StoryCellBinding +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingCell +import com.originsdigital.compositeadapter.ui.cell.viewbinding.ViewBindingViewHolder + +data class StoryCell( + override val data: StoryEntity, + override val decoration: ItemDecoration>? = null, + override val onClickListener: ((ClickItem) -> Unit)? = null +) : ViewBindingCell() { + + override val uniqueId: String = data.id + override val layoutId: Int = R.layout.story_cell + + override fun createViewBinding( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): StoryCellBinding { + return StoryCellBinding.inflate(inflater, parent, false) + } + + override fun onBindViewHolder( + holder: ViewBindingViewHolder, + position: Int + ) { + holder.binding.apply { + image.setColorFilter(data.colorInt) + text.text = data.name + } + } +} \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/res/drawable/story_cell_image_background.xml b/samples/state-as-cells/features/stories/ui/src/main/res/drawable/story_cell_image_background.xml new file mode 100644 index 0000000..b9699b6 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/res/drawable/story_cell_image_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/res/layout/story_cell.xml b/samples/state-as-cells/features/stories/ui/src/main/res/layout/story_cell.xml new file mode 100644 index 0000000..1c49cf2 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/res/layout/story_cell.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/samples/state-as-cells/features/stories/ui/src/main/res/values/ids.xml b/samples/state-as-cells/features/stories/ui/src/main/res/values/ids.xml new file mode 100644 index 0000000..aa8d1b7 --- /dev/null +++ b/samples/state-as-cells/features/stories/ui/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 82725d6..1b56029 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,21 @@ rootProject.name = "CompositeAdapter_Android" include(":composite-adapter") -include(":sample") \ No newline at end of file +include(":samples:basic") + +include(":samples:decorations") + +include(":samples:different-bindings") + +include(":samples:inner-recyclerview") + +include(":samples:state-as-cells:app") +include(":samples:state-as-cells:features:base:core") +include(":samples:state-as-cells:features:base:ui") +include(":samples:state-as-cells:features:home:data") +include(":samples:state-as-cells:features:home:core") +include(":samples:state-as-cells:features:home:ui") +include(":samples:state-as-cells:features:stories:core") +include(":samples:state-as-cells:features:stories:ui") +include(":samples:state-as-cells:features:news:core") +include(":samples:state-as-cells:features:news:ui") \ No newline at end of file