post-photo

WHAT IT IS

RecyclerViews and their Adapters are well known ways of displaying lists of data, but they are not optimal, because they load entire lists into themselves, and upon change, they simply reload the full list (using notifyDataSetChanged).

Let’s imagine the following scenario:

I have a list, containing all the currently living animal species, each has a name and a description. After a quick research, 8 hours of coding and 4 liters of coffee, I manage to create my list that contains 7.7 million Animals. Every time I update the name of a single animal, I must load 7.7M items into the adapter. Furthermore, if I move this database to cloud storage and add pictures to it, I’ll have to download around 7.7GB of data.

A few years ago, the DiffUtil class tried to solve this by loading only the modified items into the adapter, thus making the process faster, but we still had to have the full list to compare it to the modified items. With the new Jetpack library, we can load the subset that is currently visible to the user, and no more.

HOW IT WORKS

The PagedListAdapter comes with different kinds of DataSources (PositionalDataSource, ItemKeyedDataSource, PageKeyedDataSource) for different uses. In this post, we will focus on the ItemKeyedDataSource. Using this we can retrieve “pages”, subsets of a list, starting or ending at a given key.

Classes and responsibilities:

After the Application has started, the LivePagedListBuilder.build() function will be called and a LiveData will be created. When the LiveData is observed the Factory will create a new DataSource and load the requested subset of data into a PagedList. Then, trough the ViewModel this Paged Data will be added (submitted) to the PagedListAdapter. Animation will occur and the list will be visible in the RecyclerView.

text

Scrolling the PagedList will trigger the DataSource, thus additional pages will load. (The PositionalDataSource can show placeholders while the data is loading.)

text

If the database changes, we invalidate the current DataSource and create a new DataSource which loads the requested data into the proper position of a new PagedList, and then passes it to the adapter.

text

Note that the PagedList and the DataSource travel together.

Project Classes

So, how did I use all this to implement my animal dictionary?

Below you can see the classes I’ve created with a brief behaviour description.

All classes function according to the aforementioned.

Animal

Data class, contains two strings: name, description.

data class Animal(
        val name: String? = null,
        val description: String? = null
): FirebaseObject()

AnimalDataSource

Subclass of the ItemKeyedDataSource.

The loadInitial(…) function triggers a callback (LoadInitialCallback) with the first few list items, depending on the requestedLoadSize.

The loadAfter(…) and loadBefore(…) loads a give number of items starting/ending at a key.

The getKey(…) returns the key of an item, so we can find it in the list.

Also, this is a DataSource, so this must be invalidated if the database changes. In the init method we subscribe to the dataChanges, and if it happens we invalidate.

class AnimalDataSource : ItemKeyedDataSource<String, Animal>() {
     
    init {
        FirebaseManager.getAnimalChangeSubject()?.observeOn(Schedulers.io())?.subscribeOn(Schedulers.computation())?.subscribe {
            invalidate()
        }
    }
     
    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<Animal>) {
        FirebaseManager.getAnimals(params.requestedLoadSize).subscribe({
            callback.onResult(it)
        }, {})
    }
 
    override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Animal>) {
        FirebaseManager.getAnimalsAfter(params.key, params.requestedLoadSize).subscribe({
            callback.onResult(it)
        }, {})
    }
 
    override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Animal>) {
        FirebaseManager.getAnimalsBefore(params.key, params.requestedLoadSize).subscribe({
            callback.onResult(it)
        }, {})
    }
 
    override fun getKey(item: Animal): String {
        return item.objectKey ?: ""
    }
}

AnimalDataFactory

A simple DataSource.Factory. In its create() method it returns a new AnimalDataSource.

class AnimalDataFactory : DataSource.Factory<String, Animal>() {
 
    private var datasourceLiveData = MutableLiveData<AnimalDataSource>()
 
    override fun create(): AnimalDataSource {
        val dataSource = AnimalDataSource()
        datasourceLiveData.postValue(dataSource)
        return dataSource
    }
}

AnimalDataProvider

With its getAnimal() method, we can request a new LiveData<PagedList>.

Here we use the LivePagedListBuilder to get the LiveData. We need a configuration and a factory. For the factory, AnimalDataFactory is used. We create a Config isntance. In this we can set the parameters such as: InitialLoadSize, PageSize.

class AnimalDataProvider {
 
    var animalDataFactory: AnimalDataFactory = AnimalDataFactory()
    private val PAGE_SIZE = 4
 
