News App using REST API (JSON Parsing)

0

News App using REST API (JSON Parsing)
educational implementation of a News App using a REST API with JSON parsing in Android Kotlin. I’ll use the GNews API (free, no API key required for limited usage) or NewsAPI.org (requires free API key).

Prerequisites

  1. Get API Key from NewsAPI.org (free tier – 100 requests/day) or use GNews API
  2. Add Internet permission in AndroidManifest.xml

Project Structure (MVVM Pattern)

text

app/
├── manifests/AndroidManifest.xml
├── java/.../
│   ├── data/
│   │   ├── api/NewsApiService.kt
│   │   ├── model/NewsResponse.kt
│   │   ├── model/Article.kt
│   │   └── repository/NewsRepository.kt
│   ├── ui/
│   │   ├── MainActivity.kt
│   │   ├── NewsViewModel.kt
│   │   └── NewsAdapter.kt
│   └── utils/Constants.kt
└── res/layout/
    ├── activity_main.xml
    └── item_news.xml

Step 1: Add Dependencies (build.gradle.kts – Module level)

kotlin

dependencies {
    // Retrofit for API calls
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    
    // Gson for JSON parsing
    implementation("com.google.code.gson:gson:2.10.1")
    
    // ViewModel and LiveData
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // RecyclerView
    implementation("androidx.recyclerview:recyclerview:1.3.2")
    
    // CardView
    implementation("androidx.cardview:cardview:1.0.0")
    
    // Glide for loading images
    implementation("com.github.bumptech.glide:glide:4.16.0")
    
    // Swipe to refresh layout
    implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
}

Step 2: Add Internet Permission (AndroidManifest.xml)

xml

<uses-permission android:name="android.permission.INTERNET" />

<application
    android:usesCleartextTraffic="true"
    ...>

Step 3: Create Constants (utils/Constants.kt)

kotlin

package com.yourapp.newsapp.utils

object Constants {
    // For NewsAPI.org (requires API key)
    const val NEWS_API_BASE_URL = "https://newsapi.org/v2/"
    const val NEWS_API_KEY = "YOUR_API_KEY_HERE"  // Get from newsapi.org
    
    // Alternative: GNews API (free, no key required for basic usage)
    // const val GNEWS_BASE_URL = "https://gnews.io/api/v4/"
    
    // Categories
    val CATEGORIES = listOf(
        "General", "Business", "Technology", "Entertainment", 
        "Sports", "Science", "Health"
    )
    
    // Countries
    val COUNTRIES = mapOf(
        "US" to "United States",
        "GB" to "United Kingdom",
        "IN" to "India",
        "CA" to "Canada",
        "AU" to "Australia",
        "DE" to "Germany",
        "FR" to "France"
    )
}

Step 4: Create Data Models (data/model/)

Article.kt

kotlin

package com.yourapp.newsapp.data.model

import com.google.gson.annotations.SerializedName

data class Article(
    @SerializedName("source")
    val source: Source?,
    
    @SerializedName("author")
    val author: String?,
    
    @SerializedName("title")
    val title: String?,
    
    @SerializedName("description")
    val description: String?,
    
    @SerializedName("url")
    val url: String?,
    
    @SerializedName("urlToImage")
    val urlToImage: String?,
    
    @SerializedName("publishedAt")
    val publishedAt: String?,
    
    @SerializedName("content")
    val content: String?
)

data class Source(
    @SerializedName("id")
    val id: String?,
    
    @SerializedName("name")
    val name: String?
)

NewsResponse.kt

kotlin

package com.yourapp.newsapp.data.model

import com.google.gson.annotations.SerializedName

data class NewsResponse(
    @SerializedName("status")
    val status: String,
    
    @SerializedName("totalResults")
    val totalResults: Int,
    
    @SerializedName("articles")
    val articles: List<Article>,
    
    @SerializedName("code")
    val code: String?,
    
    @SerializedName("message")
    val message: String?
)

Step 5: Create API Service (data/api/NewsApiService.kt)

kotlin

package com.yourapp.newsapp.data.api

