토이 프로젝트

황규빈·2021년 4월 20일

remote config를 통한 데이터 추가 & 실시간 정보 반영

파이어베이스 Remote Config

기능
코드 수정 없이 데이터 추가
코드 수정 없이 데이터 노출 변경
무한 스와이프

사용한 기술 스택
1. Firebase Remote Config
2. View Pager2
3. RecylerView & Adapter

  1. View Pager에서 발전한 라이브러리
  2. 실무에선 아직 ViewPager를 많이 사용중
  3. 장점
    [1] 세로 방향 스와이프 지원
    [2] 오른쪽에서 왼쪽 지원(rtl)
    [3] 수정 가능한 프래그먼트 컬렉션
    [4] DiffUtil
    ViewPager2는 RecyclerView를 기반으로 빌드되므로 DiffUtil 유틸리티 클래스에 액세스할 수 있습니다. 이로 인해 여러 이점을 얻을 수 있는데, 무엇보다도 ViewPager2 객체는 기본적으로 RecyclerView 클래스의 데이터세트 변경 애니메이션을 활용할 수 있습니다.
  4. 사용하기
    [1] xml
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

[2] gradle
디폴트에 이미 할당되어 있음
implementation 'com.google.android.material:material:1.3.0'
[3] 랜더링 xml(아이템 레이아웃)

      <?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">

      <TextView
          android:id="@+id/quoteTextView"
          android:layout_width="0dp"
          android:layout_height="wrap_content"
          android:layout_marginHorizontal="40dp"
          android:ellipsize="end"
          android:gravity="end|center_vertical"
          android:maxLines="6"
          android:textSize="30sp"
          app:layout_constraintBottom_toTopOf="@id/nameTextView"
          app:layout_constraintEnd_toEndOf="parent"
          app:layout_constraintStart_toStartOf="parent"
          app:layout_constraintTop_toTopOf="parent"
          app:layout_constraintVertical_bias="0.4"
          app:layout_constraintVertical_chainStyle="packed"
          tools:text="나는 생각한다 고로 나는 존재한다." />

      <TextView
          android:id="@+id/nameTextView"
          android:layout_width="0dp"
          android:layout_height="wrap_content"
          android:layout_marginTop="15dp"
          android:ellipsize="end"
          android:gravity="end|center_vertical"
          android:maxLines="1"
          android:textSize="20sp"
          app:layout_constraintBottom_toBottomOf="parent"
          app:layout_constraintEnd_toEndOf="@id/quoteTextView"
          app:layout_constraintStart_toStartOf="@id/quoteTextView"
          app:layout_constraintTop_toBottomOf="@id/quoteTextView"
          tools:text="데카르트" />

      </androidx.constraintlayout.widget.ConstraintLayout>

[4] 데이터 클래스 추가

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

[5] 페이지 어뎁터 추가

class QuotesPagerAdapter(
	private val quotes: List<Quote>,
    private val isNameRevealed: Boolean
) : RecyclerView.Adapter<QuotesPagerAdapter.QuoteViewHolder>() {

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

    override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
    	val actualPosition = position % quotes.size
    	holder.bind(quotes[actualPosition], isNameRevealed)
    }

    override fun getItemCount() = Int.MAX_VALUE

    class QuoteViewHolder(itemView: View) : RecyclerView.ViewHolder(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}\""

    		if(isNameRevealed) {
    			nameTextView.text = "- ${quote.name}"
    			nameTextView.visibility = View.VISIBLE
    		} else {
    			nameTextView.visibility = View.GONE
    		}
    	}
    }
}

