티스토리 뷰

1. 안드로이드 오늘의 명언 어플 만들기

파이어 베이스의 원격 설정 기능을 활용해서 원격에서 설정한 명언을 그때 그때 마다 업데이트 해서 새롭게 보여주는 오늘의 명언 어플을 만들어보았습니다. 보여줄 명언을 코드 수정 또는 앱 업데이트 없이도 갱신할 수 있고 특정 기능을 끄는 것을 원격으로 할 수 있습니다.

주요 기능

  • 앱 없데이트 없이 컨텐츠 변경하기
  • 코드 수정 없이 명언 추가
  • 코드 수정 없이 이름 숨기기
  • 명언을 앞뒤로 무한 스와이프 할 수 있음

사용 기술

  • Firebase remote config
  • ViewPager2
  • JSONObject

1.1. 기본 레이아웃 구성

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</FrameLayout>

기본적인 레이아웃은 프레임 레이아웃 안에 뷰 페이저와 프로그레스 바를 가지고 있는 형태입니다. 뷰페이져에서 명언을 보여주도록 하고 원격 에서 명언을 가져올 때 로딩 완료 까지 프로그래스 바로 로딩 표시를 보여줍니다.

2. ViewPager2

viewpager2 의 경우에는 1보다 개선된 버전이라고 보면 되는데 특징으로는 가로와 세로 스와이프를 모두 지원하고, RTL 지원, modifiable 개선, 기존 뷰 페이저와는 다르게 recyclerView를 기반으로 함 등이 있습니다.

결과 적으로는 1보다 좋은 것이며 모두 2로 대체될 것이지만 현재 실무에서는 아직 까지 많이 안쓰인다고 합니다. (제가 들은 강의 기준.) 기존 1을 사용했던 것이 다수고 완전 대체하기에는 어렵다고 보는 것 같네요.

pager에 들어갈 각각 뷰를 정해주기 위해 아래와 같은 레이아웃을 추가로 구현해주었습니다. 간단하게 텍스트뷰 2개를 갖는 뷰 이며 명언과 작가를 표시하도록 하겠습니다.

data class

data class Quote(
    val quote: String,
    val name: String
)

뷰 페이져에서 사용할 데이터 클래스를 위와 같이 정의해주도록 하겠습니다. 간단하게 명언과 이름(작자)를 담는 데이터 클래스 입니다.

2.1. Adapter (어뎁터) 구현

RecyclerView.Adapter 를 상속 받아서 어뎁터를 구현합니다.

class QuotesPagerAdapter: RecyclerView.Adapter<QuotesPagerAdapter.QuoteViewHolder>() {

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

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuoteViewHolder {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}

onCreateViewHolder 에서는 viewType 형태의 아이템뷰를 위한 뷰홀더 객체를 생성합니다.

onBindViewHolder 에서는 position 에 해당하는 데이터를 뷰홀더의 아이템에 표시합니다. (현재 위치에 해당하는 데이터를 보여줌)

getItemCount 는 전체 아이템 개수를 반환하도록 합니다.

뷰 홀더의 경우에는 내부에 따로 클래스로 정의해주도록 할 것 입니다.

2.2. 뷰 홀더 클래스 작성 (ViewHolder class)

class QuoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // itemView : 우리가 정의 해준 레이아웃 뷰.
        private val quoteTextView: TextView = itemView.findViewById(R.id.quoteTextView)
        private val nameTextView: TextView = itemView.findViewById(R.id.nameTextView)

        @SuppressLint("SetTextI18n")
        fun bind(quote: Quote, isNameRevealed: Boolean) {
            // 어떻게 랜더링 할 것인가
            quoteTextView.text = "\"${quote.quote}\"" // 명언 내용 (큰따옴표 추가)

            // 원격 isNameRevealed 에 따라 분기 (Firebase 에서 설정된 불값을 가져와서 처리)
            if (isNameRevealed) {
                nameTextView.text = "- ${quote.name}" // 작가 (작대기 추가)
                nameTextView.visibility = View.VISIBLE
            } else {
                nameTextView.visibility = View.GONE
            }
        }
    }

뷰 홀더 클래스를 정의하면서 bind 매서드를 정의해서 어떻게 랜더링 할 것인지 정해주도록 합니다. 정의 해준 데이터 클래스를 받아와서 각각 텍스트뷰에 알맞게 바인딩되도록 해줍니다. 이때 명언의 경우 큰 따옴표를 추가하고 작자의 경우에는 -를 앞에 붙여 주어 좀 있어보이게 해줍니다.

추가적으로는 데이터 클래스에 더해 불 변수를 하나 받아서 작자를 표시하거나 미표시 하도록 하도록 해보았습니다.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuoteViewHolder =
        QuoteViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item_quote, parent, false)
        )

다음으로 onCreateViewHolder 에서 설정해준 레이아웃으로 inflate 시켜주어야합니다.

    override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
        val actualPosition = position % quotes.size // 무한 스크롤을 할 것 이므로 사이즈로 나눈 나머지로 위치 정함.
        holder.bind(quotes[actualPosition], isNameRevealed)
    }

onBindViewHolder 에서는 현재 포시션을 가지고 아까 위에서 정의해준 bind 매서드를 호출해서 현재 위치에 따라 알맞은 명언이 표시될 수 있도록 구현해줍니다. 또한 무한 스크롤을 적용시켜 주기 위해서 시작 포지션을 Int 의 max 값의 중간으로 해줄것이기 때문에 위와 같이 구현해주었습니다.

