Building a Modern News App with MVVM, Retrofit, Hilt, Pagging and Jetpack Compose in Kotlin

Building a Modern News App with MVVM, Retrofit, Hilt, Pagging and Jetpack Compose in Kotlin

Β·

12 min read

Hello developers! Today, we're going to create a News application using the MVVM design pattern, Retrofit for network calls, Hilt for dependency injection, and the modern UI toolkit Jetpack Compose for the user interface. Let's dive in and have some fun building this together!

Setting Up the Project

Begin by creating a fresh Android project in Android Studio with Kotlin as the main language. Ensure you've added the necessary dependencies in your project's build.gradle file for Retrofit, Hilt, and Jetpack Compose. To do this, open your build.gradle file and add the required implementation statements for Retrofit, Hilt, and Jetpack Compose libraries.

// Jetpack Compose
implementation "androidx.compose.ui:ui:1.0.5"
implementation "androidx.compose.material:material:1.0.5"
implementation "androidx.compose.ui:ui-tooling:1.0.5"
implementation "androidx.activity:activity-compose:1.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

// Hilt
implementation "com.google.dagger:hilt-android:2.39.1"
kapt "com.google.dagger:hilt-android-compiler:2.39.1"

Implementing MVVM Architecture

MVVM architecture is like splitting your app into three parts: Model, View, and ViewModel. The Model holds the data, the View shows the UI, and the ViewModel links the Model and the View together.

  1. Model: Define data classes to represent news articles, such as Article.

     data class Article(
         val title: String,
         val description: String,
         val imageUrl: String
         // Add other properties as needed
     )
    
  2. View: Design the UI using Jetpack Compose, including screens for displaying a list of articles and detailed article views.

    Setup Activity

     @AndroidEntryPoint
     class MainActivity : ComponentActivity() {
         private val newsViewModel: NewsViewModel by viewModels()
    
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContent {
                 NewsApp(newsViewModel)
             }
         }
     }
    

    The MainActivity class is like the front door of your Android app. It's based on ComponentActivity, a useful class from the Android Jetpack library for activities. By adding @AndroidEntryPoint, we let Hilt know to add the required dependencies here.

    In the onCreate method, the app's interface starts with Jetpack Compose's setContent function. Inside this function, the NewsApp function shapes how the UI looks and works. It also uses the newsViewModel passed as a parameter. This newsViewModel is obtained using the viewModels() function, creating a ViewModel that stays with the activity. With Hilt, all the necessary support is provided where it's needed.

    In short, this code prepares the main activity, sets up the ViewModel with Hilt, and designs the UI using Jetpack Compose.

    Setup Screen

     @Composable
     fun NewsApp(newsViewModel: NewsViewModel) {
         val screenState by newsViewModel.screenState.observeAsState(initial = NewsScreenState.Loading)
    
         Scaffold(
             topBar = {
                 TopAppBar(title = { Text("News App") })
             },
             content = {
                 when (screenState) {
                     is NewsScreenState.Loading -> LoadingIndicator()
                     is NewsScreenState.Success -> ArticleList((screenState as NewsScreenState.Success).articles)
                     is NewsScreenState.Error -> ErrorScreen((screenState as NewsScreenState.Error).message)
                 }
             }
         )
     }
    
     @Composable
     fun ArticleList(articles: List<Article>) {
         LazyColumn {
             items(articles) { article ->
                 ArticleItem(article)
             }
         }
     }
    
     @Composable
     fun ArticleItem(article: Article) {
         Card(
             modifier = Modifier
                 .padding(8.dp)
                 .fillMaxWidth(),
             elevation = 4.dp
         ) {
             Column(
                 modifier = Modifier
                     .padding(16.dp)
                     .fillMaxWidth()
             ) {
                 Text(text = article.title, style = MaterialTheme.typography.h6)
                 Spacer(modifier = Modifier.height(8.dp))
                 Text(text = article.description, style = MaterialTheme.typography.body1)
                 Spacer(modifier = Modifier.height(8.dp))
                 Image(
                     painter = rememberImagePainter(article.imageUrl),
                     contentDescription = null,
                     modifier = Modifier
                         .height(200.dp)
                         .fillMaxWidth(),
                     contentScale = ContentScale.Crop
                 )
             }
         }
     }
    
     @Composable
     fun LoadingIndicator() {
         Box(
             contentAlignment = Alignment.Center,
             modifier = Modifier.fillMaxSize()
         ) {
             CircularProgressIndicator()
         }
     }
    
     @Composable
     fun ErrorScreen(message: String) {
         Box(
             contentAlignment = Alignment.Center,
             modifier = Modifier.fillMaxSize()
         ) {
             Text(text = message, color = Color.Red, textAlign = TextAlign.Center)
         }
     }
    

    This code snippet defines a set of Jetpack Compose composable functions for building the UI of a news app:

    1. NewsApp: This composable function represents the main UI of the news app. It takes a newsViewModel parameter of type NewsViewModel to observe the screen state. Depending on the observed state, it displays different UI components:

      • If the state is Loading, it displays a loading indicator.

      • If the state is Success, it displays a list of articles using the ArticleList composable.

      • If the state is Error, it displays an error message using the ErrorScreen composable.

    2. ArticleList: This composable function represents a list of news articles. It takes a list of Article objects as a parameter and uses a LazyColumn to create a vertically scrolling list of articles. For each article, it calls the ArticleItem composable to display its content.

    3. ArticleItem: This composable function represents an individual news article item. It takes an Article object as a parameter and displays its title, description, and image within a Card component.

    4. LoadingIndicator: This composable function represents a loading indicator, typically displayed when the news articles are being fetched. It displays a CircularProgressIndicator centered within a Box.

    5. ErrorScreen: This composable function represents an error screen, typically displayed if there's an error fetching the news articles. It displays an error message in red, centered within a Box.

