add the search features

develop
Jordan ARTZET 2 years ago
parent 0fd619f41b
commit 95750999e5

@ -1,10 +1,8 @@
package fr.iut.pm.movieapplication.api
import fr.iut.pm.movieapplication.api.dtos.MovieDTO
import fr.iut.pm.movieapplication.api.dtos.MovieDetailsDTO
import fr.iut.pm.movieapplication.api.dtos.PopularDTO
import fr.iut.pm.movieapplication.api.dtos.ResultDTO
import fr.iut.pm.movieapplication.utils.Constants.Companion.API_KEY
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
@ -14,16 +12,16 @@ interface MovieApplicationAPI {
// Movie
@GET("movie/popular")
suspend fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getPopularMovies(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("movie/now_playing")
suspend fun getNowPlayingMovies(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getNowPlayingMovies(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("movie/upcoming")
suspend fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getUpcomingMovies(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("movie/top_rated")
suspend fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getTopRatedMovies(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
// Movie details
@GET("movie/{movie_id}")
@ -32,18 +30,26 @@ interface MovieApplicationAPI {
// TvShow
@GET("tv/popular")
suspend fun getPopularTvShows(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getPopularTvShows(@Query("api_key") apiKey : String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("tv/airing_today")
suspend fun getAiringTodayTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getAiringTodayTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("tv/on_the_air")
suspend fun getTvOnTheAirTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getTvOnTheAirTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("tv/top_rated")
suspend fun getTopRatedTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<PopularDTO>
suspend fun getTopRatedTvShows(@Query("api_key") apiKey: String = API_KEY, @Query("language") language : String = "fr", @Query("page") page : Int = 1) : Response<ResultDTO>
@GET("trending/{media_type}/{time_window}")
suspend fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Response<PopularDTO>
suspend fun getTrending(@Path("media_type") mediaType : String = "all", @Path("time_window") timeWindow : String = "day", @Query("api_key") apiKey: String = API_KEY ) : Response<ResultDTO>
@GET("search/multi")
suspend fun searchMedia(
@Query("query") query: String,
@Query("page") page: Int = 1,
@Query("api_key") apiKey: String = API_KEY,
@Query("language") language: String = "fr",
@Query("include_adult") includeAdult: Boolean = true
): Response<ResultDTO>
@GET("tv/{tv_id}")
fun getShowDetails(@Path("tv_id") tvId : Int, @Query("api_key") apiKey: String = API_KEY)

@ -22,16 +22,16 @@ open class MediaResultDTO(
@Json(name = "original_title")
val originalTitle : String? = null,
@Json(name = "original_language")
val originalLanguage : String,
val originalLanguage : String? = null,
val title : String? = null,
@Json(name = "backdrop_path")
val backdropPath : String?,
val popularity : Double,
val popularity : Double?,
@Json(name = "vote_count")
val voteCount : Int,
val voteCount : Int?,
//val video : Boolean?,
@Json(name = "vote_average")
val voteAverage : Double,
val voteAverage : Double?,
val name: String? = null,
@Json(name = "original_name")
val originalName : String? = null,

@ -5,7 +5,7 @@ import com.squareup.moshi.Json
class PopularDTO(
class ResultDTO(
@Json(name = "page")
val page : Int,
@Json(name = "results")

@ -1,4 +1,4 @@
package fr.iut.pm.movieapplication.repository
package fr.iut.pm.movieapplication.repository.api
import android.util.Log
import fr.iut.pm.movieapplication.api.RetrofitInstance
@ -68,4 +68,25 @@ class MediaRepository {
else Log.d("ERROR FAILED", response.message())
listMediaResult
}
suspend fun search(query : String, Page : Int = 1) : List<MediaResult> = withContext(Dispatchers.IO) {
val listMediaResult : MutableList<MediaResult> = mutableListOf()
val response = RetrofitInstance.api.searchMedia(query, Page)
if(response.isSuccessful) {
val listMediaResultDTO = response.body()?.results
Log.d("Response",response.body().toString())
listMediaResultDTO?.forEach {
if(it?.mediaType == "movie" || it?.mediaType == "tv") {
val mediaResult = it?.let { it1 -> MediaResultMapper.mapToMediaResult(it1) }
mediaResult?.let { it1 -> listMediaResult.add(it1) }
mediaResult?.let { it1 -> Log.d("Movie", it1.title) }
}
}
}
else Log.d("ERROR FAILED", response.message())
listMediaResult
}
}

@ -1,4 +1,4 @@
package fr.iut.pm.movieapplication.repository
package fr.iut.pm.movieapplication.repository.api
import android.util.Log
import fr.iut.pm.movieapplication.api.RetrofitInstance

@ -1,4 +1,4 @@
package fr.iut.pm.movieapplication.repository
package fr.iut.pm.movieapplication.repository.api
import android.util.Log
import fr.iut.pm.movieapplication.api.RetrofitInstance

@ -7,10 +7,7 @@ import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.iut.pm.movieapplication.R
import fr.iut.pm.movieapplication.ui.fragments.FavoritesFragment
import fr.iut.pm.movieapplication.ui.fragments.HomeSectionsFragment
import fr.iut.pm.movieapplication.ui.fragments.MoviesFragment
import fr.iut.pm.movieapplication.ui.fragments.TvShowsFragment
import fr.iut.pm.movieapplication.ui.fragments.*
class MainActivity : AppCompatActivity() {
@ -18,14 +15,14 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragments(HomeSectionsFragment(this))
loadFragments(HomeSectionsFragment())
val navigationView = findViewById<BottomNavigationView>(R.id.navigation_view)
navigationView.setOnItemSelectedListener {
when(it.itemId)
{
R.id.home_page -> {
loadFragments(HomeSectionsFragment(this))
loadFragments(HomeSectionsFragment())
return@setOnItemSelectedListener true
}
@ -57,13 +54,16 @@ class MainActivity : AppCompatActivity() {
override fun onQueryTextSubmit(query: String?): Boolean {
if(!query.isNullOrEmpty()) {
loadFragments(SearchResultFragment.newInstance(query))
}
return true;
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
//A améliorer
override fun onQueryTextChange(query: String?): Boolean {
if(!query.isNullOrEmpty()) {
loadFragments(SearchResultFragment.newInstance(query))
}
return true
}
})

@ -10,17 +10,18 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load
import fr.iut.pm.movieapplication.databinding.ItemHorizontalHomePageBinding
import fr.iut.pm.movieapplication.model.media.MediaResult
import fr.iut.pm.movieapplication.ui.interfaces.MediaSelection
import fr.iut.pm.movieapplication.ui.interfaces.MovieSelection
import fr.iut.pm.movieapplication.utils.Constants
class MediaAdapter(private val listener : MovieSelection): ListAdapter<MediaResult, MediaAdapter.ViewHolder>(DiffUtilMediaCallback) {
class MediaAdapter(private val listener : MediaSelection): ListAdapter<MediaResult, MediaAdapter.ViewHolder>(DiffUtilMediaCallback) {
private object DiffUtilMediaCallback : DiffUtil.ItemCallback<MediaResult>() {
override fun areItemsTheSame(oldItem: MediaResult, newItem: MediaResult) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: MediaResult, newItem: MediaResult) = oldItem == newItem
}
class ViewHolder(private val binding : ItemHorizontalHomePageBinding, listener: MovieSelection)
class ViewHolder(private val binding : ItemHorizontalHomePageBinding, listener: MediaSelection)
: RecyclerView.ViewHolder(binding.root) {
val mediaResult : MediaResult? get() = binding.mediaResult

@ -1,4 +1,42 @@
package fr.iut.pm.movieapplication.ui.adapter
class SearchResultAdapter {
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import coil.load
import fr.iut.pm.movieapplication.databinding.ItemSearchResultsBinding
import fr.iut.pm.movieapplication.model.media.MediaResult
import fr.iut.pm.movieapplication.ui.interfaces.MediaSelection
import fr.iut.pm.movieapplication.utils.Constants
class SearchResultAdapter(private val listener : MediaSelection)
: ListAdapter<MediaResult, SearchResultAdapter.ViewHolder>(DiffUtilMediaCallback) {
private object DiffUtilMediaCallback : DiffUtil.ItemCallback<MediaResult>() {
override fun areItemsTheSame(oldItem: MediaResult, newItem: MediaResult) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: MediaResult, newItem: MediaResult) = oldItem == newItem
}
class ViewHolder(private val binding : ItemSearchResultsBinding, listener: MediaSelection)
: RecyclerView.ViewHolder(binding.root) {
val mediaResult : MediaResult? get() = binding.mediaResult
fun bind(mediaResult : MediaResult) {
binding.mediaResult = mediaResult
val imgUri = mediaResult.posterPath?.let { (Constants.IMG_URL +it).toUri().buildUpon().scheme("https").build() }
binding.itemImage.load(imgUri)
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(ItemSearchResultsBinding.inflate(LayoutInflater.from(parent.context)), listener)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(getItem(position))
}

@ -1,6 +1,7 @@
package fr.iut.pm.movieapplication.ui.fragments
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -10,12 +11,11 @@ import fr.iut.pm.movieapplication.databinding.FragmentHomeSectionsBinding
import fr.iut.pm.movieapplication.ui.activity.MainActivity
import fr.iut.pm.movieapplication.ui.adapter.MediaAdapter
import fr.iut.pm.movieapplication.ui.adapter.HomeItemDecoration
import fr.iut.pm.movieapplication.ui.interfaces.MediaSelection
import fr.iut.pm.movieapplication.ui.interfaces.MovieSelection
import fr.iut.pm.movieapplication.ui.viewmodel.HomeSectionsVM
class HomeSectionsFragment(
private val context : MainActivity
) : Fragment(), MovieSelection {
class HomeSectionsFragment() : Fragment(), MediaSelection {
private val homeSectionsVM by viewModels<HomeSectionsVM>()
@ -67,8 +67,7 @@ class HomeSectionsFragment(
popularTvShowsAdapter.submitList(it)
}
}
override fun onMovieSelected(movieId: Int) {
TODO("Not yet implemented")
override fun onMediaSelected(mediaId: Int) {
Log.d("ITEM SELECTED", mediaId.toString())
}
}

@ -1,23 +1,60 @@
package fr.iut.pm.movieapplication.ui.fragments
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import fr.iut.pm.movieapplication.databinding.FragmentSearchResultsBinding
import fr.iut.pm.movieapplication.ui.adapter.MediaAdapter
import fr.iut.pm.movieapplication.ui.adapter.SearchResultAdapter
import fr.iut.pm.movieapplication.ui.interfaces.MediaSelection
import fr.iut.pm.movieapplication.ui.viewmodel.SearchResultVM
import fr.iut.pm.movieapplication.ui.viewmodel.SearchResultVMFactory
class SearchResultFragment : Fragment() {
class SearchResultFragment : Fragment(), MediaSelection {
private val searchResultViewModel by viewModels<SearchResultVM> { SearchResultVMFactory(arguments?.getString("query")!!) }
private val searchResultAdapter = SearchResultAdapter(this)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return super.onCreateView(inflater, container, savedInstanceState)
): View {
val binding = FragmentSearchResultsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.searchResultsVM = searchResultViewModel
with(binding.searchResultsItemRecyclerView) {
this.adapter = searchResultAdapter
}
return binding.root;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
searchResultViewModel.getSearchResultLiveData().observe(viewLifecycleOwner) {
searchResultAdapter.submitList(it)
}
}
override fun onMediaSelected(mediaId: Int) {
Log.d("ITEM SELECTED", mediaId.toString())
}
companion object {
fun newInstance(query: String): SearchResultFragment {
val fragment = SearchResultFragment()
val args = Bundle()
args.putString("query", query)
fragment.arguments = args
return fragment
}
}
}

@ -0,0 +1,6 @@
package fr.iut.pm.movieapplication.ui.interfaces
interface MediaSelection {
fun onMediaSelected(mediaId : Int)
}

@ -4,7 +4,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import fr.iut.pm.movieapplication.model.media.MediaResult
import fr.iut.pm.movieapplication.repository.MediaRepository
import fr.iut.pm.movieapplication.repository.api.MediaRepository
import kotlinx.coroutines.launch
class HomeSectionsVM : ViewModel() {

@ -3,7 +3,7 @@ package fr.iut.pm.movieapplication.ui.viewmodel
import androidx.lifecycle.*
import fr.iut.pm.movieapplication.data.database.MovieDataBase
import fr.iut.pm.movieapplication.model.media.movie.MovieDetails
import fr.iut.pm.movieapplication.repository.MovieAPIRepository
import fr.iut.pm.movieapplication.repository.api.MovieAPIRepository
import fr.iut.pm.movieapplication.repository.local.MovieLocalRepository
import kotlinx.coroutines.launch

@ -4,7 +4,7 @@ import androidx.lifecycle.*
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.iut.pm.movieapplication.model.media.movie.Movie
import fr.iut.pm.movieapplication.repository.MovieAPIRepository
import fr.iut.pm.movieapplication.repository.api.MovieAPIRepository
import fr.iut.pm.movieapplication.utils.Constants.Companion.MAX_PAGE
import kotlinx.coroutines.launch

@ -1,16 +1,25 @@
package fr.iut.pm.movieapplication.ui.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.*
import fr.iut.pm.movieapplication.model.media.MediaResult
import fr.iut.pm.movieapplication.repository.api.MediaRepository
import kotlinx.coroutines.launch
class SearchResultVM(private var query : String) : ViewModel() {
private val repository : MediaRepository = MediaRepository()
private var _queryLiveData : MutableLiveData<String> = MutableLiveData()
fun getQueryLiveData() : LiveData<String> = _queryLiveData
private var _searchResultLiveData : MutableLiveData<List<MediaResult>> = MutableLiveData()
fun getSearchResultLiveData() : LiveData<List<MediaResult>> = _searchResultLiveData
init {
_queryLiveData.postValue(query)
_queryLiveData.value = query
viewModelScope.launch {
_searchResultLiveData.postValue(repository.search(query))
}
}
}

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import fr.iut.pm.movieapplication.model.media.tvshow.TvShow
import fr.iut.pm.movieapplication.repository.TvShowRepository
import fr.iut.pm.movieapplication.repository.api.TvShowRepository
import fr.iut.pm.movieapplication.ui.adapter.TvShowAdapter
import kotlinx.coroutines.launch

@ -14,16 +14,16 @@ object MediaResultMapper {
fun mapToTvShow(mediaResultDTO: MediaResultDTO): TvShow {
return TvShow(
posterPath = mediaResultDTO.posterPath,
popularity = mediaResultDTO.popularity,
popularity = mediaResultDTO.popularity!!,
id = mediaResultDTO.id,
backdropPath = mediaResultDTO.backdropPath ?: "",
voteAverage = mediaResultDTO.voteAverage,
voteAverage = mediaResultDTO.voteAverage!!,
overview = mediaResultDTO.overview ?: "",
firstAirDate = mediaResultDTO.firstAirDate,
originCountry = mediaResultDTO.originCountry!!,
// genreIds = mediaResultDTO.genreIds,
originalLanguage = mediaResultDTO.originalLanguage,
voteCount = mediaResultDTO.voteCount,
originalLanguage = mediaResultDTO.originalLanguage!!,
voteCount = mediaResultDTO.voteCount!!,
name = mediaResultDTO.name!!,
originalName = mediaResultDTO.originalName!!
)
@ -38,13 +38,13 @@ object MediaResultMapper {
// genreIds = mediaResultDTO.genreIds,
id = mediaResultDTO.id,
originalTitle = mediaResultDTO.originalTitle!!,
originalLanguage = mediaResultDTO.originalLanguage,
originalLanguage = mediaResultDTO.originalLanguage!!,
title = mediaResultDTO.title!!,
backdropPath = mediaResultDTO.backdropPath,
popularity = mediaResultDTO.popularity,
voteCount = mediaResultDTO.voteCount,
popularity = mediaResultDTO.popularity!!,
voteCount = mediaResultDTO.voteCount!!,
//video = mediaResultDTO.video,
voteAverage = mediaResultDTO.voteAverage
voteAverage = mediaResultDTO.voteAverage!!
)
}
@ -58,12 +58,12 @@ object MediaResultMapper {
// genreIds = mediaResultDTO.genreIds,
id = mediaResultDTO.id,
originalTitle = mediaResultDTO.originalTitle ?: mediaResultDTO.originalName!!, //if it's not a movie also it's a tvshow
originalLanguage = mediaResultDTO.originalLanguage,
originalLanguage = mediaResultDTO.originalLanguage!!,
title = mediaResultDTO.title ?: mediaResultDTO.name!!, //if it's not a movie also it's a tvshow
backdropPath = mediaResultDTO.backdropPath,
popularity = mediaResultDTO.popularity,
voteCount = mediaResultDTO.voteCount,
voteAverage = mediaResultDTO.voteAverage,
popularity = mediaResultDTO.popularity!!,
voteCount = mediaResultDTO.voteCount!!,
voteAverage = mediaResultDTO.voteAverage!!,
mediaType = mediaResultDTO.mediaType
)
}

@ -41,7 +41,7 @@
android:layout_height="250dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_vertical_fragment" />
tools:listitem="@layout/item_search_results" />
<TextView
android:id="@+id/home_page_popularity_section_title"
@ -59,7 +59,7 @@
android:layout_height="250dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_vertical_fragment" />
tools:listitem="@layout/item_search_results" />
<TextView
android:id="@+id/home_page_free_section_title"

@ -16,10 +16,6 @@
android:layout_width="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_results_item_recycler_view"
@ -28,7 +24,7 @@
android:layout_marginBottom="@dimen/default_margin"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:listitem="@layout/item_vertical_fragment" />
tools:listitem="@layout/item_search_results" />
</LinearLayout>

@ -1,10 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="mediaResult"
type="fr.iut.pm.movieapplication.model.media.MediaResult" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
@ -13,11 +24,7 @@
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:cardCornerRadius="10dp">
<LinearLayout
android:layout_width="match_parent"
@ -36,18 +43,23 @@
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="center"
android:textAlignment="center" />
android:textAlignment="center"
android:text="@{mediaResult.title}"/>
<TextView
android:id="@+id/item_date"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="center"
android:textAlignment="center" />
android:textAlignment="center"
android:text="@{mediaResult.releaseDate}"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</layout>
Loading…
Cancel
Save