Android

(Kotlin) 개발 공부 3일차 - MBTI 테스트

돗개진 2024. 2. 28. 19:46

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 ]

ㄴ> MBTI 테스트 메인 화면, START 버튼을 누르면 질문지 첫 페이지로 이동된다
ㄴ> 외향형 - 내향형 질문지, 질문은 총 3가지로 과반수로 선택된 것에 따라 E/I 가 결정된다
ㄴ> 감각형 - 직관형 질문지
ㄴ> 사고형 - 감정형 질문지
ㄴ> 판단형 - 인식형 질문지
ㄴ> 결과 화면 및 다시 테스트 버튼

 

 

ㄴ> ViewPager2 를 사용할 때 질문지 페이지는 화면 전체 영역을 사용할 것이기 때문에 layout 설정을 모두 parent로 준다

 

※ 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 리스트에 추가