Movies App

abhinesh chandra
7 min readDec 2, 2023

--

  • In this article we will create movies app using clean architecture, MVVM and jetpack compose.
  • It will show a movie list to users. The users can see all the details of the movie in the list
  • When user scrolls more movies are shown to the user

App Architecture

  • This app follows MVVM and clean architecture.
clean architecture & MVVM

App Screenshots

News List

UI Layer

@Composable
fun MovieScreen(
viewModel: MovieViewModel = hiltViewModel()
) {
val res = viewModel.res.value
if (res.isLoading)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Center) {
CircularProgressIndicator()
}

if (res.error.isNotEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Center) {
Text(text = res.error)
}
}

if (res.data.isNotEmpty()) {
LazyColumn {
items(
res.data,
key = {
it.id!!
}
) { res ->
EachRow(res = res)
}
}
}
}

@Composable
private fun EachRow(
res: Movies.Results
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data("${ApiService.IMAGE_URL}${res.poster_path}")
.placeholder(R.drawable.ic_launcher_foreground)
.crossfade(true)
.transformations(CircleCropTransformation())
.build()
),
contentDescription = "",
modifier = Modifier
.size(100.dp)
.padding(10.dp)
)

Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(CenterVertically)
) {
Text(
text = res.original_title!!, style = TextStyle(
fontSize = 16.sp
),
textAlign = TextAlign.Center
)
Text(
text = res.overview!!, style = TextStyle(
fontSize = 12.sp
),
textAlign = TextAlign.Center
)
}
}
}
}

View

  • All the ui logic to show movie list is written in the MovieScreen composable.
  • We have done dependency injection of MovieViewModel in the primary constructor
  • We are first fetching result from the internet using the viewmodel.
  • If the result is in loading state we are showing a circular progress indicator.
  • If the result is in error state we are showing an error text.
  • If the result has some data then we are passing that data to LazyColumn items , which using composable function EachRow to show the data
  • EachRow composable is using card to show movie details
  • Card is using row to place elements horizontally which contains image and column
  • Image is used to show movie poster from result data.
  • Column contains two texts placed vertically. First text is used to show movie title and second text is used to show movie overview from result data.

Presentation Layer

@HiltViewModel
class MovieViewModel @Inject constructor(
private val useCase: MovieUseCase
) : ViewModel() {

private val _res: MutableState<MovieState> = mutableStateOf(MovieState())
val res: State<MovieState> = _res

init {
viewModelScope.launch {
useCase.getMovies()
.doOnSuccess {
_res.value = MovieState(
data = it!!
)
}.doOnFailure {
_res.value = MovieState(
error = it?.message!!
)
}.doOnLoading {
_res.value = MovieState(
isLoading = true
)
}.collect()
}
}
}


data class MovieState(
val data: List<Movies.Results> = emptyList(),
val error: String = "",
val isLoading: Boolean = false
)

ViewModel

  • Here MovieViewModel is providing data to the UI and following MVVM architecture.
  • MovieViewModel is annotated with HiltViewModel so that it can injected wherever needed(MovieScreen)
  • Here useCase or domain layer dependency is injected in the primary constructor.
  • Here MovieViewModel is fetching data from useCase using coroutines then assigning it to MovieState data class, thus following clean architecture.

Domain Layer

class MovieUseCase @Inject constructor(
private val repo: MovieRepository,
private val mapper: MovieMapper
) {

suspend fun getMovies(): Flow<ApiState<List<Movies.Results>?>> {
return repo.getMovies().map { results ->
results.map {
mapper.fromMap(it)
}
}
}
}
  • Here MovieUseCase is acting as a domain layer which contains only business logic and no reference to UI or view
  • Here repository and mapper dependency is injected in primary constructor.
  • It has a suspend fun getMovies which when called from a coroutine will fetch result from repository, transform data into API state using mapper which will contain data, error message and api states(success, failure and loading).
  • RepositoryModule provides MovieRepository instance to MovieUseCase
  • MovieMapper instance is provided here by using inject annotation in MovieMapper class before primary constructor.

Data Layer

class MovieRepositoryImpl @Inject constructor(
private val apiService: ApiService
) : MovieRepository, BaseRepository() {
override suspend fun getMovies(): Flow<ApiState<Movies>> = safeApiCall {
apiService.getMovies()
}
}

