lisawray / groupie

Groupie helps you display and manage complex RecyclerView layouts.
MIT License
3.66k stars 292 forks source link

Kotlin lambdas and DSL support? #171

Open wbinarytree opened 6 years ago

wbinarytree commented 6 years ago

Hi, Thanks for this great library. When using with kotlin, I feel it's a bit boilerplate can be remove by using kotlin DSL and Lambda feature. For example a GenericItem:

typealias BindFunction = ViewHolder.(position: Int) -> Unit

class GenericItem(@LayoutRes val layoutId: Int, val binder: BindFunction) : Item() {
    override fun bind(viewHolder: ViewHolder, position: Int) {
        binder(viewHolder, position)
    }

    override fun getLayout(): Int {
        return layoutId
    }
}

// when use it 
val item  = GenericItem(R.layout.item_layout){ position ->
    // the bind code 
}

By that way I can just use a GenericItem without creating a new subclass of Item.

Even more with kotlin DSL style support. We can easily build a DSL style List for RecyclerView For example :

    val adapterGroupAdapter<ViewHolder> = goupieList {
        item {
            layoutId = R.layout.item_1
            binder = {
                //bind code
            }
        }
        item {
            layoutId = R.layout.item_2
            binder = {
                //bind code
            }
        }
        item {
            layoutId = R.layout.item_3
            binder = {
                //bind code
            }
        }
    }
lisawray commented 6 years ago

I'm open to the idea of better Kotlin support, was thinking about it today ... but I'm not convinced making Items on the fly is a major use case for Groupie. In fact, I wrote it to encourage code reuse.

Do you normally make one-off Items like this? I never have ...

wbinarytree commented 6 years ago

I totally understand that using a subclass of Item can reduce the boilerplate code to spamming Layout ID and other stuff every where. But for each item/type I need to create a new Class feels just another kind of boilerplate code for me.

There is a middle point of those 2 things. That is introducing annotation processor to generate those code and even for DSL style support. (That what exactly Epoxy did.) But it's a huge change of API which I don't really think you or even other user can accept including me.

Sorry For My Bad English. Hope you understand my words :smiley:

lisawray commented 6 years ago

Your English is great! There are probably lots of ways I could make this less verbose in Kotlin. In particular, passing the layoutId in the Item constructor is a great idea.

I haven't spent much time looking at Kotlin support past an update to support the basics last year, but now I'm writing it regularly it's probably time to have a closer look!

lisawray commented 6 years ago

I thought about this over the weekend and I changed my mind. I think you're totally right. And it would be useful for creating reusable Items too. :)

wbinarytree commented 6 years ago

Great! Looking forward to those enhancements.

TylerMcCraw commented 6 years ago

I like this idea as well. This would be helpful for me if I could create the GroupAdapter object and each of the sections I need ahead of time.

Are you actively or planning on working on this one, Lisa? I wouldn't mind giving this a shot if I have time this week, but only if you don't mind.

lisawray commented 6 years ago

I'd love help on anything here! PRs welcomed :)

khatv911 commented 6 years ago

@wbinarytree thanks a lot for bringing me to the world of DSL. Didn't have any clue this existed. @TylerMcCraw a small working sample I created here.

TylerMcCraw commented 6 years ago

@khatv911 If you already have a working sample, go for it. I'm in Portugal this week so I'm not going to touch a computer until I get back 😁

Jomik commented 6 years ago

I don't feel like DSL works very well for this use case. It seems to be mostly useful for optional configuration, as you can not ensure that a function is given. Thus we can't ensure that an item gets a layout and a binder. Fell over this as I am writing my own library for handling RecyclerView.

Constructing a list of headers that expand to show children looks like this, adapted from @khatv911's code.

val expandables = groupData.rooms.asSequence().map { roomData ->
  ExpandableGroup(
    header = HeaderItem(
      title = roomData.name,
      subtitle = "%d".format(roomData.jobs.count())),
  content = CompoundGroup(roomData.jobs.asSequence().map { jobData ->
    GenericItem(
      layout = R.layout.view_item_text_1,
      binder = { viewHolder, payloads ->
        with(viewHolder.itemView) {
          tv_item_name.text = jobData.name
          editText.text = jobData.id
        }
      })
    })
  ).apply { expand() }
}
with(recyclerView) {
      setHasFixedSize(true)
      addItemDecoration(DividerItemDecoration(context, VERTICAL))
      layoutManager = LinearLayoutManager(context)
      adapter = GroupAdapter(CompoundGroup(expandables))
}

I am unsure how we would, for my case, or Groupie's, be able to use DSL in a secure way. As we are dependent on some arguments being passed. And item needs a layout and a binder, it makes sense to require this on a type level.

Is there a way to ensure that a function is called when using DSL?

Jomik commented 6 years ago

I ended up quite liking this idea. What I have decided to do, in case groupie wants to implement similar, is to show a default item in case a setter method isn't called. As with the header of an expandable group.

I use group to open up a basic group builder which returns a single group.

I realize that I often display lists of data, so I created a method to allow us to map over a list, with the GroupBuilder. fun <T> map(data: List<T>, init: GroupBuilder.(T) -> Unit): CompoundGroup

One of my issues has been calling methods on your parent, as with expanding a group upon touch of the header. With the below implementation it becomes simple to get the parent instance: this@expandable.instance I do this by having a public field which is set the moment the build method is called on the builder. override fun build() = CompoundGroup(groups).also { _instance = it }

val foo = group {
  map((0 until 10).toList()) { parentIndex ->
    expandable {
      header(R.layout.header) {
        bind { viewHolder, _ ->
          viewHolder.itemView.findViewById<TextView>(R.id.textView).text = "Parent #$parentIndex"
        }
        onClick {
          this@expandable.instance.toggle()
        }
      }

      content {
        map((0 until 3).toList()) {
          item(R.layout.item) {
            bind { viewHolder, _ ->
              viewHolder.itemView.findViewById<TextView>(R.id.textView).text =
                  "Child #$it of parent #$parentIndex"
            }
          }
        }
      }
    }
  }
}