Overall, these composable functions define the UI components of the news app, such as the main screen layout, article list, loading indicator, and error screen. They utilize Jetpack Compose's declarative UI approach to efficiently manage UI hierarchy and state changes.

  1. ViewModel: Create ViewModel classes to handle business logic and interact with the repository.

     sealed class NewsScreenState {
         object Loading : NewsScreenState()
         data class Success(val articles: List<Article>) : NewsScreenState()
         data class Error(val message: String) : NewsScreenState()
     }
    
     class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
         private val _screenState = MutableLiveData<NewsScreenState>()
         val screenState: LiveData<NewsScreenState> get() = _screenState
    
         init {
             getArticles()
         }
    
         private fun getArticles() {
             viewModelScope.launch {
                 _screenState.value = NewsScreenState.Loading
                 try {
                     val articles = repository.getArticles()
                     _screenState.value = NewsScreenState.Success(articles)
                 } catch (e: Exception) {
                     _screenState.value = NewsScreenState.Error("Error fetching articles: ${e.message}")
                 }
             }
         }
     }
    

This code snippet defines a NewsScreenState sealed class and a NewsViewModel class:

  1. NewsScreenState:

    • It's a sealed class representing the different states of the news screen.

    • It has three subclasses:

      • Loading: Indicates that the news articles are being fetched.

      • Success: Contains a list of articles retrieved successfully from the repository.

      • Error: Contains an error message indicating the reason for the failure to fetch articles.

  2. NewsViewModel:

    • It's a ViewModel class responsible for managing the UI-related data of the news screen.

    • It takes a repository parameter of type NewsRepository as its constructor parameter. This repository is responsible for fetching news articles from the data source.

    • Inside the class, there's a private mutable live data _screenState of type NewsScreenState. This LiveData holds the current state of the news screen, which can be observed by the UI components.

    • The public property screenState exposes _screenState as an immutable LiveData, allowing external components to observe changes to the screen state.

    • In the init block, the getArticles() function is called when the ViewModel is initialized.

    • The private function getArticles() is a suspend function that uses Kotlin coroutines to fetch news articles asynchronously.

    • Inside getArticles(), _screenState is first set to Loading to indicate that the articles are being fetched.

    • Then, the repository.getArticles() function is called to fetch the articles from the repository.

    • If the retrieval is successful, _screenState is updated to Success with the retrieved articles.

    • If an exception occurs during the retrieval process, _screenState is updated to Error with an error message containing the exception message.