[6] 실제 뷰 페이져 연동하기(main)

  private val viewPager: ViewPager2 by lazy {
        findViewById(R.id.viewPager)
    }
  val adapter = QuotesPagerAdapter(listOf(Quote("content", "author")))
  viewPager.adapter = adapter
  1. 정의
    [1] 앱 업데이트를 게시하지 않아도 하루 활성 사용자 수 제한 없이 무료로 앱의 동작과 모양을 변경할 수 있습니다.

  2. 원리

  3. 업데이트
    사용자가 승인해야 하는 앱 업데이트에는 원격 구성을 사용하지 마세요. 무단 업데이트는 앱의 신뢰성을 해칠 수 있습니다.

  4. 제약사항
    원격 구성 매개변수 및 조건에는 특정한 한도가 적용됩니다.
    Firebase 프로젝트에서 최대 2,000개의 매개변수와 최대 500개의 조건을 사용할 수 있습니다. 매개변수 키의 길이는 최대 256자이고 밑줄 또는 영문자(A~Z, a~z)로 시작해야 하며 숫자도 포함할 수 있습니다. 한 프로젝트에서 매개변수 값 문자열의 총 길이는 800,000자를 초과할 수 없습니다.

  5. 활용사례
    https://firebase.google.com/docs/remote-config/use-cases?authuser=0
    [1] 사용자 집단 10%에서 기능과 관련하여 만족할 만한 안정성을 얻었다면 사용자 집단을 30%, 50%까지 늘리고 기능에 대한 확신이 생긴 후 100%로 확대합니다.
    [2] 앱의 플랫폼 및 언어별 프로모션 배너 정의
    배너에 보여줄 홍보, 안내 내용들을 변경할 떄 사용
    -> 자주 사용됨
    [3] 제한된 테스트 그룹에서의 새 기능 테스트 등등

  6. 로딩전략
    서버에서 페치해오는 타이밍이랑, 실제로 앱을 통해 보여지는 타이밍이 다르기 떄문에로딩전략이 요구됨
    [1] 로드시 가져와 활성화
    이 전략에서는 앱을 처음 시작할 때 fetchAndActivate()를 호출하여 원격 구성에서 새 값을 가져와 로드가 완료되는 즉시 활성화합니다. 이 간단한 방식은 UI 모양이 크게 변경되지 않는 구성 변경에 적합하지만, 사용자가 사용하는 동안 UI가 눈에 띄게 변경될 수 있는 상황에서는 사용하지 말아야 합니다.
    [2] 로딩 화면 뒤에서 활성화(가장 흔한 전략)
    전략 1에서 발생할 수 있는 잠재적인 UI 문제를 해결하기 위해 로딩 화면을 사용할 수 있습니다. 앱을 즉시 시작하지 않고 로딩 화면을 표시하고 완료 핸들러에서 fetchAndActivate를 호출합니다. 그런 다음 즉시 콜백 또는 알림을 다시 사용하여 로딩 화면을 닫고 사용자가 앱과 상호작용을 시작할 수 있도록 합니다.
    이 전략을 사용하는 경우 로딩 화면에 제한 시간을 추가하는 것이 좋습니다. 우수한 앱 시작 환경을 기대하는 사용자에게 원격 구성의 제한 시간 1분은 너무 길 수도 있습니다.
    [3] 다음 시작시 새로운 값 로드
    효과적인 전략으로 새 구성 값을 로드하여 앱의 다음 시작 시 활성화하는 방법이 있습니다. 이 전략에서 앱은 시작 시 가져온 값을 활성화한 후 새 값을 가져오며 새 구성 값을 이미 가져왔더라도 아직 활성화하지 않았다고 가정하고 작동합니다. 이 전략의 작업 순서는 다음과 같습니다.
    시작 시 이전에 가져온 값을 즉시 활성화합니다. 이전 세션에서 서버로부터 다운로드한 모든 값을 적용하며 이 과정이 거의 즉각적으로 이루어집니다.
    사용자가 앱과 상호작용하는 동안 가져오기 간격 최솟값(기본값)에 따라 비동기 호출을 시작하여 새 값을 가져옵니다.
    가져오기 호출의 완료 핸들러 또는 콜백에서는 아무 작업도 하지 않습니다. 앱은 다운로드한 값을 다음에 앱을 시작하여 활성화할 때까지 그대로 유지합니다.
    이 전략에서는 사용자 대기 시간이 크게 줄어들지만 사용자가 앱을 다시 실행해야 최신 구성을 볼 수 있습니다. 따라서 비즈니스 및 앱 로직과 이 고려사항 사이에 균형을 맞춰야 합니다.
    -> 빠른 변화를 목적으론 부적합
    [4] 주의사항
    방금 종료한 프로모션과 관련된 옵션을 삭제하는 것과 같이 분명한 앱 또는 비즈니스 이유가 있는 경우 외에는 사용자가 UI를 보거나 상호작용하는 동안 UI 모양을 업데이트하거나 전환하지 마세요.
    동시 가져오기 요청을 대량으로 보내지 마세요. 서버에서 앱을 제한할 수 있습니다. 대부분의 프로덕션 시나리오에서는 이러한 상황이 발생할 위험이 적지만 개발 진행 중에는 문제가 될 수 있습니다. Android 및 iOS에 대한 제한 안내를 확인하세요.
    [5] 제한
    앱에서 단기간에 가져오기를 너무 많이 수행하면 가져오기 호출이 제한되고 SDK는 FirebaseRemoteConfigFetchThrottledException을 반환합니다. SDK 버전 17.0.0 이전에는 60분 동안 가져오기 요청 수가 5회로 제한되었지만 최신 버전에서는 좀 더 많이 허용됩니다.
    앱 개발 단계에서는 앱을 개발하고 테스트할 때 빠르게 반복할 수 있도록 구성 가져오기와 활성화를 자주(한 시간에 몇 번씩) 해야할 수도 있습니다. 개발자가 10명 이하인 프로젝트에서는 빠르게 반복할 수 있도록 앱의 FirebaseRemoteConfigSettings 객체에 임시로 가져오기 간격 최솟값(setMinimumFetchIntervalInSeconds)을 낮게 설정할 수 있습니다.
    원격 구성의 가져오기 간격 최솟값은 12시간(기본값)입니다. 즉, 실제로 발생하는 가져오기 호출 수에 관계없이 12시간 동안 백엔드에서 구성을 한 번만 가져올 수 있습니다. 가져오기 간격 최솟값은 구체적으로 다음과 같은 순서로 결정됩니다.
    fetch(long)의 매개변수
    FirebaseRemoteConfigSettings.setMinimumFetchIntervalInSeconds(long)의 매개변수
    기본값 12시간
    가져오기 간격 최솟값을 커스텀 값으로 설정하려면 FirebaseRemoteConfigSettings.Builder.setMinimumFetchIntervalInSeconds(long)를 사용하세요.

  7. 사용하기
    [1] 설정하기
    (0) 프로젝트 만들기
    https://firebase.google.com/docs/android/setup?authuser=0
    (1) 다운받은 파일(json)을 위치에 추가
    (2) 그래들에 설정 추가

