영화/TV 검색 기능
완성하면 아래 앱이 됩니다.
https://play.google.com/store/apps/details?id=com.enigmah2k.movieinfo
Search API를 사용하여 영화와 TV 정보를 검색하는 기능을 구현해 보겠습니다.
아쉬운 점은 영어 검색만 가능합니다.
이미 Movie와 TV 정보를 가져오는 기능을 구현해 놓았으므로 최대한 활용해 보겠습니다.
이미 만들어져 있는 NotificationsFragment 에 검색 기능을 구현하겠습니다.
DashboardFragment 는 나중에 API 사용법을 테스트 할 때 임시로 결과값을 받는 화면으로 활용하겠습니다.
1. Api.kt 에 사용 API 추가
원래는 개별 결과 아이템 Class 를 만들고, 결과 전체를 받는 Class를 먼저 만든 다음 API를 추가했지만,
이미 Movie.kt 와 TV.kt 를 만들었고, GetMoviesResponse.kt 와 GetTVResponse.kt가 만들어져 있는 상태이므로,
search API 만 추가하면 됩니다.
추후 다른 API를 사용할 때 결과값 구성이 다른 형태로 사용하려면 결과 Class를 따로 만들면 됩니다.
Search API 코드입니다.
@GET("search/movie")
fun getSearchMovies(
@Query("api_key") apiKey: String = "348eefabae0631d8003f24551c45a05c",
@Query("page") page : Int,
@Query("query") query : String,
@Query("language") language : String = "ko,en-US"
): Call<GetMoviesResponse>
@GET("search/tv")
fun getSearchTV(
@Query("api_key") apiKey: String = "348eefabae0631d8003f24551c45a05c",
@Query("page") page : Int,
@Query("query") query : String,
@Query("language") language : String = "ko,en-US"
): Call<GetTVResponse>
2. getSearchMovies() 함수 추가
MoviesRepository.kt 에 추가한 API를 호출하는 함수를 추가합니다.
fun getSearchMovies( page: Int = 1, query: String,
onSuccess: (movies: List<Movie>) -> Unit,
onError: () -> Unit ) {
api.getSearchMovies(page = page, query = query)
.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()
}
}
override fun onFailure(call: Call<GetMoviesResponse>, t: Throwable) {
onError.invoke()
}
})
}
차이점은 검색 keyword를 받은 query 파라메터를 추가하였습니다.
나머지는 동일합니다.
3. getSearchTV() 함수 추가
TVRepository.kt 에 추가한 API를 호출하는 함수를 추가합니다.
fun getSearchTV(page: Int = 1, query: String,
onSuccess: (tvlist: List<TV>) -> Unit,
onError: () -> Unit){
api.getSearchTV(page = page, query = query)
.enqueue(object : Callback<GetTVResponse> {
override fun onResponse(
call: Call<GetTVResponse>,
response: Response<GetTVResponse>
) {
if(response.isSuccessful) {
val responseBody = response.body()
if(responseBody != null) {
onSuccess.invoke(responseBody.tvlist)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
override fun onFailure(call: Call<GetTVResponse>, t: Throwable) {
onError.invoke()
}
})
}
Movie와 마찬가지로 차이점은 검색 keyword를 받은 query 파라메터를 추가하였습니다.
나머지는 동일합니다.
4. Layout 구성
fragment_notifications.xml 에 아래와 같이 Layout을 구성합니다.
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="1dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="검색어"
android:textAlignment="center"
android:textSize="20sp" />
<EditText
android:id="@+id/eSearchWord"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:inputType="textPersonName"
android:text="" />
<Button
android:id="@+id/bSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="검색" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/linearLayout2">
<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="Movie"
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="영화 검색 결과" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_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>
<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/linearLayout">
<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="TV"
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="TV 검색 결과" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
</LinearLayout>
키워드 받는 부분을 상단에 배치 하였고,
결과 표시는 기존과 동일하게 RecyclerView로 구성하였습니다.
5. Search 기능 구현
NotificationsFragment.kt 에서 검색어를 받아 Movie 와 TV 정보를 호출하는 기능을 구현합니다.
class NotificationsFragment : Fragment() {
lateinit var root : View
var searchKeyword = ""
private lateinit var searchMovies: RecyclerView
private lateinit var searchMoviesAdapter: MoviesAdapter
private lateinit var searchMoviesLayoutMgr: LinearLayoutManager
private var searchMoviesPage = 1
private lateinit var searchTV: RecyclerView
private lateinit var searchTVAdapter: TVAdapter
private lateinit var searchTVLayoutMgr: LinearLayoutManager
private var searchTVPage = 1
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? ): View? {
root = inflater.inflate(R.layout.fragment_notifications, container, false)
searchMovies = root.findViewById(R.id.search_movies)
searchMoviesLayoutMgr = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
searchMovies.layoutManager = searchMoviesLayoutMgr
searchMoviesAdapter = MoviesAdapter(mutableListOf()) { movie -> showMovieDetails(movie) }
searchMovies.adapter = searchMoviesAdapter
searchTV = root.findViewById(R.id.search_tv)
searchTVLayoutMgr = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
searchTV.layoutManager = searchTVLayoutMgr
searchTVAdapter = TVAdapter(mutableListOf()) { tv -> showTVDetails(tv) }
searchTV.adapter = searchTVAdapter
root.bSearch.setOnClickListener() {
searchKeyword = root.eSearchWord.text.toString()
if(searchKeyword == "") {
Toast.makeText(activity, "input keyword", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, searchKeyword, Toast.LENGTH_SHORT).show()
getSearchMovies()
getPopularTV()
}
}
return root
}
private fun getSearchMovies() {
MoviesRepository.getSearchMovies(
searchMoviesPage,
searchKeyword,
::onSearchMoviesFetched,
::onError
)
}
private fun attachSearchMoviesOnScrollListener() {
searchMovies.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val totalItemCount = searchMoviesLayoutMgr.itemCount
val visibleItemCount = searchMoviesLayoutMgr.childCount
val firstVisibleItem = searchMoviesLayoutMgr.findFirstVisibleItemPosition()
if (firstVisibleItem + visibleItemCount >= totalItemCount / 2) {
searchMovies.removeOnScrollListener(this)
searchMoviesPage++
getSearchMovies()
}
}
})
}
private fun onSearchMoviesFetched(movies: List<Movie>) {
searchMoviesAdapter.appendMovies(movies)
attachSearchMoviesOnScrollListener()
}
private fun getPopularTV() {
TVRepository.getSearchTV(
searchTVPage,
searchKeyword,
::onSearchTVFetched,
::onError
)
}
private fun onSearchTVFetched(tvlist: List<TV>) {
searchTVAdapter.appendTV(tvlist)
attachSearchTVOnScrollListener()
}
private fun attachSearchTVOnScrollListener() {
searchTV.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val totalItemCount = searchTVLayoutMgr.itemCount
val visibleItemCount = searchTVLayoutMgr.childCount
val firstVisibleItem = searchTVLayoutMgr.findFirstVisibleItemPosition()
if (firstVisibleItem + visibleItemCount >= totalItemCount / 2) {
searchTV.removeOnScrollListener(this)
searchTVPage++
getPopularTV()
}
}
})
}
private fun onError() {
Toast.makeText(activity, "error error", Toast.LENGTH_SHORT).show()
}
private fun showMovieDetails(movie: Movie) {
val intent = Intent(activity, MovieDetailsActivity::class.java)
intent.putExtra(MainActivity.MOVIE_BACKDROP, movie.backdrop_path)
intent.putExtra(MainActivity.MOVIE_POSTER, movie.poster_path)
intent.putExtra(MainActivity.MOVIE_TITLE, movie.title)
intent.putExtra(MainActivity.MOVIE_RATING, movie.rating)
intent.putExtra(MainActivity.MOVIE_RELEASE_DATE, movie.releaseDate)
intent.putExtra(MainActivity.MOVIE_OVERVIEW, movie.overview)
startActivity(intent)
}
private fun showTVDetails(tv: TV) {
val intent = Intent(activity, MovieDetailsActivity::class.java)
intent.putExtra(MainActivity.MOVIE_BACKDROP, tv.backdrop_path)
intent.putExtra(MainActivity.MOVIE_POSTER, tv.poster_path)
intent.putExtra(MainActivity.MOVIE_TITLE, tv.name)
intent.putExtra(MainActivity.MOVIE_RATING, tv.rating)
intent.putExtra(MainActivity.MOVIE_RELEASE_DATE, tv.first_air_date)
intent.putExtra(MainActivity.MOVIE_OVERVIEW, tv.overview)
startActivity(intent)
}
}
검색 키워드를 받아 영화 정보와 TV 정보를 조회를 합니다.
6. 구색맞춤
탭 아이콘과 이름을 수정합니다.
String 수정
<string name="title_notifications">Notifications</string> 를 <string name="title_notifications">Search</string> 로 변경합니다.
Icon 추가
File > New > Vector Asset 을 선택하여 search 아이콘을 선택하고,
ic_search_black_24dp 로 Name을 정합니다.
bottom_nav_menu.xml 파일에서
ic_notifications_black_24dp 를 ic_search_black_24dp 로 수정합니다.
실행해 보면 아래와 같이 잘 실행 됩니다.
키워드는 필수 이므로 값을 꼭 기입해야 합니다.
아쉽지만 검색은 영어만 가능합니다.
그런데 한번 검색을 하고 다른 키워드로 검색을 하게 되면 Adapter 정보가 갱신이 되지 않아서 변화가 없습니다.
그래서 Adapter 에 remove 함수를 추가합니다.
MoviesAdapter.kt
fun removeMovies(movies: List<Movie>) {
this.movies.removeAll(movies)
}
TVAdapter.kt
fun removeTV(tvlist: List<TV>) {
this.tvlist.removeAll(tvlist)
}
그리고 NotificationsFragment.kt에 아래 함수를 추가하여 getSearchMovies() 실행전에 실행해 줍니다.
private fun removeData() {
searchMovies.removeAllViews()
searchTV.removeAllViews()
searchMoviesAdapter.removeMovies(searchMoviesAdapter.movies)
searchMoviesAdapter.notifyDataSetChanged()
searchTVAdapter.removeTV(searchTVAdapter.tvlist)
searchTVAdapter.notifyDataSetChanged()
}
새로운 키워드로 검색하면 화면이 변경되는 것을 확인할 수 있습니다.
이것으로 기본 구성은 마치려고 합니다.
다음은 API 사용법을 설명해 보겠습니다.
100%는 아니어도 최대한 많이 살펴볼 예정입니다.
TMDB API 를 활용한 Android 앱 만들기
https://stockant.tistory.com/530
'프로그래밍 > 영화 TMDB API' 카테고리의 다른 글
영화 정보 앱 만들기 - TMDB API 사용법, Getting Started (0) | 2020.10.30 |
---|---|
영화 정보 앱 만들기 - TMDB API 사용, Overview, API 사용법 (1) | 2020.10.29 |
영화 정보 앱 만들기 - TMDB API 사용, TV 카테고리 추가 (0) | 2020.10.26 |
영화 정보 앱 만들기 - TMDB API 사용, TV 정보 popular 추가 (0) | 2020.10.25 |
영화 정보 앱 만들기 - TMDB API 사용, 영화 상세 정보 화면 (0) | 2020.10.24 |