Last active
January 6, 2022 05:33
-
-
Save bmc08gt/f9f9f7d8fbe5398abbb57350ba80d5ef to your computer and use it in GitHub Desktop.
Jetpack Compose Composable wrapping an Android SwipeRefresh & RecyclerView using Composable ViewHolders
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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), | |
| ) | |
| }) | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| ) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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) } | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ViewholderUtils is adapted from https://android-review.googlesource.com/c/platform/frameworks/support/+/1774787/14