    fun getAnimals(): LiveData<PagedList<Animal>>? {
        val config = PagedList.Config.Builder()
                .setInitialLoadSizeHint(PAGE_SIZE)
                .setPageSize(PAGE_SIZE)
                .build()
 
        return LivePagedListBuilder(animalDataFactory, config)
                .setInitialLoadKey("")
                .build()
    }
}

AnimalViewModel

An interface that connects the Provider and the Adapter.

class AnimalViewModel : ViewModel() {
    private val provider: AnimalDataProvider? = AnimalDataProvider()
 
    fun getAnimals(): LiveData<PagedList<Animal>>? {
        return provider?.getAnimals()
    }
}

AnimalAdapter

Subclass of PagedListAdapter.

It works like a simple Adapter, but the speciality here is the DiffUtil object. The DiffUtil.ItemCallback checks which items have changed in the newly submitted list. One checks if the item itself has changed (added, removed) and the other checks if the contents have changed. We use this instead of notifyDataSetChagned(). (Make sure you compare the right things!)

class AnimalAdapter constructor(context: Context) : PagedListAdapter<Animal, AnimalAdapter.AnimalViewHolder>(
         
        object : DiffUtil.ItemCallback<Animal>() {
            override fun areItemsTheSame(oldItem: Animal?, newItem: Animal?): Boolean = oldItem == newItem
 
            override fun areContentsTheSame(oldItem: Animal?, newItem: Animal?): Boolean = oldItem?.description == newItem?.description
        }) {
 
  ...
}

RecyclerView & Main

The RecyclerView contains the AnimalAdapter. Also in the Main Class we observe the DataProviders getAnimal() method (returns LiveData). If the LiveData changes (because of the database) the Adapter’s submitList(…) is called with the new LiveData and the list will be “refreshed”.

class MainActivity : AppCompatActivity() {
 
    ...
 
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        animalRecyclerView.adapter = animalAdapter
        animalRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
        animalViewModel.getAnimals()?.observe(this, Observer(animalAdapter::submitList))
    }
}

With this we have finished the “Android part” of the project.

Firebase

We have to make sure that we can identify each item using some kind of key, but apart from that we can use any kind of structure.

Keep in mind that Firebase uses an ascending order automatically. We have to keep the order of the project to work. I used numbers, but pushing (push()) animals works too, because it generates a key.

With these in mind, I created the following database:

text

Firebase Manager

Last but not least, we have to create the Firebase Manager.

The getAnimals() method returns the first “count” number of items from the database.

The loadAfter(…) and loadBefore(…) loads a given number of items starting/ending at a key.

The getAnimalChanges subject is used in the AnimalDataSource. This subject notifies the DataSource if the database changes and invalidation is required.

In the init method we create a listener so if the content changes, we can fetch immediately.

object FirebaseManager {
    private val ANIMAL_ROUTE = "animals"
 
    private val animalAdapterInvalidation = PublishSubject.create<Any>()
    val database = FirebaseDatabase.getInstance()
    val databaseRef = database.reference
 
    init {
        databaseRef.child(ANIMAL_ROUTE).addChildEventListener(object : ChildEventListener {
            override fun onCancelled(p0: DatabaseError) {}
 
            override fun onChildMoved(p0: DataSnapshot, p1: String?) {
               animalAdapterInvalidation.onNext(true)
           }
 
           override fun onChildChanged(p0: DataSnapshot, p1: String?) {
               animalAdapterInvalidation.onNext(true)
           }
 
          override fun onChildAdded(p0: DataSnapshot, p1: String?) {
              animalAdapterInvalidation.onNext(true)
           }
     
          override fun onChildRemoved(p0: DataSnapshot) {
             animalAdapterInvalidation.onNext(true)
          }
        })
    }
 
    fun getAnimalChangeSubject(): PublishSubject<Any>? {...}
    fun getAnimals(count: Int): Single<List<Animal>> {...}
    fun getAnimalsAfter(key: String, count: Int): Single<List<Animal>> {...}
    fun getAnimalsBefore(key: String, count: Int): Single<List<Animal>> {...}
}

DONE

Let’s see the final application!

text

It’s not big, it’s not complicated, but the potential it has can be the difference between a good or a bad application… and a crowded zoo.

The complete sample application can be found here, on the #TeamWanari GitHub. Thanks to ashish for the reminder in the comment section! 🙂

If you want to see more posts like this, follow TeamWanari on Facebook or LinkedIn!

member photo

Long is a junior Android developer who loves a challenge. He is eager to learn and also loves sharing knowledge. Read his first blog post on #WanariLeaks!

Latest post by Ádám Hosszú

Google Analytics for Firebase