Skip to main content

Searchable Fragments with the Paging Library

This post is inspired by @EpicPandaForce answer in StackOverflow. I faced the same problem which I didn't know how to solve: How to perform search when you are using a Paging Library (or how the hell to refresh after I reperform Rooms query)?

Let's suppose we have this scenario: I have a list of data, which are shown in the Fragment by LiveData observation, which are retrieved by the ViewModel through LiveDataPagedListBuilder(). I'm hoping you know the basics of the Paging Library already.

The data source:

I'm retrieving the data using a local database and Paging Library's Datasource.Factory<Key, Value>:

@Dao
interface MyDao{
    @RawQuery(observedEntities = [MyEntityRepresentation::class])
    fun selectAllMeetingCondition(query: SupportSQLiteQuery): DataSource.Factory<Int , MyEntityRepresentation>:
}

Room queries at Runtime.

My use case requires to generate a dynamic query each time the user performs a search. In other words, the query depends on the users "advance" search. Since we know that Room generates queries at compile time, we would need something different. In this case we annotate our method with @RawQuery and must place observedEntities as dependency of that annotation. The methods parameter is jut a class where you can place your query string later.

Note: The DatasourceFactory cannot be marked with suspend, the compilation would fail if you do so.

So, the query should look something like this:

fun instantiateSearch(
        field1: String,
        field2: String,
        field3: String
    ) {
        viewModelScope.launch(appCoroutineDispatchers.ioDispatchers) {
            this@AdvancedSearchViewModel.field1 = field1.toIntOrNull()
            this@AdvancedSearchViewModel.field2 = field2.toIntOrNull()
            this@AdvancedSearchViewModel.field3 = if (field3.isEmpty()) null else field3
            var selectionQuery = "SELECT * FROM table_name"
            this@AdvancedSearchViewModel.field1?.let {
                selectionQuery += "AND field_3_name LIKE '%$it%' "
            }
            this@AdvancedSearchViewModel.field2?.let {
                selectionQuery += "AND field_2_name = $it "
            }
            this@AdvancedSearchViewModel.field3?.let {
                selectionQuery += "AND field_1_name = $it "
            }
            val finalSelectionQuery = selectionQuery.replaceFirst("AND", "WHERE")
            _queryEvent.postValue(Event(finalSelectionQuery))
        }
    }

Note, the search query is performed in a BottomSheetFragment so it's easy to send the query as an Event to the PagingFragment.

I'm also not dealing with how to send an event through a SharedViewModel, but you can check this link, or this blog post of mine. But to get the picture, pretend my query is the ball below and players are Fragments or a ViewModel:

After that all we have to do is pass the query. And here is where our problem with Paging Library starts. So let's say that we have a configuration like this:

class HomeViewModel @Inject constructor( //in real project is using @AssistedInject, no matter for this case
    private val myDao: MyDao,
) : ViewModel() {
    var data: LiveData<PagedList<MyEntityRepresentation>>
    init {
        val listConfig = PagedList.Config.Builder()
            .setPageSize(20)
            .setEnablePlaceholders(false)
            .build()
        val dataSourceFactory = myDao.selectAll()
        data = LivePagedListBuilder(dataSourceFactory, listConfig).build()
    }

    fun performSearch(query: String) {
        val newData = myDao.selectAllMettingContition(query)
        /* Won't work . LiveData already taken, unless you change the value*/
        data = LivePagedListBuilder(newData, listConfig).build() 
    }
}

This is not such a sophisticated solution because your user would end up seeing no changes in the Fragment

And now let's show the right thing to do it. First, the source should be only the one I describe in the Dao. Now let's refactor:

//inside ViewModel
var data: LiveData<PagedList<MyEntityRepresentation>>
private val listConfig = PagedList.Config.Builder()
        .setPageSize(20)
        .setEnablePlaceholders(false)
        .build()
private var finalSelectionQuery = "" //we need this

And now we can do something like this:

 init {
        data = LivePagedListBuilder(
            myDao.selectAllMeetingCondition(
                SimpleSQLiteQuery("SELECT * FROM table_name $finalSelectionQuery ORDER BY name")
            ),
            listConfig
        ).build()
    }

Now, let's setup our PagedFragment so it can be updated depending on the query:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initialiseComponents(view)
        afterInitialize()
        observeDataChanges()//note this
    }

