[Android] 리사이클러 뷰 아이템에 즐겨찾기 추가하기

4 minute read


개요

안녕하세요!

저는 지금 학교에서 학교 웹 페이지의 공지사항을 크롤링해서 리사이클러 뷰 아이템으로 띄워주는 작업을 하고 있는데요, 여기에 즐겨찾기 기능을 추가하는 포스팅을 작성해보고자 합니다!

인터넷에 한 번에 쭉 정리되어 있는 자료들이 없더라구요ㅠㅠ

그럼 각설하고 코드를 보겠습니다! (주석이 달려있는 부분의 코드들을 보시면 됩니다.)

image-20211115205425671


즐겨찾기된 아이템을 출력할 공간 만들기

위에서 보면 알 수 있듯이, 저는 탭 레이아웃에 [내 공지] 탭을 추가할 것입니다.

  • NoticeActivity.kt
...
class NoticeActivity : AppCompatActivity() {

    companion object{
        ...

        /* 탭 클릭 시 가져올 데이터 */
        const val COMMON_TAB = "[일반]"
        const val BACHELOR_TAB = "[학사]"
        const val STUDENT_TAB = "[학생]"
        const val ENROLL_TAB = "[등록/장학]"
        const val MY_TAB = "즐겨찾기" // 상수 선언
    }
    val binding by lazy{ActivityNoticeBinding.inflate(layoutInflater)}
    var helper: RoomHelper? = null
    lateinit var adapter: NoticeRecyclerAdapter
    // 수정할 데이터를 임시 저장할 프로퍼티
    var updateNotice: NoticeItem? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        helper = Room.databaseBuilder(this, RoomHelper::class.java, "notice_item")
            .allowMainThreadQueries().build()
        adapter = NoticeRecyclerAdapter()
        adapter.helper = helper
        // 어댑터에 this(본인 액티비티) 전달
        adapter.noticeActivity = this

        for (category in arrayOf(0,1,2,4)) { 
            for (page in 1..MAX_PAGE) { 
                var thread: Thread
                if (page == 1) {
                    thread = Thread(UrlRun(PAGE1_FRONT_BASE_URL+"$category"+PAGE1_BACK_BASE_URL, page, applicationContext))
                } else {
                    thread = Thread(UrlRun(AFTER_PAGE2_FRONT_BASE_URL+"$page"+ AFTER_PAGE2_BACK_BASE_URL+"$category",page,applicationContext))
                }
                thread.start()
                thread.join()
            }
        }
        Log.d("NoticeActivity/OnCreate", "웹 크롤링 완료")

        var data: MutableList<NoticeItem>

        data = loadData(COMMON_TAB)
        setData(data)

        // TODO: 탭 리스너 - Room에서 가져올 데이터 지정
        binding.tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener{
            override fun onTabSelected(tab: TabLayout.Tab?) {
                when(tab?.position){
                    0 -> {
                        data = loadData(COMMON_TAB)
                    }
                    1 -> {
                        data = loadData(BACHELOR_TAB)
                    }
                    2 -> {
                        data = loadData(STUDENT_TAB)
                    }
                    3 -> {
                        data = loadData(ENROLL_TAB)
                    }
                    4 -> { // 내 공지 탭
                        data = loadData(MY_TAB)
                    }
                }
                setData(data)
            }

            override fun onTabUnselected(tab: TabLayout.Tab?) {

            }

            override fun onTabReselected(tab: TabLayout.Tab?) {

            }
        })


    }
    // 리사이클러 뷰에 아이템 출력하기
    private fun setData(data: List<NoticeItem>){
        adapter.listData.clear()
        adapter.listData.addAll(data)
        adapter.notifyDataSetChanged()
        binding.recyclerViewNotice.adapter = adapter
        binding.recyclerViewNotice.layoutManager = LinearLayoutManager(this)
    }

    // Room에 저장된 아이템 리스트 불러오기
    private fun loadData(category: String): MutableList<NoticeItem>{
        var data: MutableList<NoticeItem> = mutableListOf()
        // TODO: Room의 데이터 가져오기
        if (category == MY_TAB){
            data = helper?.noticeItemDAO()?.getFavoriteData()!!
        }else {
            data = helper?.noticeItemDAO()?.getCategoryData(category)!!
        }

        return data
    }


    // 웹 크롤링 스레드 클래스
    inner class UrlRun(var url: String, var pages: Int, var context: Context): Runnable{
        @Synchronized
        override fun run() {
            try{
                val noticeHtml = Jsoup.connect(url).get()
                val items = noticeHtml.select(ITEM_ROUTE)
                // 가져올 정보
                var url: String      // 주소
                var category: String // 카테고리
                var title: String    // 제목
                var info: String     // 정보
                for (item in items){
                    // url, category, title, info 파싱
                    // 즐겨찾기는 false로 초기값 설정
                    url = KW_URL + item.select("a").attr("href")
                    category = item.select("strong.category").text()
                    title = item.select("a").text().split("]").last()
                    info = item.select("p.info").text()
                    val noticeItem = NoticeItem(url, category, title, info, "false")
                    helper?.noticeItemDAO()?.insert(noticeItem)

                    Log.i("NoticeActivity/UrlRun", "$url, $category, $title, $info, 'false'")

                }

            }
            catch(e: Exception){
                Log.e("NoticeActivity/UrlRun", e.toString())
            }
        }

    }
}
  • activity_notice.xml

image-20211115210531967

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NoticeActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="409dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="1dp"
        android:layout_marginEnd="1dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="일반" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="학사" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="학생" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="등록/장학" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="내 공지" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerViewNotice"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tabLayout" />

