-
[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를 구성하고 데이터를 관리하는 다양한 방법을 제공
- 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 } } }
'안드로이드' 카테고리의 다른 글
[Kotlin 문법 강의] 3주차: Kotlin 객체 지향 프로그래밍의 기초 (0) 2024.03.06 [Kotlin 문법 강의] 2주차: Kotlin 프로그래밍의 기초 (0) 2024.03.05 [Kotlin 문법 강의] 1주차: Kotlin을 시작하기 전에 알아야할 내용 (0) 2024.03.05 [Android 개발 종합반] 2주차 - 로또번호생성기 (0) 2024.02.27 [Android 개발 종합반] 1주차 - BMI 계산기 (뷰 바인딩 View binding) (0) 2024.02.26