Skip to content

Instantly share code, notes, and snippets.

@bmc08gt
Last active January 6, 2022 05:33
Show Gist options
  • Select an option

  • Save bmc08gt/f9f9f7d8fbe5398abbb57350ba80d5ef to your computer and use it in GitHub Desktop.

Select an option

Save bmc08gt/f9f9f7d8fbe5398abbb57350ba80d5ef to your computer and use it in GitHub Desktop.
Jetpack Compose Composable wrapping an Android SwipeRefresh & RecyclerView using Composable ViewHolders
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun AndroidIgGrid(
viewState: InstagramGridViewState,
isRefreshing: Boolean,
items: PagingData<GridMediaItem>,
selectedItems: List<GridMediaItem>,
onRefresh: () -> Unit,
onItemSelected: (GridMediaItem) -> Unit,
onItemUnSelected: (GridMediaItem) -> Unit,
onItemClicked: (GridMediaItem) -> Unit,
onItemMoved: (from: Int, to: Int) -> Unit,
) {
val scope = rememberCoroutineScope()
val adapter = remember {
GridAdapter().apply {
this.onItemSelected = { entity -> onItemSelected(entity) }
this.onItemUnSelected = { entity -> onItemUnSelected(entity) }
this.onItemClicked = { entity -> onItemClicked(entity) }
this.canBeDisplaced = { entity -> true }
this.onItemMove = { from, to -> onItemMoved(from, to) }
}
}
val touchCallback = remember {
DragAndDropGridTouchCallback().apply {
dispatchTo = adapter
}
}
val touchHelper = remember {
ItemTouchHelper(touchCallback)
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
SwipeRefreshLayout(context).apply {
setOnRefreshListener { onRefresh() }
this.isRefreshing = isRefreshing
addView(RecyclerView(context).apply {
registerAllComposeViews()
touchHelper.attachToRecyclerView(this)
layoutManager = GridLayoutManager(context, 3)
this.adapter = adapter.apply {
itemTouchHelper = touchHelper
}
})
}
}
) {
scope.launch {
adapter.submitData(items.map { item ->
GridMediaItemEntity(
media = item,
isRefreshing = isRefreshing,
isSelecting = viewState.editEnabled,
isSelectable = viewState.editEnabled && !item.isPosted,
isSelected = selectedItems.contains(item),
)
})
}
}
}
internal class GridAdapter :
PagingDataAdapter<GridMediaItemEntity, GridAdapter.ViewHolder>(GridMediaItemDiffer()),
OnGridItemMove {
var media: GridMediaItemEntity by mutableStateOf(GridMediaItemEntity.Uninitialized)
var itemTouchHelper: ItemTouchHelper? = null
var isRefreshing: Boolean = false
var isSelecting: Boolean = false
var isSelectable: (GridMediaItem) -> Boolean = { false }
var isItemSelected: (GridMediaItem) -> Boolean = { false }
var onItemSelected: (GridMediaItem) -> Unit = { }
var onItemUnSelected: (GridMediaItem) -> Unit = { }
var onItemClicked: (GridMediaItem) -> Unit = { }
var onItemMove: (from: Int, to: Int) -> Unit = { _, _ -> }
var canBeDisplaced: (GridMediaItem) -> Boolean = { false }
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemRow: GridItemCell = itemView.findViewById(R.id.item)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(inflater.inflate(R.layout.grid_compose_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemRow.apply {
val entity = getItem(position) ?: GridMediaItemEntity.Uninitialized
media = entity
isRefreshing = [email protected]
isSelecting = [email protected]
isSelectable = [email protected](entity.media)
isItemSelected = [email protected](entity.media)
onItemSelected = [email protected]
onItemUnSelected = [email protected]
onItemClicked = [email protected]
onStartDrag = { itemTouchHelper?.startDrag(holder) }
}
}
override fun canBeDisplaced(position: Int): Boolean {
val item = getItem(position) ?: return false
return canBeDisplaced(item.media)
}
override fun onMoved(from: Int, to: Int) {
notifyItemMoved(from, to)
}
override fun onDropped(from: Int, to: Int) {
onItemMove.invoke(from, to)
}
}
class GridItemCell @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
var media: GridMediaItemEntity by mutableStateOf(GridMediaItemEntity.Uninitialized)
var isRefreshing by mutableStateOf(false)
var isSelecting by mutableStateOf(false)
var isSelectable by mutableStateOf(false)
var isItemSelected by mutableStateOf(false)
var onItemSelected: (GridMediaItem) -> Unit = { }
var onItemUnSelected: (GridMediaItem) -> Unit = { }
var onItemClicked: (GridMediaItem) -> Unit = { }
var onStartDrag: () -> Unit = { }
@Composable
override fun Content() {
GridItemCell(
entity = media,
onStartDrag = onStartDrag,
onItemSelected = onItemSelected,
onItemUnSelected = onItemUnSelected,
onItemClicked = onItemClicked,
)
}
}
/**
* A ViewCompositionStrategy that will dispose the composition when [viewHolder] is unbound or
* when the `RecyclerView` is detached from the window. For details on these lifecycle events, see
* [RecyclerView.RecyclerListener].
*/
class ViewHolderCompositionStrategy(
private val recyclerView: RecyclerView,
private val viewHolder: RecyclerView.ViewHolder
) :
ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = RecyclerView.RecyclerListener { view.disposeComposition() }
recyclerView.addRecyclerListener(listener)
return { recyclerView.removeRecyclerListener(listener) }
}
}
/**
* Register [view] to have its composition managed by this ViewHolder's lifecycle.
*
* This sets a [ViewHolderCompositionStrategy] as this view's composition strategy
*/
fun registerComposeView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
view: AbstractComposeView
) {
view.setViewCompositionStrategy(ViewHolderCompositionStrategy(recyclerView, viewHolder))
}
/**
* Traverse all [View]s within [RecyclerView.ViewHolder.itemView] and call [registerComposeView]
* on any [AbstractComposeView]s found.
*/
fun RecyclerView.registerAllComposeViews() {
val lm = layoutManager ?: return
for (i in 0..lm.childCount) {
val view = lm.getChildAt(i)
if (view != null) {
val vh = getChildViewHolder(view)
(view as? AbstractComposeView)?.let { registerComposeView(this, vh, it) }
}
}
}
@bmc08gt
Copy link
Author

bmc08gt commented Aug 19, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment