Building a Modern News App with MVVM, Retrofit, Hilt, Pagging and Jetpack Compose in Kotlin
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.
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 )
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 onComponentActivity
, 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'ssetContent
function. Inside this function, theNewsApp
function shapes how the UI looks and works. It also uses thenewsViewModel
passed as a parameter. ThisnewsViewModel
is obtained using theviewModels()
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:
NewsApp
: This composable function represents the main UI of the news app. It takes anewsViewModel
parameter of typeNewsViewModel
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 theArticleList
composable.If the state is
Error
, it displays an error message using theErrorScreen
composable.
ArticleList
: This composable function represents a list of news articles. It takes a list ofArticle
objects as a parameter and uses aLazyColumn
to create a vertically scrolling list of articles. For each article, it calls theArticleItem
composable to display its content.ArticleItem
: This composable function represents an individual news article item. It takes anArticle
object as a parameter and displays its title, description, and image within aCard
component.LoadingIndicator
: This composable function represents a loading indicator, typically displayed when the news articles are being fetched. It displays aCircularProgressIndicator
centered within aBox
.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 aBox
.
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.
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:
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.
NewsViewModel
:It's a ViewModel class responsible for managing the UI-related data of the news screen.
It takes a
repository
parameter of typeNewsRepository
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 typeNewsScreenState
. 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, thegetArticles()
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 toLoading
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 toSuccess
with the retrieved articles.If an exception occurs during the retrieval process,
_screenState
is updated toError
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.
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: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 theArticle
model class. UsingFlow
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.
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: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 theArticle
model class. UsingFlow
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.
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
.
NewsRepositoryImpl
:It's a class that implements the
NewsRepository
interface.The
getArticles()
function returns aFlow
ofPagingData<Article>
.It uses a
Pager
from the Paging library to create a flow of paginated article data.The
Pager
is configured with aPagingConfig
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.
ArticlePagingSource
:It's a class that extends
PagingSource<Int, Article>
, which is responsible for loading data for thePager
.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 aLoadResult.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.
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
:@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.
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.
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:
@Module
:- This annotation indicates that the
AppModule
object provides dependencies to the Hilt dependency injection framework.
- This annotation indicates that the
@InstallIn(ApplicationComponent::class)
:- This annotation specifies that the dependencies provided by
AppModule
should be available for injection in theApplicationComponent
, which is the top-level component for the entire application.
- This annotation specifies that the dependencies provided by
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'screate()
method.
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 theNewsApiService
dependency provided byprovideNewsApiService()
.It creates and returns an instance of
NewsRepositoryImpl
, passing theapiService
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! π