Make A Movies App in Android
Make A Movies App in Android
Make A Movies App in Android
Movies App
in
Android
Arth Limchiu
Make a Movies App Using TMDb API
Arth Limchiu
Contents
Project Specifications 3
App Specifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Feature #1: As a user, I want to see a list of movies so that I can browse through different movies. . . . . . . . . . . . . . . 3
Feature #2: As a user, I want the list of movies to be categorized by Popular, Top Rated and Upcoming so that I can easily
look for movies based on the them. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Feature #3: As a user, I want to be able to see the details of a movie so that I will know more about the movie. . . . . . . 3
Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Feature #1: As a user, I want to see a list of movies so that I can browse through different movies 6
Create a New Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Import Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Project Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Fetch Movies from TMDb API Using Retrofit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Callbacks Using Kotlin’s Higher-Order Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Create a Horizonal List and Load Images Using Glide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Feature #2: As a user, I want the list of movies to be categorized by Popular, Top Rated and Upcoming 20
Show Top Rated Movies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Show Upcoming Movies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
What’s Next? 34
Awesome Feature Ideas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Extended Version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Thank you . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2
Project Specifications
The Movie Database (TMDb) is a community built movie and TV database. Their database contains a lot of movies, tv shows, artists
and many more. They also provide developers access to their database for FREE!
Using their API is a great way to get started with networking in Android. Let’s first know the details of the app that we’re going to make.
App Specifications
Language: Kotlin
Architecture: None
The main third-party libraries that we’re going to use are:
• Retrofit - to handle our network related features
• Gson - parsing JSON objects from TMDb API
• Glide - image loading library
Features
Feature #1: As a user, I want to see a list of movies so that I can browse through different movies.
Tasks:
1. It should be a horizontal list.
2. Each item should display an image of the movie.
3. Each item should have rounded corners.
Feature #2: As a user, I want the list of movies to be categorized by Popular, Top Rated and Upcoming so that I can easily look for
movies based on the them.
Tasks:
1. Popular movies should be the first row.
2. Top Rated movies should be the second row.
3. Upcoming movies should be the last row.
4. Popular movies row should have a “Popular” label above it.
5. Top Rated movies row should have a “Top Rated” label above it.
6. Upcoming movies row should have an “Upcoming” label above it.
Feature #3: As a user, I want to be able to see the details of a movie so that I will know more about the movie.
Tasks:
1. It should show the cover photo of the movie.
2. It should show the poster of the movie.
3. It should show the title of the movie.
4. It should show the summary of the movie.
5. It should show the rating of the movie.
6. It should show the release date of the movie.
Scope
Now that we’ve covered the features that we’re going to implement, let’s move on to the features that we won’t cover:
1. Offline Support
2. Bookmark/Watchlist Feature
3. TV Shows/Series
4. Any MV* Architecture
5. Dagger
6. RxJava
3
Getting a TMDb API Key
Create a TMDb Account
1. Go to The Movie Database (TMDb) Website.
2. Click “SIGN UP” at the top right corner.
4. After you sign up, check your email and click “ACTIVATE MY ACCOUNT” to verify.
4
2. In the sidebar, select API.
3. Under Request an API key, click “click here”.
4. Select “Developer” and scroll down to the bottom and click Accept.
5
6. After you’ve completed the details, you should see your API key under API Key (v3 Auth).
7. Open the url under Example API request and you should receive a JSON response.
You can save your API key anywhere that you want or you can come back to it later by going to Settings -> API.
Now that this stuff is out of the way, let’s start coding!
Feature #1: As a user, I want to see a list of movies so that I can browse through different
movies
Create a New Project
1. Open Android Studio and start a new project.
2. Select Empty Activity.
6
3. You can name the app whatever you want.
4. For this project, we set our minimum API level to 21.
5. Make sure androidx.* artifacts is checked and click Finish.
Import Dependencies
After it’s done building, open your app-level build.gradle and add these dependencies:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
7
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.arthlimchiu.mymovies"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Support
// https://developer.android.com/jetpack/androidx/versions
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
// Retrofit
// https://github.com/square/retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
// Gson
// https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.8.5'
// Glide
// https://github.com/bumptech/glide
implementation 'com.github.bumptech.glide:glide:4.10.0'
kapt 'com.github.bumptech.glide:compiler:4.10.0'
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
As of this writing, those are the latest versions. Make sure to check the latest version of each dependency from the links provided.
Click Sync Now at the top right corner and we’re done setting up our project.
8
Project Resources
Open your strings.xml and add these strings.
<resources>
<string name="app_name">MyMovies</string>
<string name="popular">Popular</string>
<string name="most_popular_movies">Most popular movies</string>
<string name="error_fetch_movies">Please check your internet connection</string>
<string name="top_rated">Top Rated</string>
<string name="highest_rated_movies">Highest rated movies of all time</string>
<string name="upcoming">Upcoming</string>
<string name="stay_updated">Stay updated with the latest movies</string>
</resources>
<application
...
</application>
</manifest>
9
data class GetMoviesResponse(
@SerializedName("page") val page: Int,
@SerializedName("results") val movies: List<Movie>,
@SerializedName("total_pages") val pages: Int
)
@GET("movie/popular")
fun getPopularMovies(
@Query("api_key") apiKey: String = "YOUR_API_KEY_HERE",
@Query("page") page: Int
): Call<GetMoviesResponse>
}
Be sure to replace YOUR_API_KEY_HERE with your own API key that you generated from the previous chapter.
5. Create a new object called MoviesRepository.
object MoviesRepository {
init {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(GsonConverterFactory.create())
.build()
api = retrofit.create(Api::class.java)
}
}
Take note that it uses the object keyword of Kotlin which is an easy way to declare a Singleton in Kotlin.
Using the init block of Kotlin which is called when an instance is initialized, we instantiate a Retrofit instance using it’s builder. Then,
instantiate an instance of Api using the Retrofit instance.
6. Add a new method in MoviesRepository called getPopularMovies().
object MoviesRepository {
...
if (responseBody != null) {
Log.d("Repository", "Movies: ${responseBody.movies}")
} else {
Log.d("Repository", "Failed to get response")
}
}
}
10
override fun onFailure(call: Call<GetMoviesResponse>, t: Throwable) {
Log.e("Repository", "onFailure", t)
}
})
}
}
For now, we default the page to 1. We will deal with pagination later on.
First of, we execute api.getPopularMovies() asynchronously using the .enqueue() method. Then, we log the movies if the
response was successful.
7. Open your MainActivity and call the getPopularMovies() method of MoviesRepository.
class MainActivity : AppCompatActivity() {
MoviesRepository.getPopularMovies()
}
}
It should log the movies list from the response. Type Repository to easily find the log.
void onError();
}
In Kotlin, we no longer need to do that because we can pass a function to another function and we call it in Kotlin - higher-order
functions. Let’s take a closer look.
1. Open your MoviesRepository and let’s refactor getPopularMovies().
fun getPopularMovies(
page: Int = 1,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit
) {
...
}
onSuccess is a parameter that is a function that doesn’t return anything -> Unit but it accepts a list of movies.
onError is the same with onSuccess but it doesn’t accept anything. All we need to is to just invoke this method.
How do we use it?
2. In your getPopularMovies() method, remove the logs and replace it invocations of the functions.
11
fun getPopularMovies(
page: Int = 1,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit
) {
api.getPopularMovies(page = page)
.enqueue(object : Callback<GetMoviesResponse> {
override fun onResponse(
call: Call<GetMoviesResponse>,
response: Response<GetMoviesResponse>
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
onSuccess.invoke(responseBody.movies)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
invoke() is how you execute a higher-order function. Take note that it will vary depending if the higher-order function has parame-
ter(s) or not. You can see the difference by comparing onSuccess.invoke(responseBody.movies) and onError.invoke().
onSuccess: (movies: List<Movie>) -> Unit is to onSuccess.invoke(responseBody.movies).
onError: () -> Unit is to onError.invoke().
3. Open your MainActivity and let’s pass the functions needed by getPopularMovies().
class MainActivity : AppCompatActivity() {
MoviesRepository.getPopularMovies(
onSuccess = ::onPopularMoviesFetched,
onError = ::onError
)
}
The :: colon operator is used to create a class or function reference. An alternative is doing this:
12
MoviesRepository.getPopularMovies(
onSuccess = { movies ->
Log.d("MainActivity", "Movies: $movies")
},
onError = {
Toast.makeText(this, getString(R.string.error_fetch_movies), Toast.LENGTH_SHORT).show()
}
)
But using the :: operator approach just make things much cleaner. However, I leave it to your preference on which approach you
want.
4. Run the app, check your Logcat and be sure to type MainActivity in the search bar to filter the logs. You should see a log that is
the same as the previous section.
If you’d like to know more about Kotlin’s higher-order functions. Check out the documentation.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/popular"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/most_popular_movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/popular_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
</LinearLayout>
13
android:layout_height="172dp"
android:layout_marginEnd="8dp"
app:cardCornerRadius="4dp">
<ImageView
android:id="@+id/item_movie_poster"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
.load("https://image.tmdb.org/t/p/w342/<poster_url>") is how you fetch a poster of a movie from TMDb. You can learn
more about fetching images from TMDb here.
Available poster sizes are: - w92 - w154 - w185 - w342 - w500 - w780 - original
You can go for original if you want to have the highest quality image but it will take time to load. A size of w342 should be enough for
most screens.
4. Open your MainActivity and instantiate your RecyclerView and Adapter.
class MainActivity : AppCompatActivity() {
14
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
popularMovies = findViewById(R.id.popular_movies)
popularMovies.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
popularMoviesAdapter = MoviesAdapter(listOf())
popularMovies.adapter = popularMoviesAdapter
MoviesRepository.getPopularMovies(
onSuccess = ::onPopularMoviesFetched,
onError = ::onError
)
}
...
}
To make a horizontal list in RecyclerView, just provide the LinearLayoutManager with an orientation and a boolean flag that reverses
the list or not.
popularMovies.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
We removed the log in onPopularMoviesFetched() and replaced it by updating the movies inside popularMoviesAdapter.
private fun onPopularMoviesFetched(movies: List<Movie>) {
popularMoviesAdapter.updateMovies(movies)
}
5. Run the app. Scroll through the list and take a moment to enjoy your work because you deserve it.
15
As of this writing, these are the most popular movies. We might not have the same list by the time you’ve finished this section.
Pagination
While you’re scrolling through the list, you’ll notice that you only see a limited number of movies. Specifically, you only see 20 movies.
Why is that?
16
TMDb has thousands and thousands of movies in their database. Imagine sending all those data into one API call. It would take a lot
of time to receive the response and also the size of the response would be super big which is not ideal and efficient. Especially, when
most of the time the user won’t scroll all of it. That’s why they paginate their movies API.
Open Api and you’ll see a page parameter.
interface Api {
@GET("movie/popular")
fun getPopularMovies(
@Query("api_key") apiKey: String = "YOUR_API_KEY_HERE",
@Query("page") page: Int
): Call<GetMoviesResponse>
}
fun getPopularMovies(
page: Int = 1,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit
) {
...
}
In this section, our goal is to fetch the next page of movies when the user scrolls halfway through our list.
1. Open your MoviesAdapter, change the type of the movies variable to MutableList, and rename your updateMovies()
method to appendMovies().
class MoviesAdapter(
private var movies: MutableList<Movie>
) : RecyclerView.Adapter<MoviesAdapter.MovieViewHolder>() {
...
...
}
We changed the type of the movies variable to MutableList because we now have a dynamic list of movies.
class MoviesAdapter(
private var movies: MutableList<Movie>
) : ...
Instead of using notifyDataSetChanged(), we use notifyItemRangeInserted() because we don’t want to refresh the whole list.
We just want to notify that there are new items added from this start and end positions.
fun appendMovies(movies: List<Movie>) {
this.movies.addAll(movies)
notifyItemRangeInserted(
this.movies.size,
movies.size - 1
)
}
17
2. Open your MainActivity and declare a member variable for our page and LinearLayoutManager.
class MainActivity : AppCompatActivity() {
...
}
popularMovies = findViewById(R.id.popular_movies)
popularMoviesLayoutMgr = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
popularMovies.layoutManager = popularMoviesLayoutMgr
popularMoviesAdapter = MoviesAdapter(mutableListOf())
popularMovies.adapter = popularMoviesAdapter
MoviesRepository.getPopularMovies(
popularMoviesPage,
::onPopularMoviesFetched,
::onError
)
}
getPopularMovies()
}
18
val visibleItemCount = popularMoviesLayoutMgr.childCount
val firstVisibleItem = popularMoviesLayoutMgr.findFirstVisibleItemPosition()
Let’s go over the code bit by bit. The first three variables are:
• totalItemCount - the total number of movies inside our popularMoviesAdapter. This will keep increasing the more we call
popularMoviesAdapter.appendMovies().
• visibleItemCount - the current number of child views attached to the RecyclerView that are currently being recycled over
and over again. The value of this variable for common screen sizes will range roughly around 4-5 which are 3 visible views, +1
left view that’s not seen yet and +1 right view that’s not seen yet also. The value will be higher if you have a bigger screen.
• firstVisibleItem - is the position of the leftmost visible item in our list.
The condition will be true if the user has scrolled past halfway plus a buffered value of visibleItemCount.
if (firstVisibleItem + visibleItemCount >= totalItemCount / 2) {
...
}
After condition is met, we disable the scroll listener since we only want this code to run once. Next, we increment popularMoviesPage
and then call getPopularMovies().
if (firstVisibleItem + visibleItemCount >= totalItemCount / 2) {
popularMovies.removeOnScrollListener(this)
popularMoviesPage++
getPopularMovies()
}
7. In onPopularMoviesFetched() method, call the newly renamed appendMovies() method and reattach the OnScrollListener
again.
private fun onPopularMoviesFetched(movies: List<Movie>) {
popularMoviesAdapter.appendMovies(movies)
attachPopularMoviesOnScrollListener()
}
When the user has scrolled past halfway, detach the OnScrollListener and then after the new movies have been fetched reattach it
again. The complete MainActivity code should like this:
class MainActivity : AppCompatActivity() {
popularMovies = findViewById(R.id.popular_movies)
popularMoviesLayoutMgr = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
19
popularMovies.layoutManager = popularMoviesLayoutMgr
popularMoviesAdapter = MoviesAdapter(mutableListOf())
popularMovies.adapter = popularMoviesAdapter
getPopularMovies()
}
...
}
8. Run the app. Keep scrolling and you’ll notice that it now fetches a new batch of movies. Good job!
If you want to know why we detach and reattach a scroll listener, comment out popularMovies.removeOnScrollListener(this)
and replace it with Log.d("MainActivity", "Fetching movies") and you’ll see how many times you’re fetching the
movies.
Feature #2: As a user, I want the list of movies to be categorized by Popular, Top Rated and
Upcoming
This chapter will be quite easy. We will just replicate what we did for popular movies in the previous chapter for top rated and
upcoming movies.
...
@GET("movie/top_rated")
20
fun getTopRatedMovies(
@Query("api_key") apiKey: String = "YOUR_API_KEY_HERE",
@Query("page") page: Int
): Call<GetMoviesResponse>
}
As you can see, getPopularMovies() and getTopRatedMovies() are basically the same. The only difference is that
getTopRatedMovies() has a different endpoint - @GET("movie/top_rated").
2. Open your MoviesRepository and add a new method called getTopRatedMovies().
object MoviesRepository {
...
fun getTopRatedMovies(
page: Int = 1,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit
) {
api.getTopRatedMovies(page = page)
.enqueue(object : Callback<GetMoviesResponse> {
override fun onResponse(
call: Call<GetMoviesResponse>,
response: Response<GetMoviesResponse>
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
onSuccess.invoke(responseBody.movies)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
3. Open your activity_main.xml and add a new RecyclerView for top rated movies.
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
21
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/top_rated"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/highest_rated_movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/top_rated_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
Be sure to wrap your LinearLayout with NestedScrollView. You will use NestedScrollView if it has scrollable views as its children such
as a RecyclerView as it will take care of handling nested scrolling for you. If you don’t have scrollable views as children, a normal
ScrollView will do.
4. Open your MainActivity and populate your top rated movies list.
class MainActivity : AppCompatActivity() {
...
topRatedMovies = findViewById(R.id.top_rated_movies)
topRatedMoviesLayoutMgr = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
topRatedMovies.layoutManager = topRatedMoviesLayoutMgr
topRatedMoviesAdapter = MoviesAdapter(mutableListOf())
topRatedMovies.adapter = topRatedMoviesAdapter
getPopularMovies()
getTopRatedMovies()
}
22
...
...
}
5. Run the app and now you can see a list of top rated movies!
23
Show Upcoming Movies
Next up is to show a list of upcoming movies and you’ve probably guessed it, the process will be the same. I suggest doing it on your
own first by referencing what you did in the previous section. Then, come back to this section to compare and double check your
code.
24
1. Open your Api interface and add a new endpoint for upcoming movies.
interface Api {
...
@GET("movie/upcoming")
fun getUpcomingMovies(
@Query("api_key") apiKey: String = "YOUR_API_KEY_HERE",
@Query("page") page: Int
): Call<GetMoviesResponse>
}
...
fun getUpcomingMovies(
page: Int = 1,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit
) {
api.getUpcomingMovies(page = page)
.enqueue(object : Callback<GetMoviesResponse> {
override fun onResponse(
call: Call<GetMoviesResponse>,
response: Response<GetMoviesResponse>
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
onSuccess.invoke(responseBody.movies)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
3. Open your activity_main.xml and add a new RecyclerView for upcoming movies.
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
25
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/upcoming"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/stay_updated" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/upcoming_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
...
upcomingMovies = findViewById(R.id.upcoming_movies)
upcomingMoviesLayoutMgr = LinearLayoutManager(
this,
LinearLayoutManager.HORIZONTAL,
false
)
upcomingMovies.layoutManager = upcomingMoviesLayoutMgr
upcomingMoviesAdapter = MoviesAdapter(mutableListOf())
upcomingMovies.adapter = upcomingMoviesAdapter
getPopularMovies()
getTopRatedMovies()
getUpcomingMovies()
26
}
...
...
}
5. Run the app and enjoy scrolling through the list of different movies.
27
Before we proceed to the next chapter, I want you to pause, take a moment, and realize you’ve just made a fully functional app! Gone
were the days where you were just stuck in tutorials after tutorials without producing something tangible and functional that users
can actually use.
28
Feature #3: As a user, I want to be able to see the details of a movie
Movie Details Screen
1. Right click mymovies package, select New -> Activity -> Empty Activity.
2. Enter the activity name - MovieDetailsActivity.
3. Open activity_movie_details.xml and let’s build our UI.
<androidx.core.widget.NestedScrollView 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:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/movie_backdrop"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/backdrop_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/movie_poster_card"
android:layout_width="128dp"
android:layout_height="172dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
app:cardCornerRadius="4dp"
app:layout_constraintBottom_toBottomOf="@+id/backdrop_guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backdrop_guideline">
<ImageView
android:id="@+id/movie_poster"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/backdrop_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.4" />
<TextView
android:id="@+id/movie_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textColor="@android:color/white"
android:textSize="18sp"
29
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/movie_poster_card"
app:layout_constraintTop_toBottomOf="@+id/backdrop_guideline" />
<TextView
android:id="@+id/movie_release_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#757575"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="@+id/movie_title"
app:layout_constraintTop_toBottomOf="@+id/movie_title" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/movie_poster_title_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="movie_rating,movie_release_date" />
<TextView
android:id="@+id/movie_overview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/movie_poster_title_barrier" />
<RatingBar
android:id="@+id/movie_rating"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@+id/movie_poster_card"
app:layout_constraintStart_toStartOf="@+id/movie_poster_card"
app:layout_constraintTop_toBottomOf="@+id/movie_poster_card" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
backdrop = findViewById(R.id.movie_backdrop)
30
poster = findViewById(R.id.movie_poster)
title = findViewById(R.id.movie_title)
rating = findViewById(R.id.movie_rating)
releaseDate = findViewById(R.id.movie_release_date)
overview = findViewById(R.id.movie_overview)
}
}
These variables will be used as keys when we pass intent extras to MovieDetailsActivity.
6. Use the keys above to populate the movie’s details.
class MovieDetailsActivity : AppCompatActivity() {
...
if (extras != null) {
populateDetails(extras)
} else {
finish()
}
}
31
• w300
• w780
• w1280
• original
2. Open your MoviesAdapter and add a new parameter in the constructor which is a higher-order function that will be called
when a movie is clicked.
class MoviesAdapter(
private var movies: MutableList<Movie>,
private val onMovieClick: (movie: Movie) -> Unit
) : ...
...
4. Open your MainActivity and pass a higher-order function to your adapters which calls the showMovieDetails() method that
you’ve just created.
class MainActivity : AppCompatActivity() {
...
...
32
topRatedMoviesAdapter = MoviesAdapter(mutableListOf()) { movie -> showMovieDetails(movie) }
...
...
upcomingMoviesAdapter = MoviesAdapter(mutableListOf()) { movie -> showMovieDetails(movie) }
...
}
...
}
5. Run the app, tap any movie and you should see something like this:
Congratulations! You have just made a full-blown and portfolio-worthy Android app that you can put on Google Play Store.
In the next chapter, we’ll explore some feature ideas that you can do next to even improve this version.
33
What’s Next?
The hardest part is to start. The second hardest part is to finish what you’ve started.
Before you read this chapter, I want you to reward yourself first. Order some pizza, play a game, eat out with your friends, or watch
that tv series. Whatever it is, be sure that you reward yourself for this great achievement. You deserve it. After you do that, then come
back to this chapter.
Extended Version
The extended version of this book will cover everything what is covered in this book plus:
1. Offline support using Room
2. Bookmarks or watchlist feature
3. TV shows screen
4. Refactor the app to use MVVM architecture
5. Refactor the app to use Dagger
If you want to get the extended version, order it here.
Thank you
If you were able to reach this section, that means you’re on your way to becoming an Android developer. Thank you so much for
reading and I wish you all the best in life and career!
If you want to learn more about Android development, visit my website.
If you want to get the extended version, order it here.
34