Home About Contact

 

Develop a simple News Search Android app with Kotlin & NewsApi

8 min read


NewsApi.org provides a simple and easy way to fetch news headlines for different topics and sources with its REST api. To fetch news from them, you just need to sign up for a free developer account and get an API key. All you need to do is make a simple HTTP request by providing the following parameters and it’ll return results in JSON.

Parameters required for NewsAPI HTTP request

  • q = the text or topic to query
  • from – the date published in the format of yyyy-MM-dd
  • sortBy – how you want to sort the news, you can just simply use publishedAt, meaning sorting by its published date
  • apiKey – this is your API_KEY after you’ve signed up and signed into your developer account.
  • language – and you can specify the language of the news e.g. en is English

Let’s make a summary first of what we need to do for developing a simple Android app with Kotlin language to display the news headlines by using NewsAPI REST API.

This simple Android app as shown in the image above, has a RecyclerView displaying the news headlines and the first row of the RecyclerView shows a search view that allows users to search for different news headlines by a keyword. The default is Covid 19 here.

In this example, we simply take the MVC approach, which the MainActivity is our view controller, the RecyclerView that presents the searchView and the news results are the views and the model is the news Article.

Let’s break it down to more Android and the components we need are as follows:

  • A RecyclerView in our MainActivity
  • A RecyclerViewAdapter (list adapter for the RecyclerView) that shows two different types of views; they’re the search view on top and below the searchView are the views for the news headlines.
  • A SearchViewOnChangeListener interface for callback when user has entered some text in the searchView and pressed enter to continue.
  • Google Gson for easy parsing of JSON structure and convert them into objects
  • A NewsResult class that contains the status of the JSON result, and a List of news Article objects
  • The data holder class Article for holding each structure of the news headlines
  • And we need Glide to load and display the image that comes along with each news headline.
  • For making HTTP request to NewsAPI, we simply use AsyncTask here for simplicity without needing to import external library
  • An interface NewsFetchedListener, which is the callback by the AsyncTask, when news items successfully fetched or failed with error.


So, lets start the project by choosing an Empty Activity from your Android Studio 4.0. So, as mentioned above we need RecyclerView, Glide & Google Gson, and also we’ll use ConstraintLayout to more easily to construct & layout the view for each news headline item, therefore, we need to add the followings in our Project Module Gradle.

dependencies {
    ....
    .
    implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta7"
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation "androidx.recyclerview:recyclerview:1.2.0-alpha04"
    implementation 'com.github.bumptech.glide:glide:4.10.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
}

And first of all in the XML layout activity_main.xml, lets add the RecyclerView and remove the TextView added by default as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:scrollbars="none"
        android:layout_marginBottom="15sp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="true"
        android:visibility="visible"
        android:layout_weight="9"/>

</androidx.constraintlayout.widget.ConstraintLayout>

And lets design the XML layouts for the two types of views on the RecyclerView, they’re the search view and the view for each news headline item.

