I wanted to include a search bar for finding movies by name, but in the way I failed after trying 2 kinds of search bar. First I made the one on the app bar. After that one failed, I made another one under the app bar in frame layout. In both, when I typed the text inside them then pressed Enter button or clicked the search icon, nothing happened. The movie list didn't change. There's only the loading progress indicator.
Here's MovieListActivity.kt the activity where code blocks for those two search bars belong:
class MovieListActivity : AppCompatActivity() {
private var _binding: ActivityMovieListBinding? = null
private var adapter: MovieListAdapter? = null
private var layoutManager : LayoutManager? = null
private lateinit var viewModel: MovieListViewModel
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMovieListBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this)[MovieListViewModel::class.java]
viewModel.getMovieList()
showMovieList()
showMovieGenres()
setupRecyclerView()
adapter?.clickListener(object : MovieOnClickListener {
override fun onClick(movie: Movie, genres: String) {
val intent = Intent(
this@MovieListActivity,
MovieDetailActivity::class.java)
intent.putExtra(MovieDetailActivity.MOVIE, movie)
intent.putExtra(MovieDetailActivity.GENRES, genres)
startActivity(intent)
}
})
}
/**making the menu*/
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_main, menu)
return true
}
/**switching between pages*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.action_search -> {
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager
val searchItem : MenuItem = item
val searchView = searchItem.actionView as SearchView
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView.queryHint = resources.getString(R.string.search_hint)
searchItem.setOnActionExpandListener(object: MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem): Boolean { return true }
override fun onMenuItemActionCollapse(p0: MenuItem): Boolean {
viewModel.getMovieList()
return true
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.getMovieByNameList(query.toString())
searchView.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean { return false }
})
return true
}
R.id.action_switch_list -> {
val intent = Intent(
this@MovieListActivity,
GenreListActivity::class.java
)
startActivity(intent)
return true
}
else -> return true
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun showMovieList(){
viewModel.response.observe(this){
if (it != null){
when(it){
is RequestState.Loading -> showLoading()
is RequestState.Success -> {
hideLoading()
it.data?.results?.let {
data -> adapter?.differ?.submitList(data.toList())
}
}
is RequestState.Error -> {
hideLoading()
Toast.makeText(this,it.message, Toast.LENGTH_SHORT).show()
}
}
}
}
}
private fun showMovieGenres(){
viewModel.getMovieGenres().observe(this){
if (it != null){
when(it){
is RequestState.Loading -> {}
is RequestState.Success -> it.data.genres?.let { data -> adapter?.setGenres(data) }
is RequestState.Error -> Toast.makeText(this,it.message, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun setupRecyclerView(){
adapter = MovieListAdapter()
layoutManager = GridLayoutManager(this, 2)
binding.apply {
movieList.adapter = adapter
movieList.layoutManager = layoutManager
movieList.addOnScrollListener(scrollListener)
iconSearch.setOnClickListener { searchMovie() }
editInputLayout.setOnKeyListener { _, i, keyEvent ->
if (keyEvent.action == KeyEvent.ACTION_DOWN && i == KeyEvent.KEYCODE_ENTER) {
searchMovie()
showLoading()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
}
}
private val scrollListener = object : OnScrollListener(){
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(!recyclerView.canScrollVertically(1)){
viewModel.getMovieList()
}
}
}
private fun searchMovie() {
binding.apply {
val query = editInputLayout.text.toString()
if (query.isNotEmpty()) {
showLoading()
viewModel.getMovieByNameList(query)
}
}
}
private fun showLoading(){ binding.loading.show() }
private fun hideLoading(){ binding.loading.hide() }
}
I searched some solution for this, some of them told me to modify the AndroidManifest.xml, so it has meta-data with name "android.app.default_searchable" and resource searchable.xml. I've made the searchable.xml too. In the end it became like this.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nameful7"
tools:targetApi="31" >
<activity
android:name=".page.movie.detail.MovieDetailActivity"
android:exported="false" />
<activity
android:name=".page.MovieSearch"
android:exported="false" />
<activity
android:name=".page.movie.list.MovieListActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".page.genre.GenreListActivity"
android:exported="false" />
<activity
android:name=".page.movie.list.MovieByGenreListActivity"
android:exported="false" />
<activity
android:name=".page.movie.review.ReviewListActivity"
android:exported="false" />
<activity
android:name=".page.Splash"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:resource="@xml/searchable" />
</activity>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>
searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_name"
android:hint="@string/search_hint" />
I started wondering if the problem is neither in the activity nor manifest and searchable, but inside some classes related to the activity.
MovieListViewModel.kt
class MovieListViewModel : ViewModel() {
private val repository : GeneralRepository = GeneralRepository()
private var page = 1
private var listResponse: MovieListResponse? = null
private var _response = MutableLiveData<RequestState<MovieListResponse?>>()
var response:LiveData<RequestState<MovieListResponse?>> = _response
fun getMovieList(){
viewModelScope.launch {
_response.postValue(RequestState.Loading)
_response.postValue(handleMovieListResponse(repository.getMovies(page)))
}
}
fun getMovieByNameList(query: String){
viewModelScope.launch {
_response.postValue(RequestState.Loading)
_response.postValue(handleMovieListResponse(repository.getMoviesByName(page,query)))
}
}
fun getMovieGenres() : LiveData<RequestState<GenreListResponse>> = liveData {
emit(RequestState.Loading)
try {
emit(RequestState.Success(repository.getGenres().body()!!))
}catch (e: HttpException){
e.response()?.errorBody()?.string()?.let { RequestState.Error(it) }?.let { emit(it) }
}
}
private fun handleMovieListResponse(
response: Response<MovieListResponse>
): RequestState<MovieListResponse?> {
return if(response.isSuccessful){
response.body()?.let {
page++
if (listResponse == null)
listResponse = it
else {
val moviesOld = listResponse?.results
val moviesNew = it.results
moviesOld?.addAll(moviesNew)
}
}
RequestState.Success(listResponse ?: response.body())
}else RequestState.Error(
try{
response.errorBody()?.string()?.let{
JSONObject(it).get("status_message")
}
}catch (e:JSONException){
e.localizedMessage
}as String
)
}
}
GeneralRepository.kt
class GeneralRepository {
private val client = ApiConfig.getApiService()
suspend fun getMovies(page:Int) =
client.getMovies(BuildConfig.API_KEY, page)
suspend fun getMoviesByGenre(page: Int, withGenres: String) =
client.getMoviesByGenre(BuildConfig.API_KEY, page, withGenres)
suspend fun getMoviesByName(page: Int, query: String) =
client.getMoviesByName(BuildConfig.API_KEY,page,query)
suspend fun getGenres() =
client.getGenres(BuildConfig.API_KEY)
suspend fun getMovieTrailers(movieId: Int) =
client.getMovieTrailers(movieId,BuildConfig.API_KEY)
suspend fun getMovieReviews(movieId: Int, page: Int) =
client.getMovieReviews(movieId,BuildConfig.API_KEY,page)
}
ApiService.kt
interface ApiService {
@GET("movie/popular")
suspend fun getMovies(
@Query("api_key") key: String?,
@Query("page") page: Int?
): Response<MovieListResponse>
@GET("discover/movie")
suspend fun getMoviesByGenre(
@Query("api_key") key: String?,
@Query("page") page: Int?,
@Query("with_genres") withGenres: String?
): Response<MovieListResponse>
@GET("search/movie")
suspend fun getMoviesByName(
@Query("api_key") key: String?,
@Query("page") page: Int?,
@Query("query") query: String?,
): Response<MovieListResponse>
@GET("genre/movie/list")
suspend fun getGenres(
@Query("api_key") key: String?,
): Response<GenreListResponse>
@GET("movie/{movie_id}/videos")
suspend fun getMovieTrailers(
@Path("movie_id") movieId: Int?,
@Query("api_key") key: String?,
): Response<VideoListResponse>
@GET("movie/{movie_id}/reviews")
suspend fun getMovieReviews(
@Path("movie_id") movieId: Int?,
@Query("api_key") key: String?,
@Query("page") page: Int?
): Response<ReviewListResponse>
}
But I'm not sure what's wrong with them.