Overall, this code handles the process of fetching news articles from the repository and controls the UI state of the news screen. It uses LiveData and a sealed class to represent various states.

Integrating Retrofit and Repository

Retrofit simplifies network requests by allowing you to define an interface for your API calls and handle the rest for you.

  1. Define an interface for your API endpoints using Retrofit annotations.

     interface NewsApiService {
         @GET("articles")
         suspend fun getArticles(): Flow<PagingData<Article>>
     }
    

    This code snippet defines an interface NewsApiService that represents a Retrofit service for fetching news articles from an API:

    1. NewsApiService:

      • It's an interface that defines a Retrofit service for interacting with a REST API to fetch news articles.

      • It declares a suspend function getArticles() annotated with @GET("articles"). This function is responsible for fetching articles from the specified endpoint.

      • The function returns a Flow<PagingData<Article>>, indicating that it emits a stream of data represented by the Article model class. Using Flow allows for asynchronous and reactive data streaming, suitable for pagination and continuous data updates.

      • The @GET("articles") annotation specifies the relative URL of the API endpoint for fetching articles. The actual base URL is defined elsewhere, typically in the Retrofit client configuration.

Overall, this interface serves as a contract for defining the API endpoints and their corresponding data types. It encapsulates the networking logic required for fetching news articles and provides a structured way to interact with the API using Kotlin coroutines and Flow for asynchronous data handling.

  1. Create a repository interface.

     interface NewsRepository {
         suspend fun getArticles(): Flow<PagingData<Article>>
     }
    

    This code snippet defines an interface NewsRepository that acts as an abstraction layer between the data source (such as a remote server or local database) and the ViewModel:

    1. NewsRepository:

      • It's an interface that abstracts the data access operations related to news articles.

      • It declares a suspend function getArticles() that is responsible for fetching articles from the data source.

      • The function returns a Flow<PagingData<Article>>, indicating that it emits a stream of paginated article data represented by the Article model class. Using Flow allows for asynchronous and reactive data streaming, suitable for handling large datasets and continuous data updates.

      • The repository interface does not specify how the articles are fetched or from where. It serves as a contract defining the required data access operations, allowing for different implementations (e.g., fetching articles from a remote server using Retrofit, or from a local database using Room).

Overall, this interface defines a clear separation of concerns in the application architecture by abstracting the data access operations from the ViewModel. It promotes maintainability, testability, and flexibility by allowing different implementations of the repository interface to fetch data from various sources without impacting the ViewModel or UI logic.

  1. Now create a Repository implementation class.

     class NewsRepositoryImpl(
         private val apiService: NewsApiService,
         private val articleDao: ArticleDao
     ) : NewsRepository {
         override fun getArticles(): Flow<PagingData<Article>> {
             return Pager(
                 config = PagingConfig(pageSize = PAGE_SIZE, enablePlaceholders = false),
                 pagingSourceFactory = { ArticlePagingSource(apiService, articleDao) }
             ).flow
         }
     }
    
     class ArticlePagingSource(
         private val apiService: NewsApiService,
         private val articleDao: ArticleDao
     ) : PagingSource<Int, Article>() {
         override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
             val page = params.key ?: STARTING_PAGE_INDEX
             return try {
                 val response = apiService.getArticles(page)
                 articleDao.insertArticles(response)
                 LoadResult.Page(
                     data = response,
                     prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
                     nextKey = if (response.isEmpty()) null else page + 1
                 )
             } catch (e: Exception) {
                 LoadResult.Error(e)
             }
         }
     }
    

This code snippet includes two classes: NewsRepositoryImpl and ArticlePagingSource.

  1. NewsRepositoryImpl:

    • It's a class that implements the NewsRepository interface.

    • The getArticles() function returns a Flow of PagingData<Article>.

    • It uses a Pager from the Paging library to create a flow of paginated article data.

    • The Pager is configured with a PagingConfig specifying the page size and whether placeholders are enabled.

    • It uses an ArticlePagingSource as the paging source factory, which fetches articles from the API service and stores them in the local database.

  2. ArticlePagingSource:

    • It's a class that extends PagingSource<Int, Article>, which is responsible for loading data for the Pager.

    • The load function is called to load data for a given page.

    • It fetches articles from the API service based on the provided page number and stores them in the local database using the articleDao.

    • It returns a LoadResult.Page containing the loaded articles, along with the previous and next page keys, or a LoadResult.Error if an exception occurs during the loading process.

