관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 코틀린 리사이클러뷰 예제 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 코틀린 리사이클러뷰 예제

막무가내막내 2020. 3. 7. 18:25
728x90

[2021-04-13 업데이트]

최근에 사용(?), 작성한 리사이클러뷰 예제는 다음 링크에 있습니다. 참고해주세요 :)

youngest-programming.tistory.com/478

 

[안드로이드] 리사이클러뷰(RecyclerView) 어댑터 베이스 정리 코드 (복붙용)

디프유틸 사용한것 youngest-programming.tistory.com/474 ListAdapter + Diffutil 예제 정리" data-og-description="[개념(출처) 참고 및 공부자료들] thdev.tech/kotlin/2020/09/22/kotlin_effective_03/ data c..

youngest-programming.tistory.com

 

 

 

 

 

코틀린으로 짠 리사이클러뷰 표본입니다. 기록용으로 작성했습니다.

 

 

어댑터 (클릭리스너를 어댑터 생성자로 받아도 됩니다. -> 생성자로 받는게 더 깔끔한것같습니다.)

movieAdapter = MovieAdapter { movie ->
            Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
                it.resolveActivity(packageManager) != null
            }?.run(this::startActivity)
        }
class MovieAdapter(private val clickCallBack: (Movie) -> Unit) :
    RecyclerView.Adapter<MovieAdapter.ViewHolder>() {

어댑터에서 context를 받을 필요가 없습니다.

어댑터 생성자로 아이템리스트를 해놓지 않았습니다. 

package com.mtjin.androidarchitecturestudy.ui

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.Movie

class MovieAdapter :
    RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
    private lateinit var callback: (Movie) -> Unit
    private val items: ArrayList<Movie> = ArrayList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view: View = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_movie, parent, false)
        val viewHolder = ViewHolder(view)
        view.setOnClickListener {
            callback(items[viewHolder.adapterPosition])
        }
        return viewHolder
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        items[position].let {
            holder.bind(it)
        }
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val ivPoster = itemView.findViewById<ImageView>(R.id.iv_poster)
        private val rvRating = itemView.findViewById<RatingBar>(R.id.rb_rating)
        private val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
        private val tvReleaseDate = itemView.findViewById<TextView>(R.id.tv_release_date)
        private val tvActor = itemView.findViewById<TextView>(R.id.tv_actor)
        private val tvDirector = itemView.findViewById<TextView>(R.id.tv_director)

        fun bind(movie: Movie) {
            with(movie) {
                Glide.with(itemView).load(image)
                    .placeholder(R.drawable.ic_default)
                    .into(ivPoster!!)
                rvRating.rating = (userRating.toFloatOrNull() ?: 0f) / 2
                tvTitle.text = HtmlCompat.fromHtml(title, HtmlCompat.FROM_HTML_MODE_COMPACT)
                tvReleaseDate.text = pubDate
                tvActor.text = actor
                tvDirector.text = director
            }
        }
    }

    fun add(items: List<Movie>) {
        this.items.addAll(items)
        notifyDataSetChanged()
    }

    fun setItemClickListener(callback: (Movie) -> Unit) {
        this.callback = callback
    }

    fun clear() {
        this.items.clear()
        notifyDataSetChanged()
    }
}

 


 

액티비티에서 아이템 클릭리스너 정의

//어댑터 아이템 클릭리스너
movieAdapter.setItemClickListener { movie ->
            Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
                it.resolveActivity(packageManager) != null
            }?.run(this::startActivity)
        }

 

 

그냥 startActivity를 하면 Intent.ACTION_VIEW 액션을 받는 액티비티가 없으면 예외가 발생할 수 있으므로 위와 같이 액션뷰 인텐트를 실행 하면 안전하다고 합니다.

 

 

 

 


 

 

액티비티 어댑터 세팅 (레이아웃 매니저는 xml에서 세팅해놨습니다.)

class MovieSearchActivity : AppCompatActivity() {

    private lateinit var etInput: EditText
    private lateinit var btnSearch: Button
    private lateinit var rvMovies: RecyclerView
    private lateinit var movieAdapter: MovieAdapter
    private lateinit var movieCall: Call<MovieResponse>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movie_search)

        initView()
        initListener()
    }

    private fun initView() {
        etInput = findViewById(R.id.et_input)
        btnSearch = findViewById(R.id.btn_search)
        rvMovies = findViewById(R.id.rv_movies)
        movieAdapter = MovieAdapter()
        rvMovies.adapter = movieAdapter
    }

 

 

 

 

 

 


 

 

아이템 레이아웃

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp"
    android:weightSum="5">

    <ImageView
        android:id="@+id/iv_poster"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_weight="1"
        android:scaleType="fitXY" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_weight="4"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="영화제목"
            android:textSize="12sp"
            android:textStyle="bold" />

        <RatingBar
            android:id="@+id/rb_rating"
            style="@style/Widget.AppCompat.RatingBar.Small"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:isIndicator="true"
            android:numStars="5"
            android:max="10"
            android:stepSize="0.1" />

        <TextView
            android:id="@+id/tv_release_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:text="2020" />

        <TextView
            android:id="@+id/tv_director"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:text="감독" />

        <TextView
            android:id="@+id/tv_actor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:text="배우들" />

    </LinearLayout>

