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.
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'
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
Each platform need to define: ItemDelegate
for Item
, DelegatesFactory
and create ItemsAdapter
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)
}
}
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
}
}
}
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)
}
}
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 implementation was inspired by moko-units
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
}
}
Cell
class Example1TableViewCell: UITableViewCell {
@IBOutlet weak var iconView: UIImageView!
@IBOutlet weak var textView: UILabel!
}
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
}
}
- iOS version supports
default
anddiffable
version ofItemsAdapter
:
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
}
}
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