ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android 개발 종합반] 3주차 - Mbti 테스트 (뷰페이저 ViewPager2)
    안드로이드 2024. 2. 28. 18:34

    ✏️ TIL(Today I Learned)

    오늘은 Mbti 테스트 어플리케이션을 만들면서 ViewPager2, Fragment에 대해 배웠다.전에 성인애착유형 테스트를 만들어 본 적이 있는데, 그떄는 그냥 테스트 Activity를 여러 개 만들어서 intent()를 사용하는 방식으로 진행했었다. 당시 코드를 작성하면서도 굉장히 비효율적이라고 생각했는데, 이번 프로젝트에 사용한 ViewPager2로 많은 수의 프래그먼트를 관리하는 데 효율적이며, 메모리 사용을 최적화하는 방식을 알게 되어서 한층 성장한 것 같다.

     

    📝 공부한 Kotlin 요약 정리

    [ ViewPager2 ]

    ViewPager: 뷰페이저는 안드로이드 UI 컴포넌트 중 하나로, 여러 개의 페이지를 좌우로 슬라이드하여 표시할 수 있는 컨테이너이다. ViewPager2는 ViewPager의 개선된 버전으로, 더 많은 기능과 유연성을 제공한다. ViewPager2의 주요 특징과 장점은 아래와 같다.

    • 수평 및 수직 스와이프 지원: 수평 및 수직으로 스와이프하여 페이지를 전환 가능하다.
    • RTL(우측에서 좌측) 및 자동 슬라이딩 지원: RTL(Right-To-Left) 레이아웃 및 자동 슬라이딩 기능을 지원한다.
    • RecyclerView 기반 구현: RecyclerView와 거의 유사한 방식으로 작동하며, RecyclerView의 기능과 유연성을 상속받았다.
    • 프래그먼트 및 뷰(Adapter) 지원: 프래그먼트와 뷰 어댑터(Adapter)를 사용하여 페이지의 콘텐츠를 제공한다.
    • 상태 저장 및 복원: 프래그먼트 상태 저장 및 복원을 자동으로 처리한다. 따라서 앱이 일시 중단되거나 다시 시작될 때 페이지의 상태가 보존된다.
    • 스냅 효과 지원: 페이지 스크롤 중에 스냅 효과를 제공하여 사용자에게 페이지 전환을 시각적으로 나타낸다.

    ViewPager2의 주요 Adapter: ViewPager2를 구성하고 데이터를 관리하는 다양한 방법을 제공

    출처: https://recipes4dev.tistory.com/148

    • FragmentStateAdapter:
      각 페이지가 프래그먼트인 경우에 사용한다. ViewPager2가 프래그먼트 간의 상태를 보존하고 관리한다. 데이터 세트가 변경될 때 페이지를 동적으로 생성하거나 제거하여 메모리를 효율적으로 관리한다.
    • FragmentPagerAdapter:
      FragmentStateAdapter와 유사하지만, 페이지의 상태를 영구적으로 보존하지 않는다. 따라서 페이지 간 전환이 더 빠르지만, 메모리 사용량은 더 많을 수 있다. 일반적으로 페이지 수가 적을 때 사용한다.
    • RecyclerView.Adapter:
      ViewPager2에 표시될 각 페이지의 뷰를 제공하는 데 사용한다. 프래그먼트가 아닌 일반적인 뷰를 ViewPager2에 표시하고 싶을 때 사용한다. RecyclerView.Adapter를 확장하여 ViewPager2 어댑터를 만들고, onCreateViewHolder() 및 onBindViewHolder() 메서드를 사용하여 각 페이지의 뷰를 관리한다.

     

    🔎 전체 코드

    ViewPager가 표시되는 화면의 주체를 TestActivity로 했다. 우선 레이아웃을 아래와 같이 구성한다.

    <?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=".TestActivity">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

     

    TestActivity의 onCreate() 메서드에서 ViewPager의 adapter 속성을 ViewPagerAdapter로 설정하여 ViewPager가 ViewPagerAdapter와 상호작용할 수 있도록 한다. 이로써 ViewPager는 프래그먼트를 적절히 화면에 표시하는 것이다.

     

    즉, ViewPager를 TestActivity에 추가하고 ViewPagerAdapter를 설정함으로써 TestActivity는 여러 개의 QuestionFragment를 관리하고 슬라이딩을 통해 사용자에게 아래와 같은 질문페이지를 보여준다. 

    class TestActivity : AppCompatActivity() {
    
        private lateinit var viewPager: ViewPager2
        val questionnaireResults = QuestionnaireResults()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            // ViewPager2 설정
            viewPager = findViewById(R.id.viewPager)
            viewPager.adapter = ViewPagerAdapter(this)
            viewPager.isUserInputEnabled = false
        }
    
        // 다음 질문으로 이동하는 메서드
        fun moveToNextQuestion() {
            Log.d("jblee","viewPager.currentItem = ${viewPager.currentItem}")
    
            if (viewPager.currentItem==3) {
                // 마지막 질문일 경우 결과 화면으로 이동
                Log.d("jblee","result = ${ArrayList(questionnaireResults.results)}")
                val intent = Intent(this, ResultActivity::class.java)
                intent.putIntegerArrayListExtra("results", ArrayList(questionnaireResults.results))
                startActivity(intent)
            } else {
                // 다음 질문으로 이동
                val nextItem = viewPager.currentItem + 1
                if (nextItem < viewPager.adapter?.itemCount ?: 0) {
                    viewPager.setCurrentItem(nextItem, true)
                }
            }
        }
    }
    
    // 질문에 대한 결과를 저장하는 클래스
    class QuestionnaireResults {
        val results = mutableListOf<Int>()
    
        // 사용자 응답을 결과 목록에 추가하는 메서드
        fun addResponses(responses: List<Int>) {
            val mostFrequent = responses.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key
            mostFrequent?.let { results.add(it) }
        }
    }

     

    FragmentStateAdapter를 상속받은 ViewPagerAdapter 클래스ViewPager가 요청하는 각 페이지에 대한 프래그먼트(이 프로젝트에서는 QuestionFragment)를 생성하고 제공한다.

    // ViewPagerAdapter 클래스: ViewPager에 프래그먼트를 제공
    class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
    
        // ViewPager에 표시할 프래그먼트 개수를 반환
        override fun getItemCount(): Int {
            return 4 // 총 4개의 질문페이지가 있으므로 반환 값은 4
        }
    
        // position에 해당하는 프래그먼트를 생성하여 반환
        override fun createFragment(position: Int): Fragment {
            // position에 해당하는 질문을 가진 QuestionFragment 인스턴스를 생성
            return QuestionFragment.newInstance(position)
        }
    }

     

    QuestionFragment는 ViewPager 내에서 질문페이지를 나타내는데 사용한다. QuestionFragment 인스턴스는 ViewPager에서 하나의 페이지를 나타낸다. ViewPager는 각 페이지를 Fragment로 표시하며, 따라서 QuestionFragment의 인스턴스를 생성하면 화면에 하나의 질문페이지가 생성되는 것이다.

    class QuestionFragment : Fragment() {
        private var questionType: Int = 0
    
        // 질문 리스트 관련 코드 생략
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            arguments?.let {
                questionType = it.getInt(ARG_QUESTION_TYPE)
            }
        }
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // Fragment의 레이아웃을 인플레이트
            val view = inflater.inflate(R.layout.fragment_question, container, false)
    
            // 질문 설정 관련 코드 생략
    
            return view
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
    
            // answerRadioGroups 관련 코드 생략
    
            val btnNext: Button = view.findViewById(R.id.btn_next)
            btnNext.setOnClickListener {
    
                // 모든 질문에 대한 응답이 완료되었는지 확인
                val isAllAnswered = answerRadioGroups.all { it.checkedRadioButtonId != -1 }
    
                if (isAllAnswered) {
                    // 각 질문에 대한 응답을 수집
                    val responses = answerRadioGroups.map { radioGroup ->
                        val firstRadioButton = radioGroup.getChildAt(0) as RadioButton
                        if (firstRadioButton.isChecked) 1 else 2
                    }
    
                    // 응답을 TestActivity의 questionnaireResults에 추가하고 다음 질문으로 이동
                    (activity as? TestActivity)?.questionnaireResults?.addResponses(responses)
                    (activity as? TestActivity)?.moveToNextQuestion()
                } else {
                    Toast.makeText(context, "모든 질문에 답해주세요.", Toast.LENGTH_SHORT).show()
                }
            }
    
            // 마지막 질문일 경우 버튼 텍스트를 "결과 확인"으로 변경
            if(questionType==3){
                btnNext.setText("결과 확인")
            }
        }
    
        companion object {
            private const val ARG_QUESTION_TYPE = "questionType"
    
            // QuestionFragment 인스턴스를 생성하는 정적 메서드
            fun newInstance(questionType: Int): QuestionFragment {
                val fragment = QuestionFragment()
                val args = Bundle()
                args.putInt(ARG_QUESTION_TYPE, questionType)
                fragment.arguments = args
                return fragment
            }
        }
    }

    📌 참고: https://recipes4dev.tistory.com/148

Designed by Tistory.