import com.yourapp.newsapp.data.model.NewsResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApiService {
    
    @GET("top-headlines")
    suspend fun getTopHeadlines(
        @Query("country") country: String = "us",
        @Query("category") category: String? = null,
        @Query("apiKey") apiKey: String,
        @Query("pageSize") pageSize: Int = 20,
        @Query("page") page: Int = 1
    ): NewsResponse
    
    @GET("everything")
    suspend fun searchNews(
        @Query("q") query: String,
        @Query("apiKey") apiKey: String,
        @Query("pageSize") pageSize: Int = 20,
        @Query("page") page: Int = 1
    ): NewsResponse
    
    @GET("top-headlines")
    suspend fun getNewsByCategory(
        @Query("country") country: String,
        @Query("category") category: String,
        @Query("apiKey") apiKey: String,
        @Query("pageSize") pageSize: Int = 20
    ): NewsResponse
}

Step 6: Create Retrofit Instance (data/api/RetrofitInstance.kt)

kotlin

package com.yourapp.newsapp.data.api

import com.yourapp.newsapp.utils.Constants
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    
    private val retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(Constants.NEWS_API_BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    val api: NewsApiService by lazy {
        retrofit.create(NewsApiService::class.java)
    }
}

Step 7: Create Repository (data/repository/NewsRepository.kt)

kotlin

package com.yourapp.newsapp.data.repository

import com.yourapp.newsapp.data.api.RetrofitInstance
import com.yourapp.newsapp.data.model.NewsResponse
import com.yourapp.newsapp.utils.Constants

class NewsRepository {
    
    private val apiService = RetrofitInstance.api
    
