Movies App
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.
App Screenshots
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 theviewmodel
. - 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 functionEachRow
to show the data EachRow
composable is usingcard
to show movie detailsCard
is usingrow
to place elements horizontally which containsimage
andcolumn
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 followingMVVM
architecture. MovieViewModel
is annotated withHiltViewModel
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 fromuseCase
using coroutines then assigning it toMovieState
data class, thus followingclean 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 usingmapper
which will contain data, error message and api states(success, failure and loading). RepositoryModule
providesMovieRepository
instance toMovieUseCase
MovieMapper
instance is provided here by using inject annotation inMovieMapper
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 usingApiService
interface - Here
ApiService
dependency is injected in the primary constructor MovieRepositoryImpl
is implementingMovieRepository
interface and abstract classBaseRepository
which contains suspend func safeApiCallNetworkModule
providesApiService
instance by integrating base url and moshi components- Notice
MovieRepositoryImpl
is using an suspend functionsafeApiCall
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 ofMovies
.Results
itself contains many fields so we have written it insideMovies
.- Both
Movies
andResults
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 ofApiService
interfacegetMovies
suspend fun is used to fetch data from the internet.getMovies
generates a get request with the help of BASE_URL, query and api_keyMovies
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
andRepositoryModule
are used to provideApiService
andMovieRepository
dependency to data layer and domain layer respectively.- we are providing
MovieViewModel
dependency by annotating it withHiltViewModel
- We have annotated
MainActivity
withAndroidEntryPoint
, so that we can inject instances inside it. like — we have have inject viewmodel insideMainActivity
AndroidEntryPoint
annotation make the component class(activity, fragments, views, services, and broadcast receiver) ready for injection.- We annotate the
BaseApplication
class withHiltAndroidApp.
- 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 ofApiService
interfaceRepositoryModule
providesMovieRepository
instance toMovieUseCase
- 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
asandroid:name
inside application tag in manifest file, so that app knows this is our application class.
API Key
- You can generate your api key from tmdb official site.
Let’s keep this article going, clap 👏 and star ⭐ the GitHub repository.