요약
- 사전준비 : 그라데이션 배경, 뷰페이저, 질문지
- xml : 코드와 팔레트로 화면 생성, 라디오그룹 생성
- kotlin : 뷰페이저 관련, 질문 응답처리, MBTI 테스트 완성 후 사용
- 문제해결 및 회고
1. 사전준비
1.1 그라데이션 배경 만들기
<gradient/>로 그라데이션 색깔 2개를 입력하면 된다.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#ff2d9a59"
android:endColor="#ff23729a"
android:angle="90"/>
</shape>
1.2 Viewpager 준비하기
1.2.1 라이브러리 추가 : Gradle - build.gradke.kts(Module:app) - viewpager2추가 - Sync
implementation("androidx.viewpager2:viewpager2:1.0.0")
1.2.2 어답터adapter : java - New - Kotlin class - ViewPagerAdapter.kt 생성
뷰페이저를 상속받아 만드는 방법. 빨간 전구 클릭(또는 우클릭 - generate - Implement Methods)하면 상속받을 멤버들을 보여주고 클릭하면 자동으로 생성된다. override해주기 위해 TODO를 지우고 return을 넣어준다. getItemCount()는 몇 개의 페이지를 사용할 건지, createFragment()는 QuestionFragment.newInstance(position)질문지로 새로 만들어 쓴다는 의미.
package com.android.mymbti_test
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return 4
}
override fun createFragment(position: Int): Fragment {
return QuestionFragment.newInstance(position)
}
}
1.3 질문지 준비하기
res - values - strings.xml 에 질문 리소스 생성
2. xml
2.1 코드와 팔레트로 화면구성하기
이미지를 레이아웃 크기에 맞춰서 넣을 경우, android : scaleType="fitCenter"
LinearLayout 배치방향을 정할 수 있다. android:orientation="vertical" 세로 "horizontal" 가로
xml의 주석처리 : <!-- -->
2.2 라디오그룹 만들기
RadioGroup : 라디오버튼을 그룹화한다. closing code를 써주고 안에 RadioButton들을 넣어준다.
3. kotlin
3.1 Viewpager 이해하기
페이지 간 스와이프로 전환할 수 있는 viewPager를 사용하면 설문지 Activity와 xml을 하나만 만들어 반복사용할 수 있다. xml에는 뷰페이저 위젯을 생성하고 Activity에 위젯과 어답터를 연결해줘야한다.
응답을 받아내야하므로 .isUserInputEnabled = false 스와이프시 다음 페이지가 나오는 것을 막겠다는 의미.
viewPager = findViewById(R.id.viewPager)
viewPager.adapter = ViewPagerAdapter(this)
viewPager.isUserInputEnabled = false
3.2 응답 처리하기
responses는 응답한 값들을 저장하는 공간, results는 응답한 값에서 최다값을 저장하는 공간이다. groupingBy{} 그룹으로 나눠서 eachCount()각 그룹의 개수를 세어 maxByOrNull{}최대값을 반환한다.(없으면 Null을 반환한다.) 그 키값을 results에 넣어줘서 응답한 값 중 최다값이 들어가게 되는 것이다. 이렇게 해주는 함수가 addResponses
2지선다인 본 문항의 경우 responses를 1번으로 답변한 그룹, 2번으로 답변한 그룹으로 나눠서 1번그룹 n개, 2번그룹 n개 중 더 큰 값을 results로 보낸다는 것.
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) }
}
}
3.3 페이지 넘어가기
//마지막 페이지(==3페이지)일 경우 : 결과 페이지로 intent한다.
//마지막 페이지가 아닐경우 : 마지막페이지가 아닌지 if문으로 확인 후 else로 다음페이지(현재 페이지+1)처리한다.
if문 해석 : 어답터의 아이템 수가 null일 경우 0을 반환하고 그렇지 않으면 1이상의 수가 반환되므로 null을 제외하고는 다음페이지로 넘어가는 것.
true는 다음페이지로 넘어갈 때 page scrolling이 되도록 옵션을 주겠다는 뜻.
fun moveToNextQuestion() {
if (viewPager.currentItem==3) {
//마지막 페이지일 경우
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)
}
}
}
3.4 질문지 만들기
QuestionFragment를 상속받은 newInstance는 위에서 넘겨준 nextItem다음페이지 번호를 정수로 받아 새로운 fragment로 띄워주는 기능이다. Bundle로 데이터를 받고 키값을 넣어주면 다음 페이지 번호를 받아 새로운 fragment로 return해주는 것. string에 있는 각 타이틀과 질문은 list로 담고 xml을 연결한다.
companion object {
private const val ARG_QUESTION_TYPE = "questionType"
fun newInstance(questionType: Int): QuestionFragment {
val fragment = QuestionFragment()
val args = Bundle()
args.putInt(ARG_QUESTION_TYPE, questionType)
fragment.arguments = args
return fragment
}
}
3.5 페이지에 맞는 타이틀로 변경하기
페이지에 맞는 타이틀로 바뀌도록 inflate로 레이아웃을 동적활용한다. 페이지 번호가 담겨있는 정수형 questionType을 인덱스로 넣어 list에 담아둔 타이틀이 일치하게 출력된다.
private var questionType: Int = 0
val view = inflater.inflate(R.layout.fragment_question, container, false)
//중략
title.text = getString(questionTitles[questionType])
adapter와 inflater
adapter : 다른 장치를 서로 연결해주는 결합도구. 데이터와 뷰 중간 매개체 역할을 하는 추상 인터페이스. 뷰페이저어답터는 뷰페이저에 보여질 데이터들을 연결해주는 역할.
inflater : inflate부풀리다. 올리다. xml로 만든 레이아웃을 객체화 시키는 과정. 그때 그때 다른 레이아웃을 넣을 수 있게 한다.
3.6 페이지에 맞는 질답 변경하기
for 반복문을 돌려 질문과 응답을 변경한다. indices는 index의 복수형태로 인덱스와 값에 접근할 수 있다. 페이지 번호가 담겨있는 정수형 questionType을 인덱스로 넣어 list에 담아둔 질문이 일치하게 출력된다. 질문은 2차원 배열로 list안에 list로 3개의 질문을 묶어놨기 때문에 한 페이지당 3개의 질문이 나타나는 것.
for (i in questionTextViews.indices) {
//질문
questionTextView[i].text = getString(questionTexts[questionType][i])
val radioButton1 = answerRadioGroups[i].getChildAt(0) as RadioButton
val radioButton2 = answerRadioGroups[i].getChildAt(1) as RadioButton
//답변
radioButton1.text = getString(questionAnswers[questionType][i][0])
radioButton2.text = getString(questionAnswers[questionType][i][1])
}
다차원 배열
배열에 값이 아닌 배열을 넣는 것. 차원의 개수만큼 인덱스의 개수도 늘어나는 것을 알 수 있다.
1차원 타이틀 : title.text = getString(questionTitles[questionType])
2차원 질문 : questionTextViews[i].text = getString(questionTexts[questionType][i])
3차원 답변 : radioButton1.text = getString(questionAnswers[questionType][i][0])
3.7 버튼 동작하기
onViewCreated 이하 라디오버튼과 다음페이지 버튼이 동작할 수 있도록 xml과 코드를 연결하고 모든 질문에 응답해야 넘어갈 수 있도록 확인한다. all은 true/false로 값이 나오는데 checkedRadioButtonId는 선택되지 않은 라디오버튼이 있는 경우 -1을 return하므로 부정연산자로 확인할 수 있다.
모두 응답한 true는 앞서 정리한 응답처리하기와 페이지 넘어가기, 그렇지 않은 false는 else로 예외처리한다.
모두 응답한 경우 : TestActivity에 넘겨준다.
응답 처리하기(사용자의 응답은 getChildAt(0)첫번째 라디오버튼 하나만 확인해서 체크되어 있다면 1, 그렇지 않으면 두번째 라디오버튼에 체크한 것이므로 2를 responses에 담는다. 응답은 앞서 정리한 addResponses로 최다값을 뽑아 results로 결과를 도출해야하므로 questionnaireResults로 TestActivity에 접근해 값을 넘겨준다.)
페이지 넘어가기(moveToNextQuestion으로 넘겨준다.)
그렇지 않은 경우 : 예외처리한다.
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
}
(activity as? TestActivity)?.questionnaireResults?.addResponses(responses)
(activity as? TestActivity)?.moveToNextQuestion()
} else {
Toast.makeText(context, "모든 질문에 답해주세요.", Toast.LENGTH_SHORT).show()
}
}
Fragment의 생명주기
(이전에 onAttach가 먼저 호출된다)
onCreate : 프래그먼트를 만든다. Fragment생성이후 호출
onCreateView :프래그먼트에 뷰를 올려준다. view를 호출
onViewCreated : 뷰를 사용할 수 있도록 만들어 준다. view의 return을 호출
3.8 결과 만들기
응답의 최다값은 results에 정수로 넘겨받았기 때문에 알파벳 문자로 변환해줘야한다. for 반복문을 돌려 += 알파벳 결과값을 문자열에 추가시키라는 뜻, [results[i] - 1]은 list는 0부터 시작하므로 결과값에서 1을 빼주는 것. 그렇게 만들어진 String타입의 결과값으로 결과화면 컴포넌트들에 연결한다.
결과 이미지는 ic_${}의 이름을 가진 이미지 파일을 꺼내온다. {resultString알파벳으로 변환한 결과값을.toLowerCase소문자로 바꾼다(Locale.ROOT)}
for (i in results.indices) {
resultString += resultTypes[i][results[i] - 1]
}
val imageResource = resources.getIdentifier("ic_${resultString.toLowerCase(Locale.ROOT)}",
"drawable", packageName)ivResImg.setImageResource(imageResource)
3.9 처음으로 돌아가기
MainActivity로 모든 작업을 깨끗하게 비워서 돌려보낸다.
btnRetry.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
4. 문제해결
4.1 문제 : Project Errors에 에러 1,2가 나타나면서 첫페이지에서 start버튼을 누르면 앱이 중단되는 상태
에러1. TestActivity.kt
Unresolved reference: it : 42
Unresolved reference: it : 42
Unresolved reference: it : 43
에러2. ViewPagerAdapter.kt
Unresolved reference : FragmentActivity : 7
Unresolved reference : FragmentActivity : 7
4.2 해결과정
1. 구글링 : 공통적으로 나타난 ' Unresolved reference'를 검색해보았는데 대부분 라이브러리나 id에서의 문제라 나는 같은 방법으로 해결되지 않았다.
2. 도움(팀원) : 팀원분들이 문제1을 해결하고자 import 수정, 코드위치 조정을 해보았으나 해결되지 않았다.
3. 도움(튜터) : 튜터님과 같이 로그캣을 확인하고 viewpager주석처리해본 다음, ViewPagerAdapter쪽의 문제일 것으로 추정. 상속에서 잘못된 부분을 찾아 해결했다.
4.3 복습
자식클래스 ViewPagerAdapter는 부모클래스 FragmentStateAdapter로부터 상속받는다는 뜻. 부모클래스 생성자에 파라미터 fragmentActivity가 있으므로 자식클래스에서 호출하여 값을 전달한다는 뜻이다.
class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {}
회고
어려웠던 만큼 배울 게 많았던 프로젝트였다.
보다 난이도 높은 코드를 짜볼 수 있었던 기회라는 점과 Viewpager로 Fragment 재사용하는 방법을 배울 수 있다는 점도 좋았지만 좀처럼 해결할 수 없었던 에러를 포기하지 않고 해결했다는 점이 가장 의미있었다.
이번 프로젝트의 에러는 구글링퍼스트로 해결되지 않는 문제라 좀 더 적극적인 도움이 필요했다. 그래서 팀원분들께 도움을 요청했는데 사랑스러운 팀원분들이 함께 들여다봐주었으나 아쉽게도 해결할 수 없었다. 설상가상으로 이것저것 시도했더니 갑자기 에러가 20개 가량 더 늘어나 일단 안드로이드 스튜디오와 작별했고 팀원분들은 다시 만들어보기를 권했다. 앞으로 더 큰 작업을 할테고 문제가 발생할 때마다 다시 만들 수는 없을 것이기 때문에 해결하는 힘을 기르고 싶다고 했다.
다시 켰더니 이번엔 아무런 에러도 찾을 수 없다고 하는데 여전히 start버튼을 누르면 앱이 중단됐다. 결국 튜터님을 찾아가 인사를 드렸고 문제가 되는 부분을 설명하니 화면공유를 요청하셔서 보여드렸다. 계속 질문하셨는데(이 코드는 무슨 의미냐, 이 코드가 동작하면 다음은 어떻게 되느냐, 여기서 맨 처음 동작하는 코드는 뭐냐 등등) 극초보인 나는 답변하느라 상당히 어지러웠지만 답답하셨을텐데도 묵묵히 기다리고 들어주셨기 때문에 모두 답변할 수 있었고 차분히 설명해주셨다.
내 답변에 따라 파일과 로그캣을 훑어보시더니 TestActivity에서 viewPager부분에 주석처리 후 실행시켰는데, 비록 레이아웃은 나타나지 않은 백지화면이었지만 처음으로 앱이 중지되지 않았다.(!!!) 그러자 튜터님께서 ViewPagerAdapter에서 분명히 잘못 친 부분이 있을 것이라 확신하셨는데 그럴 줄 알고 튜터님 찾아가기 전에 강의 파일과 내 코드들을 비교해보고 간 나는 잘못 친 부분은 없다고 당당하게 확신했다.(그러면 안됐다.) ViewPagerAdapter의 gerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity)에서 잘못쳤다. FragmentStateAdapter(FragmentActivity())라고 쳤었다. 상당히 부끄러웠지만 상속의 개념도 모른 상태라 잘못친 줄도 모른 것이다. 같은 실수를 반복하지 않도록 튜터님께 감사인사 후 강의에서 해당 내용으로 달려가 복습했다.
'안드로이드와 앱 > 안드로이드' 카테고리의 다른 글
안드로이드 앱개발 3주차 강의 [UI] (0) | 2024.06.19 |
---|---|
안드로이드 앱개발 강의 부록 (3) | 2024.06.19 |
안드로이드 앱개발 1 ~ 2주차 강의 [개요 ~ 프로젝트] (0) | 2024.06.18 |
사전캠프 2주차 강의 [로또번호 생성기] (0) | 2024.05.28 |
사전캠프 1주차 강의 [BMI계산기] (0) | 2024.05.27 |