무한 스크롤이라고 하지만 사실은 내부적으로 정수 자료형 최대값의 중간에서 시작해서 보여주는 것이죠. 유저가 상상이상으로 스크롤하는 경우는 매우 드물기 때문에 이런식으로 구현할 수도 있다는 것을 알고 가시면 좋을것 같습니다.

    override fun getItemCount(): Int = Int.MAX_VALUE //quotes.size

getItemCount 의 경우에 무한 스크롤 기능을 위해 정수형 최대값을 반환하도록 해주었습니다.

2.3. Firebase Remote config

파이어베이스 프로젝트를 추가하고 위처럼 매개변수를 추가해주도록 합니다. json 형식으로 명언들을 담아주고 이를 파싱해서 어플에서 보여주도록 하겠습니다.

    private fun initData() {
        // remoteConfig 설정
        val remoteConfig = Firebase.remoteConfig

        // 비동기로 설정되게
        remoteConfig.setConfigSettingsAsync(
            remoteConfigSettings {
                minimumFetchIntervalInSeconds = 0 // 앱을 들어올 때마다 패치 하도록.
            }
        )

        remoteConfig.fetchAndActivate().addOnCompleteListener {
            // 패치 작업이 완료 된 경우
            progressBar.visibility = View.GONE // 로딩창 숨김

            if (it.isSuccessful) {
                // json 을 파싱하여 배열로 가져옴.
                val quotes: List<Quote> = parseQuotesJson(remoteConfig.getString("quote"))
                val isNameRevealed: Boolean = remoteConfig.getBoolean("is_name_revealed")

                displayQuotesPager(quotes, isNameRevealed)

            }
        }
    }

remote config를 설정해주고 어플을 열때 마다 패치하도록 위와 같이 코드를 작성합니다. addOnCompleteListener 로 패치 작업이 완료되었을 때 작업을 지정해줄 수 있습니다. 패치 작업 완료시 로딩창을 GONE 으로 해주고 성공적으로 패치된 경우에 데이터를 가져옵니다.

불 변수는 따로 파싱함수를 작성할 필요는 없으나 명언의 경우 json 데이터이기 때문에 따로 parseQuotesJson 함수를 작성해주었습니다.

parseQuotesJson

private fun parseQuotesJson(json: String): List<Quote> {
        val jsonArray = JSONArray(json) //JSONObject 로 구성 되어있는 배열
        var jsonList = emptyList<JSONObject>()

        // JSONArray 자체에서 foreach 등의 기능 제공하지 않으므로 아래와 같은 형태로 배열 생성 하였음.
        for (index in 0 until jsonArray.length()) {
            val jsonObject = jsonArray.getJSONObject(index)
            // null 이 아니면 리스트에 추가
            jsonObject?.let {
                jsonList = jsonList + it
            }
        }

        // Quote 리스트로 변환
        // map 을 사용하여 각 개체(JSONObject) 마다 Quote로 변환하여 Quote 리스트로 만듦.
        return jsonList.map {
            Quote(
                it.getString("quote"),
                it.getString("name")
            )
        }

    }

json string을 받아서 JSONArray로 변환하여 이를 각각 리스트에 추가해주도록 하는 코드입니다. 이후에 반환 시 map을 사용하여 Quote 배열로 변환해서 반환하게 되며 이 데이터들을 사용해서 뷰 페이져에 뿌리도록 하면 되겠습니다.

    private fun displayQuotesPager(quotes: List<Quote>, isNameRevealed: Boolean) {
        // 어뎁터를 생성하여 할
        val adapter = QuotesPagerAdapter(
            quotes,
            isNameRevealed
        )

        viewPager.adapter = adapter
        // 중앙에서 시작당 하도록 (그래야 좌우 모두 무한 스크롤 가능하기 때문) 결국 끝에 도달하지만 사용자가 그렇게 하는 경우는 드뭄
        // smoothScroll 의 경우 부드러운 스크롤인데 중앙으로 그냥 한번에 보여줘야 하기 때문에 false 로 설정.
        viewPager.setCurrentItem(adapter.itemCount / 2, false)

    }

해당 데이터 리스트를 받아서 작성해준 어뎁터를 생성하며 넘기면 완료입니다. 어뎁터를 연결하고 setCurrentItem 을 무한 스크롤이 정상적으로 작동하는 것 처럼 보이게 하기 위해서 전체 개수(int형 max값)의 절반으로 이동시켜 주도록 합니다.

setCurrentItem의 두번 째 인자의 경우 smoothScroll 인데 초기에는 바로 중앙으로 넘겨 보여주어야 하기 때문에 false 로 넘겨줍니다.

2.4. 슬라이드 형식으로 넘기기 효과 애니메이션

    private fun initViews() {

        // 뷰페이저 넘기는 방식 설정
        viewPager.setPageTransformer { page, position ->
            // position : 현재 보이는 화면에서 상대적으로 어느 위치에 있는지

            when {
                position.absoluteValue >= 1F -> {
                    page.alpha = 0F
                }

                position == 0F -> {
                    page.alpha = 1F
                }

                else -> {
                    // 절반 이상 넘길 때 부터 급격하게 투명해지도록 설정.
                    page.alpha = 1F - 2 * position.absoluteValue
                }
            }
        }
    }

뷰 페이저에서 스크롤 할 때 넘기는 효과를 주기 위해서는 setPageTransformer 를 사용하면 됩니다. 이때 페이지의 위치에 따라서 알파값을 주어 넘어갈 때 서서히 사라지거나 나타나도록 설정해줄 수 있습니다.

위 코드는 넘어갈 때 절반 이상 부터는 급격하게 투명해지도록 설정해서 전환효과를 주었습니다.

해당 프로젝트에서 사용된 자세한 코드와 파일들은 저의 깃허브 저장소에서 확인할 수 있습니다.

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함