Inside the observeDataChanges() is just our data from ViewModel LiveData observation:

homeViewModel.cards.observe(this, Observer {
            if (it.isEmpty()) {
               //show empty result view
            } else {
                cardAdapter.submitList(it)
            }
        })

Why have we refactored this method? Because once the new String has been formed, the PagedFragment will get notified, will remove it's subscription, perform the query and re-evaluate data LiveData once again:

sharedViewModel.searchObjectLiveData.observe(viewLifecycleOwner, Observer {
            it.getContentIfNotHandled()?.let { searchQuery ->
                /*I admit that I have to find a better name but it's doing two things, removing the LiveDataObserver
                and generating my new query. Best thing is to refactor to do one thing . Project still on going.*/
                homeViewModel.resetAndPerformSearch(searchQuery, this)
                observeDataChanges()//here we are again
            }
        })

Our last step is to write the resetAndPerformSearch() method:

fun resetAndPerformSearch(query: String, lifecycleOwner: LifecycleOwner) {
        data.removeObservers(lifecycleOwner) //the fragment is not observing anymore
        data = LivePagedListBuilder(
            myDao.selectAllMeetingCondition(
                SimpleSQLiteQuery(query)
            ),
            listConfig
        ).build()
    }

Now your search with Paging library will work just fine. Notice that after resetAndPerformSearch() we are calling observeDataChanges so that our Fragment will be ready to react after the new query has been performed.

Conclusion.

Hopefully, this solution would help you to perform an painless search when having Paging library around. Otherwise you would end up with a bunch of flags and a bunch of other configurations instead.

Full repository can be found here. Please excuse typos, poor design or other mistakes because the project is still on going and has a lot of redundant code and files.

Stavro Xhardha

Popular posts from this blog

What I learned from Kotlin Flow API

I used to check the docs and just read a lot about flows but didn't implement anything until yesterday. However, the API tasted really cool (even though some operations are still in Experimental state).Prerequisites: If you don't know RxJava it's fine. But a RxJava recognizer would read this faster.Cold vs Hot streamsWell, I really struggled with this concept because it is a little bit tricky. The main difference between cold and hot happened to be pretty simple: Hot streams produce when you don't care while in cold streams, if you don't collect() (or RxJava-s equivalent subscribe()) the stream won't be activated at all. So, Flows are what we call cold streams. Removing the subscriber will not produce data at all, making the Flows one of the most sophisticated asynchronous stream API ever (in the JVM world). I tried to make a illustration of hot and cold streams: Since I mentioned the word asynchronous this implies that they do support coroutines also. Flows vs…

Modularizing your Android app, breaking the monolith (Part 1)

Inspired by a Martin Fowlers post about Micro Frontends, I decided to break my monolithic app into a modular app. I tried to read a little more about breaking monolithic apps in Android, and as far as I got, I felt confident to share my experience with you. This will be some series of blog posts where we actually try to break a simple app into a modularized Android app.

Note: You should know that I am no expert in this, so if there are false statements or mistakes please feel free to criticize, for the sake of a better development. 

What do you benefit from this approach:
Well, people are moving pretty fast nowadays and delivery is required faster and faster. So, in order to achieve this, modularising Android apps is really necessary.You can share features across different apps. Independent teams and less problems per each.Conditional features update.Quicker debugging and fixing.A feature delay doesn't delay the whole app. As per writing tests, there is not too much difference about…

From Gson to Moshi, what I learned

There is no doubt that people are getting away from GSON and I agree with those reasons too. The only advantage GSON has over other parsing libraries is that it takes a really short amount of time to set up. Furthermore, the most important thing is that Moshi is embracing Kotlin support.

First let's implement the dependency:
implementation("com.squareup.moshi:moshi:1.8.0") It's not a struggle to migrate to Moshi. It's really Gson look-a-like. The only thing to do is annotate the object with @field:Json instead of @SerializedName (which is Gsons way for JS representation):

data class User( //GSON way @SerializedName("name") val name: String, @SerializedName("user_name") val userName: String, @SerializedName("last_name") val lastName: String, @SerializedName("email") val email: String ) data class User( //Moshi way @field:Json(name = "name") val name: String, @field:Json(name = "user_name…