Skip to content

KotlinMultiplatform items for RecyclerView, UICollectionView and UITableView

License

Notifications You must be signed in to change notification settings

vchernyshov/kmp-items

Repository files navigation

KotlinMultiPlatform Items

License: MIT Download kotlin-version

This is multiplatform solution to create list based interfaces from common code.

Original concept proposed by Hannes Dorfmann in AdapterDelegates.
This version is multiplatform implementation of modified solution.

Installation

root build.gradle

allprojects {
    repositories {
        maven { url = "https://dl.bintray.com/garage-dev/kmp" }
    }
}

project build.gradle

dependencies {
    commonMainApi("dev.garage.kmp:items:0.0.1-alpha")
}

settings.gradle

enableFeaturePreview("GRADLE_METADATA")

Podfile for iOS app, not sure but should work

pod 'MultiPlatformLibraryItems', :git => 'https://github.com/vchernyshov/kmp-items.git', :tag => 'release/0.0.1-alpha'

How to use

Common code:

To create new item need to implement Item interface.
Item interface contains base logic to work with DiffUtils at Android platform
and common field to reuse ItemDelegate and make relation Item -> ItemDelegate.

Typical Item implementation:

data class ExampleItem1(
    val icon: String,
    val text: String,
    override val uniqueProperty: Any = text
) : Item

Platform code:

Each platform need to define: ItemDelegate for Item, DelegatesFactory and create ItemsAdapter

Android platform:

  1. ItemDelegate:
class ExampleDelegate1 : GenericDelegate<ExampleItem1, ExampleDelegate1.ViewHolder>() {

    override fun bind(items: List<Item>, item: ExampleItem1, position: Int, holder: ViewHolder) {
        with(holder.binding) {
            Glide.with(this.root).load(item.icon).into(iconView)
            textView.text = item.text
        }
    }

    class ViewHolder(parent: ViewGroup, layoutId: Int) : BaseViewHolder(parent, layoutId) {
        val binding = ItemExample1Binding.bind(itemView)
    }
}
  1. DelegatesFactory:
object ExampleDelegatesFactory : DelegatesFactory {
    override fun create(item: Item): ItemDelegate? {
        return when (item) {
            is ExampleItem1 -> ExampleDelegate1()
            is ExampleItem2 -> ExampleDelegate2()
            is ExampleItem3 -> ExampleDelegate3()
            is ExampleItemWithPayloads -> ExampleDelegateWithPayloads()
            else -> null
        }
    }
}
  1. ItemsAdapter:
class SimpleExampleActivity : AppCompatActivity() {

    private lateinit var adapter: ItemsAdapter
    private lateinit var itemsView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        adapter = ItemsAdapter(ExampleDelegatesFactory)
        itemsView.adapter = adapter
    }
    
    fun onItemsReceived(items: List<Item>) {
        adapter.submitList(items)
    }
}

Handle item events:

Each ItemDelegate has ItemEventListener reference.
To notify listener about new event use sendEvent(ItemEvent) function.
For such cases as click events exists convenient functions setItemViewClickListener and extension
function View.clicks(RecyclerView.ViewHolder, (item: T, position: Int) -> ItemEvent):

class ExampleDelegate1 : GenericDelegate<ExampleItem1, ExampleDelegate1.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
        return ViewHolder(parent, R.layout.item_example_1).apply {
            setItemViewClickListener()
            binding.deleteView.clicks<ExampleItem1>(this) { item, _ -> DeleteItemEvent(item) }
        }
    }
}

Receive events:

class SimpleExampleActivity : AppCompatActivity() {

    private lateinit var adapter: ItemsAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        adapter = ItemsAdapter(ExampleDelegatesFactory, object : ItemEventListener {
            override fun onItemEvent(event: ItemEvent) {
                if (event is ItemClicked) {
                    Toast.makeText(
                        this@SimpleExampleActivity,
                        "Clicked ${event.item.uniqueProperty}",
                        Toast.LENGTH_SHORT
                    ).show()
                } else if (event is DeleteItemEvent) {
                    ItemsHolder.onDeleteEvent(event)
                }
            }
        })
    }
}

Samples available in app-android module: simple, with payloads

iOS platform:

iOS implementation was inspired by moko-units

  1. ItemDelegate:
class Example1TableViewDelegate: GenericDelegate<ExampleItem1, Example1TableViewCell> {
    
    override func xibName() -> String {
        return "Example1TableViewCell"
    }

    override func bind(items: [Item], item: ExampleItem1, position: Int64, cell: Example1TableViewCell) {
        cell.iconView.af.setImage(withURL: URL(string: item.icon)!)
        cell.textView.text = item.text
    }
}
  1. Cell
class Example1TableViewCell: UITableViewCell {
    
    @IBOutlet weak var iconView: UIImageView!
    @IBOutlet weak var textView: UILabel!
}
  1. DelegatesFactory:
class ExampleTableViewFactory: DelegatesFactory {
    func create(item: Item) -> ItemDelegate? {
        if item is ExampleItem1 {
            return Example1TableViewDelegate()
        }
        if item is ExampleItem2 {
            return Example2TableViewDelegate()
        }
        if item is ExampleItem3 {
            return Example3TableViewDelegate()
        }
        return nil
    }
}
  1. iOS version supports default and diffable version of ItemsAdapter:
class TableViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    private var adapter: ItemsAdapter!
          
    override func viewDidLoad() {
        super.viewDidLoad()
        // default
        adapter = ItemsAdapterKt.default(for: tableView, with: ExampleTableViewFactory())
        // diffable
        // adapter = ItemsAdapterKt.diffable(for: tableView, with: ExampleTableViewFactory())
    }

    func onItemsReceived(items: List<Item>) {
        adapter.items = items
    }
}

Handle item events:

Each ItemDelegate has ItemEventListener reference.

class Example1TableViewCell: UITableViewCell {
        
    var deleteCallback: (() -> ())!
    
    @IBAction func onDeleteButtonClicked(_ sender: UIButton) {
        deleteCallback()
    }
}

class Example1TableViewDelegate: GenericDelegate<ExampleItem1, Example1TableViewCell> {
    
    override func bind(items: [Item], item: ExampleItem1, position: Int64, cell: Example1TableViewCell) {
        cell.deleteCallback = {
            self.sendEvent(event: DeleteItemEvent(item: item))
        }
    }
}

Receive events:

class TableViewController: UIViewController, UITableViewDelegate {
    
    @IBOutlet weak var tableView: UITableView!
    
    private var adapter: ItemsAdapter!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        adapter.setItemEventListener(listener: { event in
            if let clickedEvent = event as? ItemClicked {
                self.showToast(message: "Clicked \(clickedEvent.item.uniqueProperty)")
            }
            if let deleteEvent = event as? DeleteItemEvent {
                self.holder.onDeleteEvent(event: deleteEvent)
            }
        })
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        adapter.didSelectItem(indexPath: indexPath)
    }
}

Samples available in app-ios module: TableView, CollectionView

About

KotlinMultiplatform items for RecyclerView, UICollectionView and UITableView

Resources

License

Stars

Watchers

Forks

Packages

No packages published