</androidx.constraintlayout.widget.ConstraintLayout>



아이템 레이아웃에 즐겨찾기 아이콘 추가하기

  • notice_recycler_item.xml

image-20211115212227455

즐겨찾기를 표시하기 위한 방법으로는 ‘이미지 버튼’을 사용하였고, 저기에 나타난 이미지는 안드로이드 기본 리소스인 @android:drawable/btn_star_big_off입니다.

그리고 이 아이콘의 크기가 자동으로 적절히 조정되도록 scaleType=fitCenter로 지정합니다.



데이터베이스에 컬럼과 수정 메서드 추가하기

여기서는 Room 라이브러리를 이용합니다.

  • NoticeItem.kt
...
// NoticeActivity에서 사용할 테이블을 나타내는 클래스
@Entity(tableName = "notice_item")
class NoticeItem {
    @ColumnInfo
    @PrimaryKey(autoGenerate = true)
    var no: Long? = null
    @ColumnInfo
    var url: String = ""
    @ColumnInfo
    var category = ""
    @ColumnInfo
    var title = ""
    @ColumnInfo
    var info = ""
    // 즐겨찾기 컬럼 추가
    @ColumnInfo
    var favorite = ""
    
    // 생성자 작성하기
    constructor(url: String, category: String, title: String, info: String, favorite: String){
        this.url = url
        this.category = category
        this.title = title
        this.info = info
        // 즐겨찾기 여부
        this.favorite = favorite
    }
}
  • NoticeItemDAO.kt
...
// NoticeActivity와 NoticeItem(Room)을 연결해주는 인터페이스
@Dao
interface NoticeItemDAO {
    @Query("select * from notice_item")
    fun getAll(): MutableList<NoticeItem>
    
    @Query("select * from notice_item where category=:category")
    fun getCategoryData(category: String): MutableList<NoticeItem>
    // 즐겨찾기 공지 조회
    @Query("select * from notice_item where favorite=\"true\"")
    fun getFavoriteData(): MutableList<NoticeItem>
    // REPLACE를 import 할 때는 androidx.room 패키지로 시작하는 것을 선택
    // 동일한 키를 가진 값이 입력되었을 경우 UPDATE 쿼리로 실행
    @Insert(onConflict = REPLACE)
    fun insert(noticeItem: NoticeItem)
    @Delete
    fun delete(noticeItem: NoticeItem)
    // 즐겨찾기는 값이 바뀌기 때문에 update 메서드 추가
    @Update
    fun update(memo: NoticeItem)
}



어댑터 설정하기

이제 마지막으로 어댑터에 코드를 추가해주면 됩니다.

...
class NoticeRecyclerAdapter: RecyclerView.Adapter<NoticeRecyclerAdapter.NoticeRecyclerHolder>() {

    var listData = mutableListOf<NoticeItem>()
    var helper: RoomHelper? = null
    // 데이터 수정을 위해서 NoticeActivity 프로퍼티 생성
    var noticeActivity: NoticeActivity? = null

    inner class NoticeRecyclerHolder(val binding: NoticeRecyclerItemBinding): RecyclerView.ViewHolder(binding.root){

        var tmpNoticeItem: NoticeItem? = null

        // 아이템에 데이터를 세팅
        fun setNoticeItem(item: NoticeItem){
            binding.textCategory.text = item.category
            binding.textTitle.text = item.title
            binding.textInfo.text = item.info
            // 즐겨찾기 버튼 세팅
            if(item.favorite == "true"){
                binding.btnFavorite.setImageResource(android.R.drawable.btn_star_big_on)
            }else{
                binding.btnFavorite.setImageResource(android.R.drawable.btn_star_big_off)
            }
            // 즐겨찾기 버튼 클릭 시 즐겨찾기 등록/해제 및 아이콘 변경
            binding.btnFavorite.setOnClickListener {
                if(item.favorite == "true"){
                    binding.btnFavorite.setImageResource(android.R.drawable.btn_star_big_off)
                    item.favorite = "false"
                    Toast.makeText(binding.root.context,
                                    "즐겨찾기 해제되었습니다.",
                                    Toast.LENGTH_SHORT).show()
                }else{
                    binding.btnFavorite.setImageResource(android.R.drawable.btn_star_big_on)
                    item.favorite = "true"
                    Toast.makeText(binding.root.context,
                                "즐겨찾기 등록되었습니다.",
                                Toast.LENGTH_SHORT).show()
                }
                // 데이터 변경 알리기
                helper?.noticeItemDAO()?.update(item)
                notifyDataSetChanged()
            }
            
            itemView.setOnClickListener{
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(item.url))
                binding.root.context.startActivity(intent)
            }

        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoticeRecyclerHolder {
        val binding = NoticeRecyclerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        return NoticeRecyclerHolder(binding)
    }

    override fun onBindViewHolder(holder: NoticeRecyclerHolder, position: Int) {
        val noticeItem = listData.get(position)
        holder.setNoticeItem(noticeItem)
    }

    override fun getItemCount(): Int {
        return listData.size
    }

}



정리

지금까지 코드로 즐겨찾기 기능을 추가하는 법을 보았습니다!

흐름을 정리하면 다음과 같습니다.

  • 즐겨찾기된 아이템을 출력할 탭(목록) 생성
  • 아이템 레이아웃에 즐겨찾기 아이콘 추가하기
  • DB에 컬럼을 추가하고 수정 메서드 추가하기
  • 어댑터에 코드 추가하기

다들 도움이 되셨기를 바랍니다~



Categories:

Updated:

Leave a comment