2024. 06. 19 시작 !
Lv.1 SignInActivity 로그인
조건 :
새 프로젝트를 만들고 MainActivity의 이름을 SignInActivity로 바꿔주세요.
로고 이미지는 원하는 이미지로 넣어주세요.
아이디, 비밀번호를 입력 받는 EditText를 넣어주세요.(미리보기 글씨(플레이스 홀더) 포함)
비밀번호 EditText는 입력 내용이 가려져야 합니다.(●●● 처리)
로그인 버튼을 누르면 HomeActivity가 실행되도록 구현합니다.(Extra로 아이디를 넘겨줍니다.)
아이디/비밀번호 모두 입력 되어야만 로그인 버튼이 눌리도록 구현합니다.(“로그인 성공”이라는 토스트 메세지 출력하도록 구현)
아이디/비밀번호 중 하나라도 비어 있다면 “아이디/비밀번호를 확인해주세요” 라는 토스트 메세지가 출력되도록 구현합니다.
회원가입 버튼을 누르면 SignUpActivity가 실행되도록 구현합니다.
플레이스 홀더는 hint로 입력한다.
팀원분께서 isEmpty()는 공백도 값으로 인정하기 때문에 isBlank()를 쓰는 것이 좋겠다고 알려주셔서 변경했다.
1레벨에서는 크게 문제 없었고 사진이 안들어갔는데 알고보니 기본 아바타 들어있던 tools에 입력해서 그렇다. android로 변경하니 됐다.
tools:srcCompat="@tools:sample/avatars"
android:src="@drawable/img"
Lv.2 SignUpActivity 회원가입
조건 :
SignpActivity를 생성해 주세요.
타이틀 이미지는 원하는 이미지로 넣어주세요.
이름, 아이디, 비밀번호 모두 입력 되었을 때만 회원가입 버튼이 눌리도록 구현합니다. 셋 중 하나라도 비어있으면 “입력되지 않은 정보가 있습니다” 라는 토스트 메세지를 출력하도록 구현합니다.
비밀번호 EditText는 입력 내용이 가려져야 합니다.(●●● 처리)
회원가입 버튼이 눌리면 SignInActivity로 이동하도록 구현합니다. (finish 활용)
선택사항 1.회원 가입 페이지에서 입력한 아이디/비밀번호가 로그인 화면에 자동으로 입력되기
어차피 액티비티 만드는김에 여기서 같이 진행 했는데 처음에 이렇게 했더니 자동입력은 되지만 회원가입 안하고 로그인만 하면 if문을 통과하지 못했다.
//로그인데이터
val idData = etId.text
val passwordData = etPassword.text
//회원가입데이터
val signUpIdData = intent.getStringExtra("idFromSignUpActivity")
etId.setText(signUpIdData)
val signUpPwData = intent.getStringExtra("pwFromSignUpActivity")
etPassword.setText(signUpPwData)
문맥에 따라 로그인화면에서 아이디와 비밀번호를 입력해도 회원가입데이터가 후순위로 덮어쓰기 때문이었다. 회원가입을 안했으니 값이 없는 상태로 덮어씌워져서 if문의 isBlank는 늘 true가 반환돼서 통과하지 못한거였다. 로그인데이터와 회원가입데이터의 순서를 바꾸니 됐다.
아이디와 비밀번호 외에 다른 값(이름, 나이, 성별)도 넘겨줘야하는데 하나하나 넘겨주면 비효율적일 것 같아서 튜터님께 여쭤봤다. 클래스로 넘겨주는 방법이 있는데 숙련챕터이므로 그런 방법이 있구나만 알면 되겠다고 하셨고, 배열은 어떻냐고 여쭤보니 배열로 넘겨받아서 분해하는 게 복잡한 과정이라고 말씀해주셔서 하나하나 넘기기로 했다.
//회원가입페이지에서 넘겨주기
val intent = Intent(this, SignInActivity::class.java)
intent.putExtra("idFromSignUpActivity",idData.toString())
intent.putExtra("pwFromSignUpActivity",passwordData.toString())
intent.putExtra("nameFromSignUpActivity",nameData.toString())
intent.putExtra("genderFromSignUpActivity",genderData)
intent.putExtra("ageFromSignUpActivity",ageData.toString())
startActivity(intent)
//로그인페이지에서 넘겨받기
val signUpIdData = intent.getStringExtra("idFromSignUpActivity")
etId.setText(signUpIdData)
val signUpPwData = intent.getStringExtra("pwFromSignUpActivity")
etPassword.setText(signUpPwData)
val signUpNameData = intent.getStringExtra("nameFromSignUpActivity")
val signUpGenderData = intent.getStringExtra("genderFromSignUpActivity")
val signUpAgeData = intent.getStringExtra("ageFromSignUpActivity")
처음에는 이렇게 구현했다. intent로 모든 값을 넘겼는데 동작은 문제 없었지만 registerForActivityResult로 넘기는 방법을 구현하는 게 선택과제였기 때문에 바꿔야했다. 여러 설명을 읽어봐도 이해가 안돼서 우선 따라쳤고 동작하는 걸 보고나서 설명을 다시 보니까 다행히 이해됐다. 특히 이 블로그에 설명이 잘 나와있어서 참고하여 정리했다.
[출처] registerForActivityResult (Kotlin)|작성자 tgyuu
registerForActivityResult()
이해하기 어려웠던 이유 1.
인자가 2개 필요하다지만 괄호 안에는 하나만 보여서 답답했다.
함수형 인자가 두번째 인자라는 설명이 너무 도움됐다.
(ActivityResultContracts.StartActivityForResult()) {
result : ActivityResult ->
이렇다.
(ActivityResultContracts.StartActivityForResult()) {
result : ActivityResult ->
첫 번째 인자 : ActivityResultContracts. 활동 결과 계약은 어떤 활동을 통해서 결과를 얻어올지 계약한다고 이해하면 쉽다.
ActivityResultContracts클래스에 정의된 활동 중 하나인 startActivityForResult(다른 화면에서 정보를 가지고 오는 방식)으로 계약한다.
두 번째 인자 : ActivityResultCallback. ActivityResult 객체가 인자로 들어오는데 타입은 추론 가능해 생략가능하지만 적었다. resultCode가 일치하는대로 계약활동을 불러와준다. 만약 버튼별 내용이 달라져야한다면 resultCode를 각각 다르게 설정하면 된다. resultCode는 안드로이드에 디폴트로 상수값이 저장되어 있다.
이해하기 어려웠던 이유 2. 인자가 너무 길어서 무슨 의미인지 파악하기 어려웠다.
하나하나 풀어서 설명해주고 코드가 무슨 의미인지 알려줘서 좋았다.
val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result : ActivityResult ->
if(result.resultCode == RESULT_OK) {
val signUpIdData = result.data?.getStringExtra("idFromSignUpActivity") ?: ""
val signUpPwdData = result.data?.getStringExtra("pwdFromSignUpActivity") ?: ""
val signUpNameData = result.data?.getStringExtra("nameFromSignUpActivity") ?: ""
val signUpGenderData = result.data?.getStringExtra("genderFromSignUpActivity") ?: ""
val signUpAgeData = result.data?.getStringExtra("ageFromSignUpActivity") ?: ""
etId.setText(signUpIdData)
etPwd.setText(signUpPwdData)
}
}
이렇게 회원가입 데이터를 지역변수로 받았더니 자기소개로 념겨줄 수가 없어서 전역변수로 바꿨다. id는 editText에 값이 입력되니 그대로 뒀고 password는 넘겨줄 필요 없어서 그대로 뒀다. 튜터님께서 변수를 만들지 않고 아예 etId.text로 값을 바꾸기만 하는 방법도 설명해주셨다.
Lv.3 HomeActivity 자기소개
조건 :
HomeActivity를 생성해 주세요.
SignInActivity에서 받은 extra data(아이디)를 화면에 표시합니다.
ImageView, TextView 외에 각종 Widget을 활용해 자유롭게 화면을 디자인 해주세요.
이름, 나이, MBTI 등 자기소개등이 들어가는 위젯을 자유롭게 디자인해주세요.
종료 버튼이 눌리면 SignInActivity로 이동하도록 구현합니다. (finish 활용)
선택사항 1.회원 가입 페이지에서 입력한 아이디/비밀번호가 로그인 화면에 자동으로 입력되기
사진 넣는김에 여기서 같이 했다. 랜덤은 생각보다 쉽게 됐다. Random.nextInt() 범위는 Int,Int로 지정하고 Int로 반환된다.
//랜덤사진
when(Random.nextInt(1,6)) {
1 -> ivroom.setImageResource(R.drawable.img_room_1)
2 -> ivroom.setImageResource(R.drawable.img_room_2)
3 -> ivroom.setImageResource(R.drawable.img_room_3)
4 -> ivroom.setImageResource(R.drawable.img_room_4)
5 -> ivroom.setImageResource(R.drawable.img_room_5)
}
여러가지 뷰를 써보고 싶어서 라디오버튼을 넣어봤다. 라디오그룹으로 하는 이유는 중복 선택을 막을 수 있고 다른 버튼 클릭하면 취소도 된다. 이벤트도 함께 지정할 수 있으므로 권장된다. orientation을 horizontal로 해서 가로로 배치했다. Activity에서는 setOnCheckedChangeListener{group,i ->}로 이벤트를 설정할 수 있다. 라디오버튼이 안돼서 한참 헤맸는데 intent 키값을 잘못써서 그런거였다..... 나의 오류 80%는 스펠링문제다.
rgGender.setOnCheckedChangeListener { radioGroup, checkedId ->
when (checkedId) {
R.id.rb_male -> genderData = "남성"
R.id.rb_female -> genderData = "여성"
}
}
2024. 06. 24 피드백 !
검토받으러 갔는데 갑자기 id를 HomeActivity로 넘겨주는게 안돼서 매우 당황스러웠다.
id editText에 text받는 변수를 로그인 버튼 Listener 안으로 넣으니 됐다.
그것만 해결하고 선택과제까지 모두 통과됐다!!! 그런데 도전과제가 추가로 주어졌다.
(....골인 앞에서 '다시 출발선으로 가세요' 카드 뽑은 기분)
1. 버튼액션
HomeActivity에 버튼 만들기
조건 :
ConstraintLayout 을 이용해 버튼 모양의 레이아웃을 만듭니다.
레이아웃 안에 아이콘과 텍스트를 배치합니다. (합쳐서 중앙 정렬)
레이아웃 안에 레이아웃을 넣는 게 적응이 잘 안되지만 중첩하고 그 안에 TextView와 ImageView를 배치했다. 본래 있던 버튼의 아이디를 그대로 물려주되 Activity와 View를 연결해주는 변수에 타입 Button을 ConstraintLayout으로 변경해줘야한다는 점을 주의 다른 분도 이 부분 때문에 막혀서 먼저 고생한 내가 도움을 드릴 수 있었다.
val btnFinish = findViewById<ConstraintLayout>(R.id.btn_home_finish)
액션별 버튼 xml파일 만들기
조건 : 레이아웃은 각 모서리를 둥글게 합니다. (shape 이용)
drawable에 새 파일을 생성하여 default, press 버튼 이미지 2개를 아래와 같은 형식으로 제작했다.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">/*모양 : 직사각형*/
<corners android:radius="30dp"/>/*모서리 : 30dp만큼 둥글게*/
<stroke/*테두리 : 두께 2dp, 색깔 지정*/
android:color="#97000000"
android:width="2dp"/>
<solid android:color="#40FFFFFF"/>/*배경 색깔 지정*/
</shape>
selector로 버튼 옵션 지정하기
조건 : selector 를 이용해 state_pressed 버튼과 텍스트에 효과를 추가합니다.
drawable에 새 파일을 생성하여 selector를 제작했다. state_pressed가 false면 기본상태로 default 버튼 이미지를 연결했고 true면 클릭된 상태로 press 버튼 이미지를 연결했다.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_finish_btn_default" android:state_pressed="false"/>
<item android:drawable="@drawable/bg_finish_btn_press" android:state_pressed="true"/>
</selector>
글씨색과 아이콘도 바뀌도록 각각 selector를 생성했다.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="#97000000" />
<item android:color="#000000"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/img_finish1" android:state_pressed="false"/>
<item android:drawable="@drawable/img_finish2" android:state_pressed="true"/>
</selector>
2. 클래스 전달
data class 만들기
조건 : UserClass를 만들어요. (dataclass로)
data 키워드를 붙이고 파라미터에 var/val 키워드를 작성해줘야 한다는 점이 다르다. 키워드 붙이니까 초기화까지 해줘야될 것만 같은데 객체 생성될 때 초기화 돼서 굳이 초기값은 설정해주지 않아도 된다.
data class UserClass(
var id:String,
var pwd:String,
var name:String,
var gender:String,
var age:String
)
Parcelable로 전달하기
조건 :
회원 가입이 완료되면 UserClass의 객체를 생성합니다.
intent에 생성된 userclass담에 LoginActivity로 전달합니다.
객체 전달에는 **Parcelable** 을 이용합니다.
로그인 버튼을 누르면 UserClass의 객체를 LoginActivity에서 HomeActivity로 전달합니다.
HomeActivity에서 UserClass 내용을 화면에 표시합니다.
Parcelable
Parcel은 꾸러미, 소포, 택배라는 뜻. 안드로이드 SDK에 포함된 인터페이스. 택배를 포장해서 보내듯 데이터를 직렬화하여 저장하고 택배를 받고 언박싱하듯 역직렬화하여 복원할 수 있다. 빠른 속도가 장점이지만 보일러 플레이트 코드(준비해야되는 코드)가 길다는 단점이 있다. 이를 보완하기 위해 Pacelize가 마련됐다.
실제로 Parcelize가 있는걸 몰라서 Parcelable을 직접 작성해봤는데 차이가 많이 난다.
Parcelize 덕분에 직렬화는 어렵지 않았다.
1. plug in : gradle(Module:app) - plugins - {} 안에 작성 - sinc now
2. import : data class 위에 @Parcelize를 적으면 import되며 타입 지정
3. SingUpActivity에 객체를 생성한다.
//1. gradle(Module:app) plugins{안에 sinc해주기}
id("kotlin-parcelize")
//2.
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserClass(
var id:String,
var pwd:String,
var name:String,
var gender:String,
var age:String
) : Parcelable
//3.
UserClass(
idData.toString(),
pwdData.toString(),
nameData.toString(),
genderData,
ageData.toString())
)
그냥 intent해서 역직렬화는 되는데 이걸 어떻게 런처를 통해 받을까?
userData는 UserClass의 인스턴스인데 여기서 타입 미스매치가 나와 택배를 풀어 담을 수 없었다. userData에 : UserClass를 적어보고 getParcelableExtra에 제네릭으로 <UserClass>를 적어도 안돼서 튜터님께 찾아갔더니 userData는 지금 받는 데이터가 UserClass타입인지 몰라서 뒤에 무언가를 넣어줘야한다는 힌트를 주셨다.
getResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
userData = result.data?.getParcelableExtra("userData")/*문제의 라인*/
etId.setText(userData.id)
etPwd.setText(userData.pwd)
}
}
힌트를 받고 고민 끝에 해결한 방법은 UserClass타입이 맞다고 !!단언 연산자로 우겼더니 됐다.
userData = result.data?.getParcelableExtra("userData")!!
인스턴스화 덕분에 SignIn에서 Home으로 넘기는 건 간단했다.
전체코드는 Github
https://github.com/hyehyunj/HH_SignUp
+2024. 07. 03 피드백
1. userData를 받을 때 강제 언래핑(!!)을 사용하지 않고, 안전하게 널 체크를 하는 것이 좋습니다.
//Nullable로 NPE를 방지한다.
private var userData: UserClass? = UserClass("", "", "", "", "")
//safe call로 바꿔줬다.
userData = result.data?.getParcelableExtra("userData")
etId.setText(userData?.id)
etPwd.setText(userData?.pwd)
2. 데이터 클래스: UserClass의 필드들을 val로 선언하여 불변성을 유지하는 것이 좋습니다. 데이터 클래스를 사용할 때는 객체의 상태가 변하지 않도록 하는 것이 바람직합니다.
@Parcelize
data class UserClass(
var id:String,//id는 로그인페이지에서 값이 바뀔 수 있으므로 var로 하고 나머지는 val로 바꿨다.
val pwd:String,
val name:String,
val gender:String,
val age:String
) : Parcelable
완벽합니다!!
필수,선택, 도전과제까지 모든 기능이 잘 구현되어 있습니다. 전체적인 흐름이 깔끔하고, 필요한 데이터 전송도 잘 처리되었습니다.
코드의 가독성면에서도 각 뷰를 변수에 연결하고, 적절한 이름을 사용하여 코드를 이해하기 쉽게 작성한 점이 좋습니다.
또한 빈 필드 체크와 같은 기본적인 예외 처리가 잘 되어 있어, 앱이 비정상적으로 종료되는 것을 방지했습니다.
생각했던것보다 너무 잘만들어서 깜짝 놀랬네요.ㅎㅎ 고생많았어요. 앞으로의 발전이 기대됩니다!!
회고
registerForActivityResult() 구현에서 1차, Parcelize된 데이터 클래스 객체 역직렬화에서 2차로 고비였다. 하고 싶은게 많았는데 여전히 여유롭진 못해 아쉬웠지만 만들면서 문제가 생기면 어느 동작에서 문제가 생기는지 확인하고 해당 코드로 가서 찾아내는 요령이 생긴 점, 최대한 스스로의 힘으로 해볼 수 있는 데까지 해보다가 도저히 안되는 부분에서만 힌트를 받아 해결했다는게 뿌듯한 프로젝트였다.
'안드로이드와 앱 > 프로젝트' 카테고리의 다른 글
팀프로젝트 [산타의 리스트] (0) | 2024.07.22 |
---|---|
개인 프로젝트 [에코마켓 앱] (0) | 2024.07.11 |
팀프로젝트 [MenuJo] (0) | 2024.07.02 |
개인 프로젝트 [콘솔형 키오스크] (0) | 2024.06.11 |
개인 프로젝트 [콘솔형 계산기] (0) | 2024.06.05 |