The search view (news_search_layout.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:padding="8dp"
        android:background="@android:color/black"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    <SearchView
            android:id="@+id/search"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@android:color/darker_gray"
            android:iconifiedByDefault="false"
            app:queryBackground="@android:color/background_light"
            app:submitBackground="@android:color/background_dark">
    </SearchView>
</LinearLayout>

And the XML layout of each news headline item (news_item_layout.xml). Each item contains an image displayed at the left side, a news title and some description of the news and also the date published.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="140sp"
    android:padding="2sp"
    tools:context=".MainActivity">


    <ImageView
        android:id="@+id/newsImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginStart="4dp"
        android:layout_marginLeft="4dp"
        android:padding="2sp"
        android:scaleType="centerCrop"
        android:src="@drawable/image"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHeight_max="100sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.138"
        app:layout_constraintWidth_max="100sp" />

    <TextView
        android:id="@+id/newsTitle"
        android:layout_width="260sp"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:ellipsize="end"
        android:gravity="start"
        android:maxLines="1"
        android:text="@string/default_news_title"
        android:textAlignment="gravity"
        android:textColor="@android:color/background_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/newsImage"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.063"
        app:layout_constraintWidth_max="300sp"
        app:layout_constraintWidth_min="160sp" />

    <TextView
        android:id="@+id/newsDescription"
        android:layout_width="260sp"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        android:ellipsize="end"
        android:gravity="start"
        android:maxLines="3"
        android:text="@string/default_news_description"
        android:textAlignment="gravity"
        android:textColor="@android:color/black"
        android:textSize="18sp"
        app:layout_constraintLeft_toRightOf="@id/newsImage"
        app:layout_constraintTop_toBottomOf="@+id/newsTitle"
        app:layout_constraintWidth_max="300sp"
        app:layout_constraintWidth_min="160sp" />


    <TextView
        android:id="@+id/newsDate"
        android:layout_width="200sp"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        android:ellipsize="end"
        android:gravity="start"
        android:maxLines="1"
        android:text="@string/default_date"
        android:textAlignment="gravity"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        app:layout_constraintLeft_toRightOf="@id/newsImage"
        app:layout_constraintTop_toBottomOf="@+id/newsDescription"
        app:layout_constraintWidth_max="200sp"
        app:layout_constraintWidth_min="100sp" />

</androidx.constraintlayout.widget.ConstraintLayout>

The design view of the news_item_layout.xml as follows:

Now, before proceeding to build the RecyclerViewAdapter, let’s build the few data holder classes that hold the structure of the JSON result and each of the news article as follows.

class NewsResult{
    var status : String? = null
    var articles : List<Article>? = null
}

class Article {
    var title : String? = null
    var description : String? = null
    var url : String? = null
    var urlToImage : String? = null
    var publishedAt : Date? = null
}

For simplicity, the Article class just holds the title, description, url to news, url to the image and the date of when it’s published.

Now, lets build a NewsFetchingAsyncTask which is a sub-class of AsyncTask, that will fetch the news items from NewsApi.org.

In the doBackground method, we simply make it to fetch news published on the system date or today, and the query “q” is based on the constructor parameter passed in. Please note that the date has been converted to the format yyyy-MM-dd by using SimpleDateFormat

override fun doInBackground(vararg p0: String?): String {
        val date = Date()
        val formatter = SimpleDateFormat("yyyy-MM-dd",  Locale.getDefault())
        val dateAsString = formatter.format(date)
        val myurl = "https://newsapi.org/v2/everything?q=$q&from=$dateAsString&sortBy=publishedAt&apiKey=API_KEY&language=en"
        val s = sendGet(myurl)

        return s
}

And in the onPostExecute method, the returned JSON result will be parsed using Google Gson and list of articles that will be sent to callback, a class that implements the NewsFetchedListener interface, which is supposed to be implemented by the MainActivity here.

override fun onPostExecute(result: String?) {
    if ( result != null ){
         parseReturnedJsonData(result)
    }
}

private fun parseReturnedJsonData(s: String) {
    val p = Gson()
    val rt = p.fromJson(s, NewsResult::class.java)

    if ( rt.status == "ok" ){
       newsFetchedListener?.whenNewsFetchedSuccessfully(rt.articles)
    }
    else {
       newsFetchedListener?.whenNewsFetchedOnError("Error")
    }
}

The NewsFetchedListener interface for callbacks when news items are fetched successfully or with error

interface NewsFetchedListener {
    fun whenNewsFetchedSuccessfully ( articles : List<Article>?)

    fun whenNewsFetchedOnError ( error: String? )
}

In order to display the results of the news fetched from NewsAPI, we use the RecyclerView here to display a list of the news headline items and we need a list adapter for our RecyclerView which extends the RecyclerView.Adapter and name it as RecyclerViewAdapter here.

Since there are two types of views in our RecyclerViewAdapter, on top is a search view and below the search view is the list of news headline items. Therefore, in the onCreateViewHolder method of the adapter, will inflate the respective XML layout and return the respective kind of view holder, dependent on the viewType.

The getViewType method is overridden as follows, whereas the first position 0 is the viewType 0 and the rest are viewType 1

override fun getItemViewType(position: Int): Int {

    if ( position == 0) return 0
    return 1
} 

The onCreateViewHolder() method returns two different types of view holders dependent on the viewType

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

    return if ( viewType == 0 ){
        val v = parent.inflate(R.layout.news_search_layout)
        SearchViewHolder(v)
    } 
    else {
        val v = parent.inflate(R.layout.news_item_layout)
        NewsItemViewHolder(v)
    }
 }

The two different types of view holders are as follows:
The news item view holder

inner class NewsItemViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView) {
        val newsImage : ImageView = itemView.findViewById(R.id.newsImage)
        val newsTitle : TextView = itemView.findViewById(R.id.newsTitle)
        val newsDescription : TextView = itemView.findViewById(R.id.newsDescription)
        val newsDate : TextView = itemView.findViewById(R.id.newsDate)

        init {

            itemView.setOnClickListener {

                // should further implement here when clicked to 
                // open another activity to show the news details
            }
        }
}

The SearchViewHolder. Please take note the searchView OnQueryTextListener, the onQueryTextSubmit method, is which when the user has entered some text on the searchView and press enter, will trigger the searchView to send the searched text to the MainActivity that implements the callback SearchViewOnChangeListener.

inner class SearchViewHolder(itemView : View) :
        RecyclerView.ViewHolder(itemView) {

        private val searchView : SearchView = itemView.findViewById(R.id.search_view)

        init {
            searchView.clearFocus()
            searchView.setOnQueryTextListener(object :
               SearchView.OnQueryTextListener {
                  override fun onQueryTextChange(newText: String): Boolean {
                      return false
                  }

                  override fun onQueryTextSubmit(query: String): Boolean {
                     searchViewOnChangeListener?.searchViewOnQueryTextSubmit(query)
                     return false
                  }
              } 
          )
        }
    }