    suspend fun getTopHeadlines(
        country: String = "us",
        category: String? = null
    ): Result<NewsResponse> {
        return try {
            val response = apiService.getTopHeadlines(
                country = country,
                category = category,
                apiKey = Constants.NEWS_API_KEY
            )
            
            if (response.status == "ok") {
                Result.success(response)
            } else {
                Result.failure(Exception(response.message ?: "Failed to fetch news"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun searchNews(query: String): Result<NewsResponse> {
        return try {
            val response = apiService.searchNews(
                query = query,
                apiKey = Constants.NEWS_API_KEY
            )
            
            if (response.status == "ok") {
                Result.success(response)
            } else {
                Result.failure(Exception(response.message ?: "Search failed"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getNewsByCategory(category: String, country: String = "us"): Result<NewsResponse> {
        return try {
            val response = apiService.getNewsByCategory(
                country = country,
                category = category.lowercase(),
                apiKey = Constants.NEWS_API_KEY
            )
            
            if (response.status == "ok") {
                Result.success(response)
            } else {
                Result.failure(Exception(response.message ?: "Failed to fetch category news"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Step 8: Create ViewModel (ui/NewsViewModel.kt)

kotlin

package com.yourapp.newsapp.ui

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yourapp.newsapp.data.model.Article
import com.yourapp.newsapp.data.repository.NewsRepository
import kotlinx.coroutines.launch

class NewsViewModel : ViewModel() {
    
    private val repository = NewsRepository()
    
    private val _newsArticles = MutableLiveData<List<Article>>()
    val newsArticles: LiveData<List<Article>> = _newsArticles
    
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading
    
    private val _errorMessage = MutableLiveData<String>()
    val errorMessage: LiveData<String> = _errorMessage
    
    private val _currentCategory = MutableLiveData<String>("General")
    val currentCategory: LiveData<String> = _currentCategory
    
    private val _isSearching = MutableLiveData<Boolean>()
    val isSearching: LiveData<Boolean> = _isSearching
    
    init {
        fetchTopHeadlines()
    }
    
    fun fetchTopHeadlines(country: String = "us") {
        viewModelScope.launch {
            _isLoading.value = true
            _isSearching.value = false
            
            val result = repository.getTopHeadlines(country)
            
            result.onSuccess { newsResponse ->
                _newsArticles.value = newsResponse.articles
                _errorMessage.value = null
            }.onFailure { exception ->
                _errorMessage.value = exception.message ?: "Failed to load news"
                _newsArticles.value = emptyList()
            }
            
            _isLoading.value = false
        }
    }
    
    fun fetchNewsByCategory(category: String, country: String = "us") {
        viewModelScope.launch {
            _isLoading.value = true
            _isSearching.value = false
            _currentCategory.value = category
            
            val result = repository.getNewsByCategory(category, country)
            
            result.onSuccess { newsResponse ->
                _newsArticles.value = newsResponse.articles
                _errorMessage.value = null
            }.onFailure { exception ->
                _errorMessage.value = exception.message ?: "Failed to load $category news"
                _newsArticles.value = emptyList()
            }
            
            _isLoading.value = false
        }
    }
    
    fun searchNews(query: String) {
        if (query.trim().isEmpty()) {
            fetchTopHeadlines()
            return
        }
        
        viewModelScope.launch {
            _isLoading.value = true
            _isSearching.value = true
            
            val result = repository.searchNews(query)
            
            result.onSuccess { newsResponse ->
                _newsArticles.value = newsResponse.articles
                _errorMessage.value = null
            }.onFailure { exception ->
                _errorMessage.value = exception.message ?: "Search failed"
                _newsArticles.value = emptyList()
            }
            
            _isLoading.value = false
        }
    }
    
    fun refreshNews() {
        if (_isSearching.value == true) {
            // If currently searching, we don't know the search query
            // In a real app, you'd store the last search query
            fetchTopHeadlines()
        } else {
            fetchNewsByCategory(_currentCategory.value ?: "General")
        }
    }
}

Step 9: Create News Adapter (ui/NewsAdapter.kt)

kotlin

package com.yourapp.newsapp.ui

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.yourapp.newsapp.R
import com.yourapp.newsapp.data.model.Article
import java.text.SimpleDateFormat
import java.util.*

class NewsAdapter(
    private var articles: List<Article>,
    private val onItemClick: (Article) -> Unit
) : RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_news, parent, false)
        return NewsViewHolder(view)
    }
    
    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        val article = articles[position]
        holder.bind(article)
        holder.itemView.setOnClickListener { onItemClick(article) }
    }
    
    override fun getItemCount(): Int = articles.size
    
    fun updateArticles(newArticles: List<Article>) {
        articles = newArticles
        notifyDataSetChanged()
    }
    
    class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val ivNewsImage: ImageView = itemView.findViewById(R.id.ivNewsImage)
        private val tvTitle: TextView = itemView.findViewById(R.id.tvTitle)
        private val tvDescription: TextView = itemView.findViewById(R.id.tvDescription)
        private val tvSource: TextView = itemView.findViewById(R.id.tvSource)
        private val tvTime: TextView = itemView.findViewById(R.id.tvTime)
        
        fun bind(article: Article) {
            tvTitle.text = article.title ?: "No title"
            tvDescription.text = article.description ?: "No description available"
            tvSource.text = article.source?.name ?: "Unknown source"
            
            // Format the published date
            article.publishedAt?.let { dateString ->
                tvTime.text = formatDate(dateString)
            } ?: run {
                tvTime.text = "Unknown date"
            }
            
            // Load image using Glide
            article.urlToImage?.let { imageUrl ->
                Glide.with(itemView.context)
                    .load(imageUrl)
                    .placeholder(R.drawable.placeholder_image)
                    .error(R.drawable.error_image)
                    .into(ivNewsImage)
            } ?: run {
                ivNewsImage.setImageResource(R.drawable.placeholder_image)
            }
        }
        
        private fun formatDate(dateString: String): String {
            return try {
                val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
                inputFormat.timeZone = TimeZone.getTimeZone("UTC")
                val date = inputFormat.parse(dateString)
                
                val outputFormat = SimpleDateFormat("MMM dd, yyyy • hh:mm a", Locale.getDefault())
                outputFormat.format(date ?: Date())
            } catch (e: Exception) {
                dateString.substring(0, minOf(10, dateString.length))
            }
        }
    }
}

Step 10: Create Layout Files

activity_main.xml

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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- Toolbar -->
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:title="News App" />

    <!-- Search Section -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <EditText
            android:id="@+id/etSearch"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Search news..."
            android:padding="12dp"
            android:background="@drawable/edit_text_border"
            android:imeOptions="actionSearch"
            android:inputType="text"/>

        <Button
            android:id="@+id/btnSearch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Search" />
    </LinearLayout>

    <!-- Category Chips -->
    <com.google.android.material.chip.ChipGroup
        android:id="@+id/chipGroupCategories"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        app:singleSelection="true"
        app:selectionRequired="false"/>

    <!-- Swipe to Refresh Layout -->
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvNews"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="8dp"/>

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <!-- Loading Indicator -->
    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone"/>

    <!-- Error Message -->
    <TextView
        android:id="@+id/tvError"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:padding="16dp"
        android:textColor="#FF0000"
        android:textSize="16sp"
        android:visibility="gone"/>

</LinearLayout>

item_news.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- News Image -->
        <ImageView
            android:id="@+id/ivNewsImage"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:scaleType="centerCrop"
            android:src="@drawable/placeholder_image" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="12dp">

            <!-- Title -->
            <TextView
                android:id="@+id/tvTitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                android:textStyle="bold"
                android:maxLines="2"
                android:ellipsize="end" />

            <!-- Description -->
            <TextView
                android:id="@+id/tvDescription"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:textSize="14sp"
                android:maxLines="3"
                android:ellipsize="end" />

            <!-- Source and Time -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/tvSource"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textSize="12sp"
                    android:textColor="#666666" />

                <TextView
                    android:id="@+id/tvTime"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="12sp"
                    android:textColor="#888888" />
            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

</androidx.cardview.widget.CardView>

Drawable resources (res/drawable/)

edit_text_border.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white" />
    <stroke android:width="1dp" android:color="#CCCCCC" />
    <corners android:radius="4dp" />
</shape>

placeholder_image.xml (vector drawable or use a PNG)

Step 11: Create MainActivity (ui/MainActivity.kt)

kotlin

package com.yourapp.newsapp.ui

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.yourapp.newsapp.R
import com.yourapp.newsapp.data.model.Article
import com.yourapp.newsapp.utils.Constants

class MainActivity : AppCompatActivity() {
    
    private lateinit var viewModel: NewsViewModel
    private lateinit var newsAdapter: NewsAdapter
    private lateinit var recyclerView: RecyclerView
    private lateinit var swipeRefreshLayout: SwipeRefreshLayout
    private lateinit var progressBar: ProgressBar
    private lateinit var tvError: TextView
    private lateinit var etSearch: EditText
    private lateinit var btnSearch: Button
    private lateinit var chipGroupCategories: ChipGroup
    
    private var currentCountry = "us"
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        initializeViews()
        setupRecyclerView()
        setupViewModel()
        setupObservers()
        setupListeners()
        setupCategoryChips()
        
        // Load initial news
        viewModel.fetchTopHeadlines(currentCountry)
    }
    
    private fun initializeViews() {
        recyclerView = findViewById(R.id.rvNews)
        swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
        progressBar = findViewById(R.id.progressBar)
        tvError = findViewById(R.id.tvError)
        etSearch = findViewById(R.id.etSearch)
        btnSearch = findViewById(R.id.btnSearch)
        chipGroupCategories = findViewById(R.id.chipGroupCategories)
        
        // Setup toolbar
        val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)
    }
    
    private fun setupRecyclerView() {
        newsAdapter = NewsAdapter(emptyList()) { article ->
            // Open article in browser when clicked
            article.url?.let { url ->
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                startActivity(intent)
            }
        }
        
        recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = newsAdapter
        }
    }
    
    private fun setupViewModel() {
        viewModel = ViewModelProvider(this)[NewsViewModel::class.java]
    }
    
    private fun setupObservers() {
        // Observe news articles
        viewModel.newsArticles.observe(this, Observer { articles ->
            newsAdapter.updateArticles(articles)
            swipeRefreshLayout.isRefreshing = false
        })
        
        // Observe loading state
        viewModel.isLoading.observe(this, Observer { isLoading ->
            if (isLoading && newsAdapter.itemCount == 0) {
                progressBar.visibility = View.VISIBLE
                recyclerView.visibility = View.GONE
            } else {
                progressBar.visibility = View.GONE
                recyclerView.visibility = View.VISIBLE
            }
            if (!isLoading) {
                swipeRefreshLayout.isRefreshing = false
            }
        })
        
        // Observe error messages
        viewModel.errorMessage.observe(this, Observer { errorMsg ->
            if (!errorMsg.isNullOrEmpty() && newsAdapter.itemCount == 0) {
                tvError.text = errorMsg
                tvError.visibility = View.VISIBLE
                recyclerView.visibility = View.GONE
            } else {
                tvError.visibility = View.GONE
            }
        })
    }
    
    private fun setupListeners() {
        // Search button click
        btnSearch.setOnClickListener {
            val query = etSearch.text.toString().trim()
            if (query.isNotEmpty()) {
                viewModel.searchNews(query)
                hideKeyboard()
            } else {
                Toast.makeText(this, "Please enter a search term", Toast.LENGTH_SHORT).show()
            }
        }
        
        // Search on keyboard enter
        etSearch.setOnEditorActionListener { _, actionId, _ ->
            if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH) {
                val query = etSearch.text.toString().trim()
                if (query.isNotEmpty()) {
                    viewModel.searchNews(query)
                    hideKeyboard()
                }
                true
            } else {
                false
            }
        }
        
        // Swipe to refresh
        swipeRefreshLayout.setOnRefreshListener {
            viewModel.refreshNews()
        }
    }
    
    private fun setupCategoryChips() {
        Constants.CATEGORIES.forEach { category ->
            val chip = Chip(this)
            chip.text = category
            chip.isCheckable = true
            chip.id = View.generateViewId()
            
            chip.setOnCheckedChangeListener { _, isChecked ->
                if (isChecked) {
                    viewModel.fetchNewsByCategory(category, currentCountry)
                    etSearch.text.clear()
                    hideKeyboard()
                }
            }
            
            chipGroupCategories.addView(chip)
        }
    }
    
    private fun hideKeyboard() {
        val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
        currentFocus?.let {
            inputMethodManager.hideSoftInputFromWindow(it.windowToken, 0)
        }
    }
}

Step 12: Optional – Add WebView for Reading Articles

If you prefer to read articles inside the app instead of opening browser:

Create WebViewActivity.kt

kotlin

package com.yourapp.newsapp.ui

import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import com.yourapp.newsapp.R

class WebViewActivity : AppCompatActivity() {
    
    private lateinit var webView: WebView
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_webview)
        
        webView = findViewById(R.id.webView)
        val url = intent.getStringExtra("url")
        
        setupWebView()
        
        url?.let {
            webView.loadUrl(it)
        }
    }
    
    private fun setupWebView() {
        webView.apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            webViewClient = WebViewClient()
        }
    }
}

activity_webview.xml

xml

<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/webView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Then modify the click listener in MainActivity:

kotlin

newsAdapter = NewsAdapter(emptyList()) { article ->
    val intent = Intent(this@MainActivity, WebViewActivity::class.java)
    intent.putExtra("url", article.url)
    startActivity(intent)
}

How It Works – Understanding the Flow

  1. JSON Response Structure: NewsAPI returns JSON with articles array containing title, description, image URL, etc.
  2. Retrofit Parsing: Retrofit with Gson automatically converts the JSON string into NewsResponse and Article objects.
  3. Data Flow:
    • User selects category or searches → Activity calls ViewModel method
    • ViewModel launches coroutine → calls Repository
    • Repository makes Retrofit API call → gets JSON response
    • Gson parses JSON → Kotlin objects
    • Result flows back through LiveData
    • Adapter updates RecyclerView
  4. JSON to Object Mapping: The @SerializedName annotation maps JSON keys to Kotlin property names.

Testing the App

  1. Get API key from NewsAPI.org
  2. Replace YOUR_API_KEY_HERE in Constants.kt
  3. Run the app
  4. Browse top headlines, search for topics, or filter by category

Understanding JSON Parsing with Retrofit

Retrofit handles JSON parsing automatically when you use GsonConverterFactory. The process is:

text

JSON Response → Gson → Kotlin Data Class

Example of JSON response from NewsAPI:

json

{
  "status": "ok",
  "totalResults": 100,
  "articles": [
    {
      "source": {"id": "cnn", "name": "CNN"},
      "author": "John Doe",
      "title": "Breaking News",
      "description": "Something important happened",
      "url": "https://example.com/article",
      "urlToImage": "https://example.com/image.jpg",
      "publishedAt": "2024-01-15T10:30:00Z"
    }
  ]
}

This JSON automatically maps to your NewsResponse and Article classes.

Common Issues and Solutions

Issue: API key invalid or expired

Issue: No images loading

  • Solution: Some articles don’t have images, check for null urlToImage

Issue: CORS errors in testing

  • Solution: Use physical device or emulator with internet access

Issue: JSON parsing error

Solution: Verify your data classes match the exact JSON structure from the API

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept