Skip to content

An Android lightweight synchronizer between (Views) RecyclerView & TabLayout, (Compose) LazColumn and TabRow.

License

Notifications You must be signed in to change notification settings

Ahmad-Hamwi/TabSync

Repository files navigation

TabSync

License API Medium Medium

Buy Me A Coffee

A lightweight synchronizer between Android's Tabs and Lists! Available for Views and Jetpack Compose!

Behaviour expected

As you scroll through items, the corresponding tabs will be selected automatically, and vice-versa; selecting tabs by hand will correspond to a scroll of the list to the corresponding item.

Mediator attached Mediator attached with smooth scroll

Available on the View System & Jetpack Compose

  • On Android's View, synchronization is made between TabLayout and RecyclerView
  • Android's Jetpack Compose, the synchronization is made between any LazeListState-based Composable (e.g. LazyColumn) and an index-based Composable (e.g. ScrollableTabRow).

Articles

  • RecyclerView and TabLayout: Here's a Medium Article demonstrating the example in this repo step-by-step.
  • Jetpack compose: Here's the Jetpack Compose version Medium Article demonstrating the example in this repo step-by-step, and explaining how it's working.

Setup for the View system

Get the latest version via Maven Central:

Maven Central

Add Maven Central repository to your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        mavenCentral()
    }
}

Then add the dependency

dependencies {
    implementation 'io.github.ahmad-hamwi:tabsync:1.0.1'
}

Usage

Here is a non-comprehensive guide to TabSync:

Create a TabbedListMediator, and pass a RecyclerView, a TabLayout, and a list of RecyclerView's items indices that you wish to sync the tabs with.

val mediator = TabbedListMediator(
    recyclerView,
    tabLayout,
    categories.indices.toList()
)

Make sure that RecyclerView and the TabLayout are instantiated with their data (adapter with its data for RecyclerView, and tabs for the TabLayout) and call the attach method. Note that the tabs' count must not be less than the number of the passed indices.

method.attach()

To make the RecycerView smooth scrolls when pressing on a tab, you can pass a smooth scroll flag to the mediator:

val mediator = TabbedListMediator(
    recyclerView,
    tabLayout,
    categories.indices.toList(),
    true
)

To stop syncing between the RecyclerView and the TabLayout, call the detach method:

mediator.detach()

In case of refreshing the mediator (like data set has been changed), call the reAttach method:

mediator.reAttach()

To get the smooth scroll flag, call the getter isSmoothScroll()

val isSmoothScrolling = mediator.isSmoothScroll()

NOTE: You don't have to pass the full indices of the list provided as shown in the previous example. You may want to provide a couple of indices from your recyclerview's items to sync the tabs with:

 val mediator = TabbedListMediator(
   recyclerView,
   tabLayout,
   // Syncing the first tab with the item of index 0, the second one with the item of
   // index 2, and the third with the item of index 4
   mutableListOf(0, 2, 4) 
 )

Setup for Jetpack Compose

Get the latest version via Maven Central:

Maven Central

Add Maven Central repository to your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        mavenCentral()
    }
}

Then add the dependency

dependencies {
    implementation 'io.github.ahmad-hamwi:tabsync-compose:1.0.1'
}

Usage

Here is a non-comprehensive guide to TabSync Compose:

Call lazyListTabSync composable, and pass the indices of your list that you wish to sync the tabs with, and assign the result to a destructuring declaration like so:

@Composable
fun MyComposable(items: List<Item>) {

    val (selectedTabIndex, setSelectedTabIndex, syncedListState) = lazyListTabSync(items.indices.toList())

}

Make sure that whenever you call lazyListTabSync, the indices and the list's data are ready (above we're passing the state as a parameter).

Note that the tabs' count must not be less than the number of the passed indices.

  • The selectedTabIndex is the state of a ScrollableTabRow that we're going to sync with.
  • The setSelectedTabIndex is a function that we should call whenever the state of the ScrollableTabRow changes directly, for example by pressing the tab.
    ScrollableTabRow(selectedTabIndex) {
        categories.forEachIndexed { index, category ->
            Tab(
                selected = index == selectedTabIndex,
                onClick = { setSelectedTabIndex(index) },
            )
        }
    }
  • The listState is a LazyListState which should be attached to a scrollable that accepts it, for example a LazyColumn.
    LazyColumn(state = syncedListState) {
        ...
    }

Combined:

(Full example can be found under compose-app)

@Composable
fun MyComposable(categories: List<Category>) {
    val (selectedTabIndex, setSelectedTabIndex, syncedListState) = lazyListTabSync(categories.indices.toList())

    Column {
        ScrollableTabRow(selectedTabIndex) {
            categories.forEachIndexed { index, category ->
                Tab(
                    selected = index == selectedTabIndex,
                    onClick = { setSelectedTabIndex(index) },
                    ...
                )
            }
        }

        LazyColumn(state = syncedListState) {
            ...
        }
    }
}

NOTE: You don't have to pass the full indices of the list provided as shown in the previous example. You may want to provide a couple of indices from your list's items to sync the tabs with:

   // Syncing the first tab with the item of index 0, the second one with the item of
   // index 2, and the third with the item of index 4
   val (selectedTabIndex, setSelectedTabIndex, syncedListState) = lazyListTabSync(mutableListOf(0, 2, 4))

Additional arguments

  • By default, the list smooth scrolls when the selected tab changes, if you would like to stop the smooth scrolling, you can pass an optional smoothScroll flag:
  • tabsCount can be used to check for the viability of the synchronization with the tabs, the optimal case is that the number of tabs that we're syncing with is the same as the number of indices provided. (You cannot have a number of tabs lower than the number of indices provided, use tabsCount to make sure you're not doing that).
  • lazyListState can be used if you would like to provide your own state, the one coming from the destructuring declaration is going to be the same.
@Composable
fun MyComposable(items: List<Item>) {

    val (selectedTabIndex, setSelectedTabIndex, listState) = tabSyncMediator(
        mutableListOf(0, 2, 4), //Mandatory. The indices of lazy list items to sync the tabs with
        tabsCount = 3, //Optional. To check for viability of the synchronization with the tabs. Optimal when equals the count of syncedIndices.
        lazyListState = rememberLazyListState(), //Optional. To provide your own LazyListState. Defaults to rememberLazyListState().
        smoothScroll = true, // Optional. To make the auto scroll smooth or not when clicking tabs. Defaults to true
    )

}

Contributing

This library is made to help other developers out in their app developments, feel free to contribute by suggesting ideas and creating issues and PRs that would make this repository more helpful.

License

Copyright (C) 2023 Ahmad Hamwi

Licensed under the Apache License, Version 2.0