The SearchViewOnChangeListener

interface SearchViewOnChangeListener {
    fun searchViewOnQueryTextSubmit ( text : String? )
}

The onBindViewHolder() method is called by the RecyclerViewAdapter to display the news headline items for position greater than zero (position 0 is where the searchView is). Please take note, Glide is used here to load the image of the news item.

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

    if ( position >= 1 ){

        if ( articles != null ){
            val idx =  position - 1

            if ( articles?.indices?.contains(idx ) == true ){

                    val a = articles!![ idx ]
                    val newsItemViewHolder = (holder as NewsItemViewHolder)
                    newsItemViewHolder.newsTitle.text  = a.title
                    newsItemViewHolder.newsDescription.text = a.description
                    newsItemViewHolder.newsDate.text = a.publishedAt

                    if ( context != null ){
                        Glide.with(context).load(a.urlToImage)
                            .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC )
                            .placeholder(R.drawable.image)
                            .error(R.drawable.image)
                            .fallback(R.drawable.image)
                            .into(newsItemViewHolder.newsImage)
                    }

             }

        }

    }
}

Please take of the getItemCount() method of the RecylcerViewAdapter returns number of articles plus one as the first row is the searchView and below the searchView are the news article headlines.

override fun getItemCount(): Int {
   return ( 1 + (articles?.size ?: 0) )
}

And in the RecyclerViewAdapter, we have a method refreshNewsItems, which is for the MainActivity to pass in new news headline items, when they’re obtained from its NewsFetchedListener callback.

Please take note, we refresh the adapter for change from position 1 onwards till the articles’ size (as position 0 is the searchView, which does not need to be refreshed here) and we need to remove those rows if the newly fetched articles have smaller size than the previously stored articles.

internal fun refreshNewsItems ( articles: List<Article>?){

    val initialSize = this.articles?.size ?: 0

    this.articles = articles
    if ( articles != null ){

        notifyItemRangeChanged(1,articles?.size ?: 0)
        if ( articles.size < initialSize ){

            val sizeDifference = initialSize - articles.size
            notifyItemRangeRemoved(articles?.size, sizeDifference)
        }
    }
}

Lastly, putting them together, the MainActivity is our main view controller here, what it does is having callback method listening to the user's search input and send the searched text to the NewsFetchingAsyncTask and with callbacks listening to the results sent back by the AsyncTask and updates the RecyclerView accordingly. The MainActivity has the following methods:

  • initializeRecyclerView() to initialize the RecyclerView and apply its adapter etc.
  • The private method fetchNewsItems() which creates the NewsFetchingAsyncTask for fetching news items by the searched text.
  • searchViewOnQueryTextSubmit() - the callback method for implementing the SearchViewOnChangeListener, so when user has searched for a keyword and pressed enter, it'll send the searched text to fetchNewsItems() method for fetching the news headline items by using AsyncTask
  • whenNewsFetchedSuccessfully() - callback method for implementing NewsFetchedListener interface, so when news articles fetched successfully, it updates the adapter of the RecylerView to display the newly fetched articles.
  • whenNewsFetchedOnError - callback method for implementing NewsFetchedListener interface, for handling error when failing to fetch news articles, we simply show a toast message here.
  • The onCreate() method of the MainActivity will initialize the RecyclerView and fetch the news articles by the default keyword "Covid 19"

class MainActivity : AppCompatActivity(), NewsFetchedListener, SearchViewOnChangeListener {

    private lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initializeRecyclerView()
        fetchNewsItems()
    }

    private fun initializeRecyclerView(){

        recyclerView = recycler_view
        val recyclerViewAdapter = RecyclerViewAdapter(null, this, this)

        recyclerView.apply {
            layoutManager = LinearLayoutManager(
                context,
                RecyclerView.VERTICAL, false
            )
            adapter = recyclerViewAdapter
        }
    }

    private fun fetchNewsItems( query : String = getString(R.string.default_search_text)){
        val n = NewsFetchingAsyncTask(query, this )
        n.execute()
    }

    override fun whenNewsFetchedSuccessfully(articles: List<Article>?) {
        val adapter = recyclerView.adapter as RecyclerViewAdapter
        adapter.refreshNewsItems(articles)
    }

    override fun whenNewsFetchedOnError(error: String?) {
        val t = Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT)
        t.setGravity(Gravity.TOP, 0, 500)
        t.show()
    }

    override fun searchViewOnQueryTextSubmit(text: String?) {
        val t = Toast.makeText(this, R.string.loading, Toast.LENGTH_SHORT)
        t.setGravity(Gravity.TOP, 0, 500)
        t.show()
        fetchNewsItems(text ?: "")
    }
}

The result of how the simple app works is shown in the animated gif below. The complete code can be found on GitHub

Spread the love
Posted on July 22, 2020 By Christopher Chee

Please leave us your comments below, if you find any errors or mistakes with this post. Or you have better idea to suggest for better result etc.


Our FB Twitter Our IG Copyright © 2024