BMI 계산기, 로또 번호 생성기에 이어서 이번에는 MBTI를 테스트하고 결과를 볼 수 있는 어플을 만들어 보자
MBTI를 검사하는 어플 특성상 질문지가 많기 때문에 페이지를 여러 개 사용할 수밖에 없는 구조인데 매 질문지마다 Activity를 생성해서 만드는 것은 번거로움이 있기 때문에 이를 해결하기 위해 'ViewPager2' 라는 라이브러리를 사용한다
[ ViewPager2 ]
: 페이지가 부드럽게 넘어가는 애니메이션 효과와 액티비티를 여러 개 생성하지 않고 getItemCount() 메소드를 오버라이드하여 내가 사용할 페이지 개수를 설정할 수 있다.
// ViewPagerAdapter는 FragmentStateAdapter를 상속받는다
class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return 4 // 4개의 페이지 사용
}
override fun createFragment(position: Int): Fragment {
return QuestionFragment.newInstance(position)
}
}
ㄴ> 위 코드는 ViewPagerAdapter 라는 클래스를 정의하는 코드인데 FragmentStateAdapter 를 상속받아 createFragment(position: Int) 메소드를 사용한다. 여기서 position은 Fragment의 위치를 뜻한다.
[ MBTI 테스트 UI - xml ]
※ ViewPager2 는 다른 xml 과 달리 androidx 로 태그를 시작한다
[ MBTI 테스트 주요 Activity 구성 - Kotlin ]
- MainActivity : btnStart 변수를 iv_start 와 연결, START 버튼을 클릭했을 때 다음 Activity(= TestActivity)로 넘김
// MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// btn_start 변수와 iv_start 연결
val btnStart = findViewById<ImageView>(R.id.iv_start)
btnStart.setOnClickListener{
// 새로운 Activity 호출
val intent = Intent(this@MainActivity, TestActivity::class.java)
startActivity(intent)
}
}
}
- TestActivity : AppCompatActivity 를 상속받고 ViewPager2를 초기화하고 설정하는 주요 Activity, 질문지에 대한 응답을 저장하는 공간과 다음 페이지로 넘어가는 조건 및 동작 구현
// TestActivity
// 하나의 Fragment 를 재사용 해서 효율성 증대
class TestActivity : AppCompatActivity() {
// onCreate 전에 전역 변수 추가 (onCreate의 시기 정리)
private lateinit var viewPager: ViewPager2
// lateinit = 늦은 초기화 가변 변수, viewPager는 현재 액티비티에 사용할 ViewPager2를 저장
val questionnaireResults = QuestionnaireResults()
// 질문지에 대한 응답을 저장하기 위해 사용, 이 인스턴스를 이용해서 addResponse가 있는 클래스에 접근
// onCreate : 액티비티가 생성될 때 호출되는 메소드, 레이아웃 설정 및 ViewPager2 초기화
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
// setConetenView : 화면에 표시할 레이아웃 설정
// (activity_test는 ViewPager2을 사용하기 위한 레이아웃 파일 xml)
viewPager = findViewById(R.id.viewPager) // ViewPager2 제어를 위한 변수
viewPager.adapter = ViewPagerAdapter(this) // 페이지 간 Fragement 제공 관리
viewPager.isUserInputEnabled = false // 스와이프로 페이지 넘김 방지
}
// 다음 페이지 이동 함수
fun moveToNextQuestion() {
if(viewPager.currentItem==3) { // viewPager의 현재 아이템이 마지막(3)인 경우
// 마지막 페이지 -> 결과 화면으로 이동 (currentItem : 현재 보여지고 있는 페이지의 인덱스)
val intent = Intent(this, ResultActivity::class.java)
intent.putIntegerArrayListExtra("results", ArrayList(questionnaireResults.result))
startActivity(intent)
} else { // 마지막 페이지가 아닐 때 -> 다음 페이지로 이동
val nextItem = viewPager.currentItem + 1
if(nextItem < viewPager.adapter?.itemCount ?: 0) { // 엘비스 연산자
viewPager.setCurrentItem(nextItem, true)
}
}
}
}
// 질문지에 대한 응답 저장 공간 (페이지당 제일 많이 나온 응답)
class QuestionnaireResults {
val result = mutableListOf<Int>()
// result : 페이지당 제일 많이 나온 응답을 저장하는 리스트
// 1, 1, 2 를 그룹화하여 각각 응답을 Count 하고 Max를 까고 가장 많은 게 key 값이 됨
fun addResponses(response: List<Int>) {
val mostFrequent = response.groupingBy{ it }.eachCount().maxByOrNull { it.value }?.key
// (groupingBy 함수: 응답 값을 키로 사용하여 '응답 리스트 그룹화')
// (eachCount(): 각 그룹의 개수를 계산하여 맵으로 반환, 어떤 응답이 몇 번인지 Count)
// (maxByOrNull { it.value }?.key: 최대값 찾기 it.value는 그룹의 개수를 나타냄
// -> ?.key는 가장 많이 나온 그룹의 키값 반환, ?. 는 maxxByOrNull이 null인 경우 대비)
mostFrequent?.let { result.add(it) } // mostFrequent의 key값을 result에 추가
// mostFrequent가 null이 아닌 경우(가장 많이 나온 응답이 존재)에만 실행됨
// let 함수, 블록 내의 코드를 실행 { result.add(it) } = 가장 많이 나온 응답을 result 리스트에 추가
}
}
- ResultActivity : 결과값을 가져와 문자열과 이미지로 표시하는 기능 및 재시도 버튼 설정
// ResultActivity
class ResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_result) // activity_result 레이아웃 설정
val results = intent.getIntegerArrayListExtra("results") ?: arrayListOf()
// ?: -> 결과값이 null 일 때 사용할 것 선언
val resultTypes = listOf(
listOf("E", "I"),
listOf("N", "S"),
listOf("T", "F"),
listOf("J", "P")
)
var resultString = ""
for(i in results.indices) {
resultString += resultTypes[i][results[i]-1]
}
// 결과값(resultString)을 결과값 뷰(tv_resValue)에 띄움
val tvResValue : TextView = findViewById(R.id.tv_resValue)
tvResValue.text = resultString
// 결과 이미지(를 결과 이미지 뷰에 띄움
val ivResImg : ImageView = findViewById(R.id.iv_resImg)
val imageResource = resources.getIdentifier("ic_${resultString.lowercase(Locale.ROOT)}", "drawable", packageName)
// getIdentifier : 리소스 식별자를 찾기 위해 리소스 이름을 사용
// 문자열 템플릿을 사용하여 ic_ 뒤에 오는 resultString의 소문자 버전 덧붙임
ivResImg.setImageResource(imageResource)
// 결과 이미지에 imgResource 로 가져온 이미지를 설정
// 재시도 버튼 설정
val btnRetry : Button = findViewById(R.id.btn_res_retry)
btnRetry.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
// 액티비티 스택 초기화 플래그를 추가하여 이전 화면들을 모두 지우고 새로운 액티비티 시작
startActivity(intent)
}
}
}
[ Kotlin 문법 간단 정리 (기억할 만한 것 위주) ]
- groupingBy: 응답 값을 키로 사용하여 '응답 리스트 그룹화'
- eachCount(): 각 그룹의 개수를 계산하여 맵으로 반환, 어떤 응답이 몇 번인지 Count
- maxByOrNull { it.value }?.key: 최대값 찾기 it.value는 그룹의 개수를 나타냄 -> ?.key는 가장 많이 나온 그룹의 키값 반환, ?. 는 maxxByOrNull이 null인 경우 대비
- getIdentifier: 리소스 식별자를 찾기 위해 리소스 이름을 사용하는 함수
- let { }: 함수 블록 내의 코드를 실행, ex) mostFrequent?.let { result.add(it) } = mostFrequent가 null이 아닌 경우, 가장 많이 나온 응답을 result 리스트에 추가
'Android' 카테고리의 다른 글
(Android Studio) - 레이아웃 종류와 쓰임새 (0) | 2024.03.19 |
---|---|
(Android Studio) 프로젝트 구조 (1) | 2024.03.18 |
(Android Studio) Activity 와 Fragment 의 차이점 (0) | 2024.02.29 |
(Kotlin) 개발 공부 2일차 - 로또 번호 생성기 (0) | 2024.02.27 |
(Kotlin) 개발 공부 1일차 - BMI 계산기 (1) | 2024.02.27 |