Repository

  • Here we are using MovieRepositoryImpl class to fetch data from internet using ApiService interface
  • Here ApiService dependency is injected in the primary constructor
  • MovieRepositoryImpl is implementing MovieRepository interface and abstract class BaseRepository which contains suspend func safeApiCall
  • NetworkModule provides ApiService instance by integrating base url and moshi components
  • Notice MovieRepositoryImpl is using an suspend function safeApiCall to process fetched data.
  • if the request is successful it returns the data my mapping it to ApiState for further processing, and throws exception in case of any errors.
  • Other scenarios it handles are if the request was not sent, if the response failed, if the response was successful with null data and an error or without any error.

Model

data class Movies(
val page: Int?,
val results: List<Results?>?
) {
data class Results(
val id:Long?,
val original_title:String?,
val overview:String?,
val poster_path:String?,
val vote_average:Float?
)
}
  • Above is the Movies data model, It will be used to store response from the internet
  • Results is one of the fields of Movies. Results itself contains many fields so we have written it inside Movies.
  • Both Movies and Results can have many other fields but we have only written the fields which we want to fetch. Like — Movies — total_pages, total_results, Results — release_date, popularity, vote_count etc.

Api Service

interface ApiService {

companion object {
const val BASE_URL = "https://api.themoviedb.org/3/"
const val IMAGE_URL = "https://image.tmdb.org/t/p/w500"
}

@GET("discover/movie?api_key=1234")
suspend fun getMovies(): Response<Movies>

}
  • Retrofit object is created using BASE_URL of ApiService interface
  • getMovies suspend fun is used to fetch data from the internet.
  • getMovies generates a get request with the help of BASE_URL, query and api_key
  • Movies data class will be used to fetch specific fields of response and store it.
  • If you need to pass fields to get the response you can add it in the primary constructor of getMovies .

Dependency Injection

  • we are using dagger-hilt to provide dependency injection inside the app
  • NetworkModule and RepositoryModule are used to provide ApiService and MovieRepository dependency to data layer and domain layer respectively.
  • we are providing MovieViewModel dependency by annotating it with HiltViewModel
  • We have annotated MainActivity with AndroidEntryPoint, so that we can inject instances inside it. like — we have have inject viewmodel inside MainActivity
  • AndroidEntryPoint annotation make the component class(activity, fragments, views, services, and broadcast receiver) ready for injection.
  • We annotate the BaseApplication class with HiltAndroidApp.
  • This will make this class trigger Hilt’s code generation which will have the base class for our application and it acts as the application-level dependency container.
//NetworkModule object
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideMoshi(): Moshi = Moshi
.Builder()
.run {
add(KotlinJsonAdapterFactory())
build()
}

@Provides
@Singleton
fun provideApiService(moshi: Moshi): ApiService =
Retrofit.Builder()
.run {
baseUrl(ApiService.BASE_URL)
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}.create(ApiService::class.java)
}

//MovieRepository abstract class
@Module
@InstallIn(ViewModelComponent::class)
abstract class RepositoryModule {

@Binds
abstract fun provideMovieRepository(
repo: MovieRepositoryImpl
): MovieRepository
}
  • Retrofit object is created using BASE_URL of ApiService interface
  • RepositoryModule provides MovieRepository instance to MovieUseCase
  • Dagger 2 is a compile-time android dependency injection framework
  • basics annotations used in dagger2 are
  • Module — This annotation is used over the class which is used to construct objects and provide the dependencies.
  • Inject — This is used over the fields, constructor, or method and indicate that dependencies are requested. Use constructor injection with Inject to add types to the Dagger graph whenever it’s possible. when its not use binds and provides.
  • Singleton — This is used to indicate only a single instance of dependency object is created.
  • Binds — Use Binds to tell Dagger which implementation an interface should have.
  • Provides — Use Provides to tell Dagger how to provide classes that your project doesn’t own.
  • So the execution flow in a dagger app would be:
    - The android app starts and the MyApplication class builds the graph, parsing the Module annotated classes and keeping an instance of it.
    - Then, the classes declared in the module can access its objects just injecting themselves in the object graph. Gradle then will evaluate their Inject annotations and perform the dependency injections.

Pre-Configuration

  • we need to add internet permission in the manifest file so that app can connect to the internet.
  • we need to add BaseApplication as android:name inside application tag in manifest file, so that app knows this is our application class.

API Key

Let’s keep this article going, clap 👏 and star the GitHub repository.

--

--