in Building NiceFeed, Programming

Building an RSS Reader for Android from nothing #2: Searching for feeds using Feedly’s Search API

Previously, I wrote about the component that handles retrieving and parsing raw RSS feeds from the web. This component represents one of NiceFeed’s main data sources, giving us an actual RSS feed, in the form of interrelated model objects that we’ve defined (Feed, Entry, FeedWithEntries), which the app can then display. All we need to do is give it the URL of any particular RSS feed, and it does all the work.

In real life, nobody’s going to want to keep entering URLs manually; sometimes, a user won’t even know what they are. A respectable RSS reader would have to have some way for the user to search for new feeds. Therefore, we need another data source—this data source would accept a keyword and return to the user a list of RSS feeds from the internet based on that keyword. The user would then able to choose any of these RSS feeds to subscribe to.

Feedly’s public Search API exists for this very purpose. It accepts a string search query as input, with some optional parameters such as the number of desired search results and a specific locale; in return, we get an organized collection of data in the form of JSON. This data, basically a collection of RSS feeds, is organized into fields such as “title,” “website,” “feedId,” “description,” and so on.

To get this data into the app, we pick the fields we want and create a new data class containing relevant properties corresponding to those fields. I named this data class SearchResultItem, which represents one item (containing data from one RSS feed) in a list of search results.

data class SearchResultItem(
    val title: String?,
    @SerializedName("feedId") val id: String?,
    val website: String?,
    val description: String?,
    val updated: String?,
    @SerializedName("visualUrl") val imageUrl: String?
): Serializable

To actually accomplish the task of communicating with Feedly API to obtain the needed data, some housekeeping is needed. I used the Retrofit library to do all the heavy lifting with regard to networking and parsing JSON from Feedly, making it very easy to implement the whole thing. All that’s needed is a simple interface which sends the request to the API, with an annotation that defines the request—in this case @GET, which represents the method for fetching data. The method returns a Call, which represents one HTTP request, with the parameter SearchResult—a class defined solely to contain one or more SearchResultItems—representing the type of a successful response.

interface FeedlyApi {
    @GET
    fun fetchSearchResult(@Url url: String): Call<SearchResult>
}
class SearchResult {
    @SerializedName("results")
    lateinit var items: List<SearchResultItem>
}

And now for the main event. I created a class called FeedSearcher to contain all the methods needed to carry out a Feedly search request. It has one public method: getFeedList, which takes a String query or keyword, and returns a LiveData wrapping a list of SearchResultItems.

/*  Generates a search query and returns a list of results from Feedly */
class FeedSearcher(private val networkMonitor: NetworkMonitor) {

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    private val feedlyApi = retrofit.create(FeedlyApi::class.java)

    fun getFeedList(query: String): LiveData<List<SearchResultItem>> {
        return if (networkMonitor.isOnline) {
            val path = generatePath(query)
            val request: Call<SearchResult> = feedlyApi.fetchSearchResult(path)
            fetchSearchResult(request)
        } else {
            MutableLiveData(emptyList())
        }
    }

    private fun generatePath(query: String): String {
        return Uri.Builder()
            .path("v3/search/feeds")
            .appendQueryParameter("count", RESULTS_COUNT.toString())
            .appendQueryParameter("query", URLEncoder.encode(query, "UTF-8"))
            .build()
            .toString()
    }

    private fun fetchSearchResult(
        request: Call<SearchResult>
    ): MutableLiveData<List<SearchResultItem>> {
        val searchResultLiveData = MutableLiveData<List<SearchResultItem>>()
        val callback = object : Callback<SearchResult> {
            override fun onFailure(call: Call<SearchResult>, t: Throwable) {} // Do nothing

            override fun onResponse(
                call: Call<SearchResult>,
                response: Response<SearchResult>
            ) {
                val feedSearchResult = response.body()
                searchResultLiveData.value = feedSearchResult?.items ?: emptyList()
            }
        }

        request.enqueue(callback)
        return searchResultLiveData
    }

    companion object {
        private const val RESULTS_COUNT = 100
        private const val BASE_URL = "https://cloud.feedly.com/"
    }
}

There are several things going on here. First we need a Retrofit object with a base URL of “https://cloud.feedly.com/” to be able to do anything—it accepts the above FeedlyApi interface, and uses it to create a request. To the request itself, we pass a String path to make a complete search URL on top of the base URL; the private method generatePath accepts our String query and returns it in the form (which includes a specified number of search results, in this case 100) that the Feedly search engine expects.

Finally, within the private method fetchSearchResult, the request object is enqueued, which actually executes the HTTP request. The method immediately returns a LiveData to the previous method that called it. We also pass in a callback object that defines what should happen after a request completes successfully or unsuccessfully. In this case, upon a successful request, the aforementioned LiveData is updated with the response—a list of SearchResultItems. And thanks to the attached GsonConverterFactory, Retrofit automatically does the work of parsing the response body from JSON.

Like our first data source, FeedParser, FeedSearcher is also self-contained so that we can completely replace it with something else without affecting anything else in the app, although Feedly’s search engine works perfectly for my purposes. The most important thing it gives us is a collection of usable RSS feed URLS—any of these URLS can then be passed on to FeedParser to attempt to retrieve an actual RSS feed.

Write a Comment

Comment

Webmentions

  • Building an RSS Reader for Android #3: Setting up a relational database, many-to-many relationships, and Room – Joshua Cerdenia

    […] in this series, I wrote about FeedParser and FeedSearcher, each representing a data source for the app. The former is responsible for fetching and parsing […]