Overall, these classes collaborate to bring the repository pattern to life for fetching news articles. NewsRepositoryImpl gives us a big-picture view of how data access works, while ArticlePagingSource takes care of fetching data from the API service and local database. By splitting these responsibilities, we make our code more organized and easier to manage.

Utilizing Hilt for Dependency Injection

Hilt simplifies dependency injection, making your code cleaner and more maintainable.

  1. Annotate your application class with @HiltAndroidApp.

     @HiltAndroidApp
     class MyApp : Application()
    

    This code snippet demonstrates the declaration of an Android application class named MyApp annotated with @HiltAndroidApp:

    1. @HiltAndroidApp:

      • This annotation is used to generate the required Hilt components and initialize the dependency injection framework within the Android application.

      • It informs Hilt to generate the necessary components for dependency injection and sets up the application for field and method injection.

    2. MyApp class:

      • It extends the Application class provided by the Android framework, making it the entry point for the application's lifecycle.

      • By annotating this class with @HiltAndroidApp, it enables Hilt to integrate with the application's lifecycle and perform dependency injection.

Overall, this setup ensures that Hilt is properly integrated into the application and ready to provide dependency injection throughout the app's components, such as activities, fragments, ViewModels, and more.

  1. Define modules for providing dependencies, such as Retrofit instances and ViewModel bindings.

     @Module
     @InstallIn(ApplicationComponent::class)
     object AppModule {
         @Provides
         fun provideNewsApiService(): NewsApiService {
             return Retrofit.Builder()
                 .baseUrl(BASE_URL)
                 .addConverterFactory(GsonConverterFactory.create())
                 .build()
                 .create(NewsApiService::class.java)
         }
    
         @Provides
         fun provideNewsRepository(apiService: NewsApiService): NewsRepository {
             return NewsRepositoryImpl(apiService)
         }
     }
    

This code snippet defines an AppModule object annotated with @Module and @InstallIn(ApplicationComponent::class), providing dependencies for the application using Hilt:

  1. @Module:

    • This annotation indicates that the AppModule object provides dependencies to the Hilt dependency injection framework.
  2. @InstallIn(ApplicationComponent::class):

    • This annotation specifies that the dependencies provided by AppModule should be available for injection in the ApplicationComponent, which is the top-level component for the entire application.
  3. provideNewsApiService():

    • This function provides an instance of the NewsApiService interface using Retrofit.

    • It configures a Retrofit instance with a base URL and Gson converter factory to serialize and deserialize JSON.

    • Finally, it creates an instance of the NewsApiService interface using Retrofit's create() method.

  4. provideNewsRepository(apiService: NewsApiService):

    • This function provides an instance of the NewsRepository interface, which abstracts the data access operations related to news articles.

    • It takes apiService as a parameter, which is the NewsApiService dependency provided by provideNewsApiService().

    • It creates and returns an instance of NewsRepositoryImpl, passing the apiService dependency to its constructor.

Overall, this module sets up the connections for the tools needed by the application. It offers solutions for interfaces like NewsApiService and NewsRepository, utilizing Retrofit for handling network tasks and a repository pattern for managing data access. Hilt will utilize these connections to insert tools into other parts of the application, like ViewModels or activities.

Alright devs, as we finish up this blog post, I hope the details shared have helped you grasp MVVM architecture, Retrofit, Hilt, Kotlin Coroutines, Jetpack Compose, and the Paging library. With these tools, we've built a detailed news app reflecting modern Android development methods. This provides a strong base for future upgrades and new features, making sure the news app remains polished and user-friendly. Keep an eye out for our next article where we explore more interesting subjects!


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. πŸ“²πŸ‘¨β€πŸ’» Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my LinkedIn

Β