</androidx.appcompat.widget.LinearLayoutCompat>

 

 

 

 


 

 

액티비티 레이아웃

<?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"
    android:padding="8dp"
    tools:context=".ui.MovieSearchActivity">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.08" />

    <EditText
        android:id="@+id/et_input"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:hint="입력"
        android:inputType="text"
        app:layout_constraintBottom_toTopOf="@+id/guideline"
        app:layout_constraintEnd_toStartOf="@+id/btn_search"
        app:layout_constraintHorizontal_weight="8"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_search"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="검색"
        app:layout_constraintBottom_toTopOf="@id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintStart_toEndOf="@+id/et_input"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_movies"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:clipToPadding="false"
        app:layout_constraintTop_toBottomOf="@id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

 


 

데이터바인딩 적용된 리사이클러뷰

package com.mtjin.androidarchitecturestudy.ui.search

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.databinding.ItemMovieBinding

class MovieAdapter(private val itemClick: (Movie) -> Unit) :
    RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
    private val items: ArrayList<Movie> = ArrayList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding : ItemMovieBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_movie,
            parent,
            false
        )
        val viewHolder = ViewHolder(binding)
        binding.root.setOnClickListener {
            itemClick(items[viewHolder.adapterPosition])
        }

        return viewHolder
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        items[position].let {
            holder.bind(it)
        }
    }

    class ViewHolder(private val binding: ItemMovieBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(movie: Movie) {
            binding.movie = movie
            binding.executePendingBindings()
        }
    }

    fun addItems(items: List<Movie>) {
        this.items.addAll(items)
        notifyDataSetChanged()
    }

    fun clear() {
        this.items.clear()
        notifyDataSetChanged()
    }
}
package com.mtjin.androidarchitecturestudy.utils

import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.search.Movie
import com.mtjin.androidarchitecturestudy.ui.search.EndlessRecyclerViewScrollListener
import com.mtjin.androidarchitecturestudy.ui.search.MovieAdapter
import com.mtjin.androidarchitecturestudy.ui.search.MovieSearchViewModel


@BindingAdapter("htmlText")
fun TextView.setHtmlText(html: String) {
    text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
}

@BindingAdapter("urlImage")
fun ImageView.setUrlImage(url: String) {
    Glide.with(this).load(url)
        .placeholder(R.drawable.ic_default)
        .into(this)
}

@BindingAdapter("movieRating")
fun RatingBar.setMovieRating(score: String) {
    rating = (score.toFloatOrNull() ?: 0f) / 2
}

@BindingAdapter("setItems")
fun RecyclerView.setAdapterItems(items: List<Movie>?) {
    with((adapter as MovieAdapter)) {
        this.clear()
        items?.let { this.addItems(it) }
    }
}

@BindingAdapter("endlessScroll")
fun RecyclerView.setEndlessScroll(
    viewModel: MovieSearchViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestPagingMovie(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="movie"
            type="com.mtjin.androidarchitecturestudy.data.search.Movie" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        android:weightSum="5">

        <ImageView
            android:id="@+id/iv_poster"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_weight="1"
            android:scaleType="fitXY"
            bind:urlImage="@{movie.image}" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_weight="4"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:textStyle="bold"
                bind:htmlText="@{movie.title}" />

            <RatingBar
                android:id="@+id/rb_rating"
                style="@style/Widget.AppCompat.RatingBar.Small"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:isIndicator="true"
                android:max="10"
                android:numStars="5"
                android:stepSize="0.1"
                bind:movieRating="@{movie.userRating}" />

            <TextView
                android:id="@+id/tv_release_date"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="@{movie.pubDate}" />

            <TextView
                android:id="@+id/tv_director"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="@{movie.director}" />

            <TextView
                android:id="@+id/tv_actor"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="@{movie.actor}" />
        </LinearLayout>

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

 

 

 

혹은 다음과 같이 어댑터를 해도 됩니다.

package com.mtjin.cnunoticeapp.views.board_list

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.data.board_list.Board
import com.mtjin.cnunoticeapp.databinding.ItemBoardBinding

class BoardAdapter :
    RecyclerView.Adapter<BoardAdapter.ViewHolder>() {
    private val items = mutableListOf<Board>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemBoardBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_board,
            parent,
            false
        )
        return ViewHolder(binding).apply {
            binding.root.setOnClickListener { view ->
                val position = adapterPosition.takeIf { it != RecyclerView.NO_POSITION }
                    ?: return@setOnClickListener


            }
        }
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        items[position].let {
            holder.bind(it)
        }
    }

    class ViewHolder(private val binding: ItemBoardBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(board: Board) {
            binding.item = board
            binding.executePendingBindings()
        }
    }

    fun addItems(items: List<Board>) {
        this.items.addAll(items)
        notifyDataSetChanged()
    }

    fun addItem(item: Board) {
        this.items.add(item)
        notifyDataSetChanged()
    }

    fun clear() {
        this.items.clear()
        notifyDataSetChanged()
    }
}

 

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

 

 

728x90
Comments