dependencies {
    classpath 'com.google.gms:google-services:4.3.5'  // Add Google Services plugin
}

apply plugin: 'com.google.gms.google-services'  // Add Google Services plugin

dependencies {
    implementation platform('com.google.firebase:firebase-bom:26.5.0')
    implementation 'com.google.firebase:firebase-config-ktx'
    implementation 'com.google.firebase:firebase-analytics-ktx'
}

[2] publish 진행(파이어베이스 콘솔 사이트)
(0) Parameter key와 JSON 추가
(1) 디스플레이 설정 옵션인 is_name_revealed 와 boolean vlaue를 넣어준다
(2) publish
[3] 코드 작성

val remoteConfig = Firebase.remoteConfig
remoteConfig.setConfigSettingsAsync(
	remoteConfigSettings {
		minimumFetchIntervalInSeconds = 0
	}
)

remoteConfig.fetchAndActivate().addOnCompleteListener {
	if (it.isSuccessful) {
		val quotes = parseQuotesJson(remoteConfig.getString("quotes"))
		val isNameRevealed = remoteConfig.getBoolean("is_name_revealed")
		displayQuotesPager(quotes, isNameRevealed)
	}
}
  
private fun parseQuotesJson(json: String): List<Quote> {
	val jsonArray = JSONArray(json)
	var jsonList = emptyList<JSONObject>()
	
	for (index in 0 until jsonArray.length()) {
		val jsonObject = jsonArray.getJSONObject(index)
		jsonObject?.let {
			jsonList = jsonList + it
		}
	}

	return jsonList.map {
		Quote(
			quote = it.getString("quote"),
			name = it.getString("name")
		)
	}
}
  
private fun displayQuotesPager(quotes: List<Quote>, isNameRevealed: Boolean) {
	val adapter = QuotesPagerAdapter(
            quotes = quotes,
            isNameRevealed = isNameRevealed
        )
	viewPager.adapter = adapter
	viewPager.setCurrentItem(adapter.itemCount / 2, false)
}

[4] 디스플레이 옵션을 어뎁터가 받을 수 있도록 재설정

class QuotesPagerAdapter(
    private val quotes: List<Quote>,
    private val isNameRevealed: Boolean
) : RecyclerView.Adapter<QuotesPagerAdapter.QuoteViewHolder>() {

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

    override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
        val actualPosition = position % quotes.size
        holder.bind(quotes[actualPosition], isNameRevealed)
    }

    override fun getItemCount() = Int.MAX_VALUE

    class QuoteViewHolder(itemView: View) : RecyclerView.ViewHolder(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}\""

            if(isNameRevealed) {
                nameTextView.text = "- ${quote.name}"
                nameTextView.visibility = View.VISIBLE
            } else {
                nameTextView.visibility = View.GONE
            }
        }
    }
}

[5] 완성도 높이기
(1) 액션바 제거

<style name="Theme.Aoppart3chapter02" parent="Theme.MaterialComponents.DayNight.NoActionBar">

(2) status바 색상 변화

<item name="android:statusBarColor" tools:targetApi="l">@color/white</item>
<item name="android:windowLightStatusBar">true</item>

(3) 텍스트 스타일 조정

android:maxLines="6"
android:ellipsize="end"
app:layout_constraintVertical_bias="0.4"

(4) 로딩간 사용자에게 보여줄 프로그레스바 적용

<ProgressBar
	android:id="@+id/progressBar"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:layout_gravity="center" />
        
    
remoteConfig.fetchAndActivate().addOnCompleteListener {
	progressBar.visibility = View.GONE
}

(5) 페이지 이동 효과 주기
https://developer.android.com/reference/androidx/viewpager/widget/ViewPager.PageTransformer

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

		else -> {
			page.alpha = 1F - 2 * position.absoluteValue
		}
	}
}

(6) Page View 무한 스크롤링 구현

val actualPosition = position % quotes.size
holder.bind(quotes[actualPosition], isNameRevealed)
profile
어제보다 더 나음을 위해.

0개의 댓글