1. 결과 화면
![]() |
![]() |
![]() |
앱 처음 화면 | 검색어 입력 | 아이템 클릭 시 좋아요 표시 |
2. RecyclerView UI
① fragment_main.xml
- xml에서 layoutManager 설정 가능
② item_grid.xml
3. RecyclerView
3-1. RecyclerViewAdapter / ViewHolder
① ViewHolder을 RecyclerViewAdapter 내부에서 만드느냐, 외부에서 만드느냐에 따라 adpater 접근이 다름
- 외부에서 만들어 접근
class KakaoRecyclerViewAdapter() : RecyclerView.Adapter<KakaoViewHoler>() {}
- 내부에서 만들어 접근
class KakaoRecyclerViewAdapter() : RecyclerView.Adapter<RecyclerView.KakaoViewHoler>() {}
② KakaoRecyclerViewAdapter.kt
interface KakaoImageClickListener {
fun onClickItem(kakaoData: Documents)
}//클릭 이벤트
class KakaoRecyclerViewAdapter(
private val kakaoImageClickListener: KakaoImageClickListener
) : RecyclerView.Adapter<KakaoViewHoler>() {//KakaoViewHoler 외부파일로 꺼냄
var kakoList: List<Documents> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KakaoViewHoler {
val layoutInflater = LayoutInflater.from(parent.context)
return KakaoViewHoler(
binding = ItemGridBinding.inflate(layoutInflater, parent, false),
kakaoImageClickListener = kakaoImageClickListener
)
}
override fun onBindViewHolder(holder: KakaoViewHoler, position: Int) {
val kakaoItem = kakoList[position]
holder.bind(kakaoItem = kakaoItem)
}
override fun getItemCount(): Int {
return this.kakoList.size
}
fun submitList(item: List<Documents>) {
this.kakoList = item
notifyDataSetChanged()
}
}
③ KakoViewHolder.kt
class KakaoViewHoler(
private val binding: ItemGridBinding,
private val kakaoImageClickListener: KakaoImageClickListener
//inner class: 밖에 있는 변수, 함수 접근 가능 <-> class: 접근 불가하여 클릭이벤트 생성자 주입
) : RecyclerView.ViewHolder(binding.root) {
private var kakaoData: Documents? = null
init {
binding.root.setOnClickListener {
kakaoData?.let {
kakaoImageClickListener.onClickItem(kakaoData = it)
}
}
}
fun bind(kakaoItem: Documents) {
this.kakaoData = kakaoItem
//서버 이미지 사용 : Glide
Glide.with(binding.root.context).load(kakaoItem.thumbnail_url).into(binding.ivProfile)
//with: Activity,Fragment로 부터 가져온 Context, load: 이미지 로드, into: 이미지를 보여줄 view
binding.tvTitle.text = kakaoItem.display_sitename
binding.tvDate.text = kakaoItem.datetime.toString()
}
}
3-2. RecyclerView 연결
① MainFragment.kt
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private val adapter: KakaoRecyclerViewAdapter =
KakaoRecyclerViewAdapter(kakaoImageClickListener = object : KakaoImageClickListener {
override fun onClickItem(kakaoData: Documents) {
//isLike 데이터 전달/이동
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = this.adapter
communicateNetWork("")
}
private fun communicateNetWork(query: String) = lifecycleScope.launch {
val responseData = NetWorkClient.kakaoNetWork.getKakao(query, 80)
Log.d("debug100", responseData.toString())
adapter.submitList(responseData.documents) //데이터 갱신
}
}
}
4. 검색 기능 및 키보드/Focus 숨기기
- 검색어를 입력하고 검색 버튼을 눌렀을 때, 키보드/Focus 숨기기
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private val adapter: KakaoRecyclerViewAdapter =
KakaoRecyclerViewAdapter(kakaoImageClickListener = object : KakaoImageClickListener {
override fun onClickItem(kakaoData: Documents) {
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = this.adapter
binding.btnSearchClick.setOnClickListener {
if (binding.etSearchText.text.toString().isNotEmpty()) {
communicateNetWork(binding.etSearchText.text.toString())
hideKeyboard()
requireActivity().currentFocus!!.clearFocus() //Focus 숨기기
} else {
return@setOnClickListener
}
}
}
private fun communicateNetWork(query: String) = lifecycleScope.launch {
val responseData = NetWorkClient.kakaoNetWork.getKakao(query, 80)
Log.d("debug100", responseData.toString())
adapter.submitList(responseData.documents)
}
private fun hideKeyboard(){ //키보드 숨기기
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0)
}
}
- 공백 및 특수문자 검색 시 HttpException 발생 : 예외처리
private fun communicateNetWork(query: String) = lifecycleScope.launch {
try {
val responseData = NetWorkClient.kakaoNetWork.getKakao(query, 80)
Log.d("debug100", responseData.toString())
adapter.submitList(responseData.documents)
}catch (e: retrofit2.HttpException){
}
}
5. 아이템을 클릭하였을 때, 좋아요(하트) 송출
① data class에서 좋아요 기능을 위한 변수 생성
data class Documents(
...
var isLike : Boolean
//val isLike : Boolean > 4번
)
② isLake : 데이터 변경 처리
class KakaoViewHoler(
private val binding: ItemGridBinding,
private val kakaoImageClickListener: KakaoImageClickListener
) : RecyclerView.ViewHolder(binding.root) {
private var kakaoData: Documents? = null
init {
binding.root.setOnClickListener {
kakaoData?.let {
kakaoImageClickListener.onClickItem(kakaoData = it)
it.isLike = !it.isLike //데이터 바뀜
}
}
fun bind(kakaoItem: Documents) {
this.kakaoData = kakaoItem
Glide.with(binding.root.context).load(kakaoItem.thumbnail_url).into(binding.ivProfile)
//with : Activity,Fragment로 부터 가져온 Context
//load : 이미지 로드, into : 이미지를 보여줄 view
binding.tvTitle.text = kakaoItem.display_sitename
binding.tvDate.text = kakaoItem.datetime.toString()
when(kakaoItem.isLike){//좋아요
true -> binding.ivLike.isVisible = true
false -> binding.ivLike.isVisible = false
}
}
}
③ notifyDataSetChanged() : 바뀐 데이터를 리사이클러뷰에 알려줌
- 방법 1. ViewHolder : 일반 클래스 > inner 클래스로 변경하여 notifyDataSetChanged() 호출
더보기
inner class KakaoViewHoler(
private val binding: ItemGridBinding,
private val kakaoImageClickListener: KakaoImageClickListener
) : RecyclerView.ViewHolder(binding.root) {
private var kakaoData: Documents? = null
init {
binding.root.setOnClickListener {
kakaoData?.let {
kakaoImageClickListener.onClickItem(kakaoData = adapterPosition)
it.isLike = !it.isLike //바뀐 데이터를
notifyDataSetChanged() //리사이클러뷰에 알려줌
}
}
}
fun bind(kakaoItem: Documents) {
this.kakaoData = kakaoItem
Glide.with(binding.root.context).load(kakaoItem.thumbnail_url).into(binding.ivProfile)
binding.tvTitle.text = kakaoItem.display_sitename
binding.tvDate.text = kakaoItem.datetime.toString()
when(kakaoItem.isLike){
true -> binding.ivLike.isVisible = true
false -> binding.ivLike.isVisible = false
}
}
}
- 방법 2. RecyclerView가 연결되는 Fragment/Activity에서 notifyDataSetChanged() 호출
더보기
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {//뷰 변경
super.onViewCreated(view, savedInstanceState)
adapter = KakaoRecyclerViewAdapter(object : KakaoImageClickListener{
override fun onClickItem(kakaoData: Documents) {
adapter.notifyDataSetChanged()
}
})
binding.recyclerView.adapter = this.adapter
binding.btnSearchClick.setOnClickListener {
if (binding.etSearchText.text.toString().isNotEmpty()) {
communicateNetWork(binding.etSearchText.text.toString())
hideKeyboard()
requireActivity().currentFocus!!.clearFocus()
} else {
return@setOnClickListener
}
}
}
- 방법 3. RecyclerView 내부에서 notifyDataSetChanged() 호출
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KakaoViewHoler {
val layoutInflater = LayoutInflater.from(parent.context)
return KakaoViewHoler(
binding = ItemGridBinding.inflate(layoutInflater, parent, false),
object : KakaoImageClickListener{
override fun onClickItem(kakaoData: Documents) {
notifyDataSetChanged()
}
}
)
}
④ 클릭 이벤트 구문은 communicateNetWork()가 동작하는 곳에서 작성 : 클릭한 데이터는 RecyclerViewAdapter의 데이터가 아닌 카카오 API를 통해서 가져오는 데이터이기 때문
더보기
- KakaoViewHolder
class KakaoViewHoler(
private val binding: ItemGridBinding,
private val kakaoImageClickListener: KakaoImageClickListener,
) :
RecyclerView.ViewHolder(binding.root) {
private var kakaoData: Documents? = null
init {
binding.ivProfile.setOnClickListener {
kakaoData?.let {
kakaoImageClickListener.onClickItem(kakaoData = it)
}
}
}
fun bind(kakaoItem: Documents) {
this.kakaoData = kakaoItem
Glide.with(binding.root.context).load(kakaoItem.thumbnail_url).into(binding.ivProfile)
binding.tvTitle.text = kakaoItem.display_sitename
binding.tvDate.text = kakaoItem.datetime.toString()
when(kakaoItem.isLike){
true -> binding.ivLike.isVisible = true
false -> binding.ivLike.isVisible = false
}
}
}
- KakaoRecyclerViewAdapter
interface KakaoImageClickListener {
fun onClickItem(kakaoData: Documents)
}
class KakaoRecyclerViewAdapter(
private val kakaoImageClickListener: KakaoImageClickListener
) : RecyclerView.Adapter<KakaoViewHoler>() {
var kakoList: List<Documents> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KakaoViewHoler {
val layoutInflater = LayoutInflater.from(parent.context)
return KakaoViewHoler(
binding = ItemGridBinding.inflate(layoutInflater, parent, false),
kakaoImageClickListener = kakaoImageClickListener
)
}
override fun onBindViewHolder(holder: KakaoViewHoler, position: Int) {
val kakaoItem = kakoList[position]
holder.bind(kakaoItem = kakaoItem)
}
override fun getItemCount(): Int {
return this.kakoList.size
}
fun submitList(item: List<Documents>) {
this.kakoList = item
notifyDataSetChanged()
}
}
- MainFragment
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private var kakaoDataList = mutableListOf<Documents>()
private val adapter: KakaoRecyclerViewAdapter by lazy {
createAdapter()
}
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { //뷰 변경
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = this.adapter
binding.btnSearchClick.setOnClickListener {
if (binding.etSearchText.text.toString().isNotEmpty()) {
communicateNetWork(binding.etSearchText.text.toString())
hideKeyboard()
requireActivity().currentFocus!!.clearFocus()
} else {
return@setOnClickListener
}
}
}
private fun communicateNetWork(query: String) = lifecycleScope.launch {
try {
val responseData = NetWorkClient.kakaoNetWork.getKakao(query, 80)
Log.d("debug100", responseData.toString())
kakaoDataList.addAll(responseData.documents) //클릭 한 이미지 데이터 추가
adapter.submitList(kakaoDataList) //데이터 갱신
} catch (e: retrofit2.HttpException) {
}
}
private fun hideKeyboard() {
val imm =
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0)
}
private fun createAdapter(): KakaoRecyclerViewAdapter {
return KakaoRecyclerViewAdapter(kakaoImageClickListener = object : KakaoImageClickListener {
override fun onClickItem(kakaoData: Documents) {
val index = kakaoDataList.indexOf(kakaoData) //인덱스 찾기
kakaoDataList.set(
index, kakaoData.copy( //isLike가 val이므로 copy 이용
isLike = !kakaoData.isLike
)
)
adapter.submitList(kakaoDataList) //kakaoDataList 데이터 갱신
}
})
}
}
'연습장 > 실습' 카테고리의 다른 글
6주차_ Kakao API 활용 앱 2. Kakao API 연결 (0) | 2024.05.07 |
---|---|
6주차_ Kakao API 활용 앱 1. ViewPager/TabLayout (0) | 2024.05.03 |
Basic 6주차_ 1. Enum Class, Sealed Class 사용 (1) | 2024.05.01 |
5주차_ 짝퉁마켓 앱 5. FloatingActionButton (플로팅 버튼) (0) | 2024.04.17 |
5주차_ 짝퉁마켓 앱 4. Parcelize 데이터 전달 (0) | 2024.04.16 |