1차 세미나 과제
-
GIF
-
SignInActivity
-
로그인 버튼을 눌렀을 때, ID, PW가 모두 입력되어있을 시 HomeActivity로 이동하고 그렇지 않으면 Toast 출력
private fun clickLogin() { if(!binding.etSigninId.text.isNullOrBlank() && !binding.etSigninPw.text.isNullOrBlank()) { Toast.makeText(this, "안녕하세요 ${binding.etSigninId.text}!", Toast.LENGTH_SHORT).show() val intent = Intent(this, HomeActivity::class.java) startActivity(intent) } else { Toast.makeText(this, "ID/PW를 확인해주세요!", Toast.LENGTH_SHORT).show() } }
- ID, PW 입력 여부는 isNullOrBlank() 메서드를 활용하여 체크하였음
- 각 조건문 분기마다 Toast 출력하였음
-
비밀번호 EditText는 입력 내용이 가려져 있어야하고, 모든 EditText는 미리보기가 있어야 함
<EditText android:id="@+id/et_signin_pw" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="@string/signin_hint_pw" android:inputType="textPassword" android:maxLines="1" android:ellipsize="end" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_signin_pw" />
- 모든 EditText마다 hint 속성을 활용하여 미리보기를 추가하였고, 비밀번호 EditText의 경우 inputType을 textPassword로 설정하여 입력 내용을 가렸음
-
회원가입 버튼을 누를 시 SignUpActivity로 이동
private fun clickSignUp() { val intent = Intent(this, SignUpActivity::class.java) startActivity(intent) }
-
-
SignUpActivity
-
회원가입 버튼을 눌렀을 때, 이름, ID, PW가 모두 입력되어있을 시 SignInActivity로 다시 돌아가고 그렇지 않으면 Toast 출력
private fun clickSignUp() { if(!binding.etSignupName.text.isNullOrBlank() && !binding.etSignupId.text.isNullOrBlank() && !binding.etSignupPw.text.isNullOrBlank()) { Toast.makeText(this, "회원가입이 완료되었습니다.", Toast.LENGTH_SHORT).show() finish() } else { Toast.makeText(this, "이름/ID/PW를 확인해주세요.", Toast.LENGTH_SHORT).show() } }
- 이름, ID, PW 입력 여부는 isNullOrBlank() 메서드를 활용하여 체크하였음
- 각 조건문 분기마다 Toast 출력하였음
- finish() 메서드를 활용하여 이전 스택의 Activity로 복귀하였음
-
비밀번호 EditText는 입력 내용이 가려져 있어야하고, 모든 EditText는 미리보기가 있어야 함
<EditText android:id="@+id/et_signup_pw" android:layout_width="0dp" android:layout_height="wrap_content" android:ellipsize="end" android:hint="@string/signin_hint_pw" android:inputType="textPassword" android:maxLines="1" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_signup_pw" />
- 모든 EditText마다 hint 속성을 활용하여 미리보기를 추가하였고, 비밀번호 EditText의 경우 inputType을 textPassword로 설정하여 입력 내용을 가렸음
-
2차 세미나 과제
-
GIF
-
자기소개 페이지를 만든 HomeActivity 하단에 FollowerRecyclerView, RepositoryRecyclerView 만들기(HomeActivity.kt)
-
각각의 RecyclerView를 담고있는 Fragment 2개 만들기
-
FollowerFragment, RepoFragment 생성
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".FollowerFragment"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rcv_follower" android:layout_width="match_parent" android:layout_height="match_parent" tools:itemCount="5" tools:listitem="@layout/item_follower" /> </FrameLayout>
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".RepoFragment"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rcv_repo" android:layout_width="match_parent" android:layout_height="match_parent" tools:itemCount="5" tools:listitem="@layout/item_repo" /> </FrameLayout>
-
-
각각의 버튼을 눌렀을 때 알맞은 RecyclerView가 있는 Fragment로 전환하기
-
initTransaction() 을 구현하여 각각의 버튼을 눌렀을 때 알맞은 RecyclerView가 있는 Fragment로 전환
-
default로 보이는 Fragment는 FollowerFragment로 설정
private fun initTransaction() { val followerFragment = FollowerFragment() val repoFragment = RepoFragment() supportFragmentManager.beginTransaction().add(R.id.frg_home_rcv, followerFragment).commit() binding.btnHomeRepo.setOnClickListener { supportFragmentManager.beginTransaction().replace(R.id.frg_home_rcv, repoFragment) .commit() } binding.btnHomeFollower.setOnClickListener { supportFragmentManager.beginTransaction().replace(R.id.frg_home_rcv, followerFragment) .commit() } }
-
-
설명이 일정 길이를 넘어가면 xml의 ellipsize 속성을 활용
<TextView android:id="@+id/tv_follower_info" android:layout_width="0dp" android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="1" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/tv_follower_name" app:layout_constraintTop_toBottomOf="@id/tv_follower_name" tools:text="info" />
<TextView android:id="@+id/tv_repo_info" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="5dp" android:ellipsize="end" android:maxLines="1" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_repo_name" tools:text="info" />
-
-
둘 중 하나의 RecyclerView는 Grid Layout으로 만들기
-
FollowerFragment의 RecyclerView를 Grid Layout으로 설정
binding.rcvFollower.layoutManager = GridLayoutManager(context, 2)
-
-
GIF
-
아이템 클릭 시 상세 설명을 보여주는 Activity로 이동하기(DetailActivity.kt)
-
이름과 사진은 화면 전환 시 넘겨줄 것
-
Adapter 생성자에 매개변수로 itemClick 선언
class FollowerAdapter(val itemClick: (FollowerData) -> Unit) : RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>()
-
Fragment에서 Adapter 객체 생성 시 itemClickListener 구현
private val adapter by lazy { FollowerAdapter() { val intent = Intent(context, DetailActivity::class.java) intent.putExtra("profile", it.image) intent.putExtra("name", it.name) intent.putExtra("detailInfo", it.detailInfo) startActivity(intent) } }
-
DetailActivity에서 getExtra 사용해 구현
val profile = intent.getIntExtra("profile", 0) val name = intent.getStringExtra("name") val detailInfo = intent.getStringExtra("detailInfo") binding.imgDetailProfile.setImageResource(profile) binding.tvDetailName.text = name binding.tvDetailInfo.text = detailInfo
-
-
-
ItemDecoration 활용해서 리스트 간 간격과 구분선 주기
-
ItemDecoration을 활용해서 구분선 넣기
-
ItemDecoration을 상속받은 MyDecoration 클래스 구현
-
onDrawOver 메서드를 오버라이드하여 구분선 넣기
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val paint = Paint() paint.color = color val left = parent.paddingStart + padding val right = parent.width - parent.paddingEnd - padding for (i in 0 until parent.childCount) { val child = parent.getChildAt(i) val params = child.layoutParams as RecyclerView.LayoutParams val top = (child.bottom + params.bottomMargin).toFloat() val bottom = top + height c.drawRect(left, top, right, bottom, paint) } }
-
getItemOffsets 메서드를 오버라이드하여 아이템 간 간격(margin) 주기
override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { super.getItemOffsets(outRect, view, parent, state) outRect.bottom += padding.toInt() outRect.top += padding.toInt() outRect.left += padding.toInt() outRect.right += padding.toInt() }
-
-
-
RecyclerView Item 이동 삭제 구현
-
ItemTouchHelper.SimpleCallback 사용
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback( ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { val fromPos = viewHolder.adapterPosition val toPos = target.adapterPosition val temp = adapter.itemList[fromPos] if(fromPos < toPos) { for(i in fromPos until toPos) { adapter.itemList[i] = adapter.itemList[i+1] } adapter.itemList[toPos] = temp } else if(fromPos > toPos) { for(i in toPos+1..fromPos) { adapter.itemList[i] = adapter.itemList[i-1] } adapter.itemList[toPos] = temp } adapter.notifyItemMoved(fromPos, toPos) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val delPos = viewHolder.adapterPosition adapter.itemList.removeAt(delPos) adapter.notifyItemRemoved(delPos) } } val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) itemTouchHelper.attachToRecyclerView(binding.rcvFollower)
- Grid Layout인 rcvFollower에서는 dragDirs를 상하좌우, swipeDirs를 좌우로 설정했고 Linear Layout인 rcvRepo에서는 dragDirs를 상하, swipeDirs를 좌우로 설정함
-
-
보일러 플레이트 코드 개선
- 보일러 플레이트 코드
- 최소한의 변경으로 여러 곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드
- BaseActivity, BaseFragment를 사용
- 보일러 플레이트 코드
-
보다 효율적으로 RecyclerView의 아이템을 갱신하기
- notifyDataSetChanged
- Adapter가 DataSet 전부를 갱신하도록 하는 메서드
- 리스트의 크기와 아이템 둘 다 변경되는 경우에 사용
- 문제점: DataSet의 크기가 작은 경우에는 별로 티나지 않지만, DataSet의 크기가 커질 경우 일부만 갱신하면 될 일을 굳이 전체를 갱신한다면? >> 비효율적
- notifyItemChanged(position: Int)
- 특정 position의 아이템만 변경된 경우 사용
- notifyItemRangeChanged(positionStart: Int, itemCount: Int)
- 특정 positionStart부터 itemCount 개수만큼 아이템이 변경된 경우 사용
- notifyItemInserted(position: Int)
- 특정 position에 아이템이 삽입된 경우 사용
- notifyItemRangeInserted(positionStart:Int, itemCount: Int)
- 특정 positionStart부터 itemCount 개수만큼 아이템이 삽입된 경우 사용
- notifyItemRemoved(position: Int)
- 특정 position에서 아이템이 삭제된 경우 사용
- ItemTouchHelper.SimpleCallback의 onSwipe에서 아이템 삭제했을 때 사용함
- notifyItemRangeRemoved(positionStart: Int, itemCount: Int)
- 특정 positionStart부터 itemCount 개수만큼 아이템이 삭제된 경우 사용
- notifyItemMoved(fromPosition: Int, toPosition: Int)
- fromPosition에 있던 아이템이 toPosition으로 이동한 경우 사용
- ItemTouchHelper.SimpleCallback의 onMove에서 아이템 이동했을 때 사용함
- notifyDataSetChanged
3차 세미나 과제
-
GIF
-
과제에 디자인 적용하기
-
로그인 화면 / 회원가입 화면
-
EditText에 selector 활용하기(focus 되었을 때, 안 되었을 때)
-
selector_textbox
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/shape_textbox_not_empty" android:state_focused="true" /> <item android:drawable="@drawable/shape_textbox_empty" android:state_focused="false" /> </selector>
-
-
간단한 도형들은 ShapeDrawable로 직접 만들기
-
shape_textbox_not_empty.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/shape_textbox_not_empty" android:state_focused="true" /> <item android:drawable="@drawable/shape_textbox_empty" android:state_focused="false" /> </selector>
-
shape_textbox_empty.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/textbox_empty_body" /> <stroke android:width="1dp" android:color="@color/textbox_empty_border" /> <padding android:bottom="13dp" android:left="16dp" android:right="16dp" android:top="13dp" /> <corners android:radius="5dp" /> </shape>
-
shape_button_sign.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/medium_pink" /> <corners android:radius="5dp" /> <padding android:bottom="12dp" android:top="12dp" /> </shape>
-
-
-
ProfileFragment
-
Button에 selector 활용하기(선택되었을 때, 안 되었을 때)
-
selector_button.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_selected="true" android:drawable="@drawable/shape_selected_button"/> <item android:state_selected="false" android:drawable="@drawable/shape_unselected_button"/> </selector>
-
shape_selected_button.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/button_selected" /> <corners android:radius="5dp" /> </shape>
-
shape_unselected_button.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/textbox_empty_body" /> <corners android:radius="5dp" /> </shape>
-
-
이미지의 경우 Glide의 CircleCrop 기능 활용해서 넣어주기
-
ProfileFragment.kr
private fun initProfilePicture() { Glide.with(requireContext()) .load("https://avatars.githubusercontent.com/u/81508084?v=4") .circleCrop() .into(binding.imgProfilePicture) }
-
-
하단에 BottomNavigation 넣어주기
-
MainActivity.kt
private fun initViewPagerAdapter() { val fragmentList = listOf(profileFragment, homeFragment, cameraFragment) viewPagerAdapter = MainViewPagerAdapter(this) viewPagerAdapter.fragments.addAll(fragmentList) binding.vpMain.adapter = viewPagerAdapter } private fun initBottomNavigation() { binding.vpMain.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { binding.bnvMain.menu.getItem(position).isChecked = true } }) binding.bnvMain.setOnItemSelectedListener { binding.vpMain.currentItem = when(it.itemId) { R.id.menu_profile -> PROFILE_FRAGMENT R.id.menu_home -> HOME_FRAGMENT else -> CAMERA_FRAGMENT } return@setOnItemSelectedListener true } } private companion object { const val PROFILE_FRAGMENT = 0 const val HOME_FRAGMENT = 1 const val CAMERA_FRAGMENT = 2 }
-
-
-
HomeFragment
-
3차 세미나에서 배웠던 TabLayout + ViewPager2 넣어주기
-
HomeFragment.kt
private fun initViewPager() { val fragmentList = listOf(homeFollowingFragment, homeFollowerFragment) viewPagerAdapter = HomeViewPagerAdapter(this) viewPagerAdapter.fragments.addAll(fragmentList) binding.vpHome.adapter = viewPagerAdapter } private fun initTabLayout() { val tabLabel = listOf("팔로잉", "팔로워") TabLayoutMediator(binding.tabHome, binding.vpHome) { tab, position -> tab.text = tabLabel[position] }.attach() }
-
-
-
-
ViewPager2 중첩 스크롤 문제 해결하기
-
NestedScrollableHost로 내부 ViewPager2를 wrapping하여 처리함
-
NestedScrollableHost.kt
class NestedScrollableHost : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) private var touchSlop = 0 private var initialX = 0f private var initialY = 0f private val parentViewPager: ViewPager2? get() { var v: View? = parent as? View while (v != null && v !is ViewPager2) { v = v.parent as? View } return v as? ViewPager2 } private val child: View? get() = if (childCount > 0) getChildAt(0) else null init { touchSlop = ViewConfiguration.get(context).scaledTouchSlop } private fun canChildScroll(orientation: Int, delta: Float): Boolean { val direction = -delta.sign.toInt() return when (orientation) { 0 -> child?.canScrollHorizontally(direction) ?: false 1 -> child?.canScrollVertically(direction) ?: false else -> throw IllegalArgumentException() } } override fun onInterceptTouchEvent(e: MotionEvent): Boolean { handleInterceptTouchEvent(e) return super.onInterceptTouchEvent(e) } private fun handleInterceptTouchEvent(e: MotionEvent) { val orientation = parentViewPager?.orientation ?: return // Early return if child can't scroll in same direction as parent if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { return } if (e.action == MotionEvent.ACTION_DOWN) { initialX = e.x initialY = e.y parent.requestDisallowInterceptTouchEvent(true) } else if (e.action == MotionEvent.ACTION_MOVE) { val dx = e.x - initialX val dy = e.y - initialY val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL // assuming ViewPager2 touch-slop is 2x touch-slop of child val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f if (scaledDx > touchSlop || scaledDy > touchSlop) { if (isVpHorizontal == (scaledDy > scaledDx)) { // Gesture is perpendicular, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } else { // Gesture is parallel, query child if movement in that direction is possible if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { // Child can scroll, disallow all parents to intercept parent.requestDisallowInterceptTouchEvent(true) } else { // Child cannot scroll, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } } } } } }
-
fragment_home.xml
<co.kr.soptandroidseminar.home.NestedScrollableHost android:id="@+id/nsh_home" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tab_home"> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/vp_home" android:layout_width="match_parent" android:layout_height="match_parent" /> </co.kr.soptandroidseminar.home.NestedScrollableHost>
-
-
-
리스트에 각기 다른 이미지 넣기
-
RecyclerView의 data class에 url을 저장할 변수 추가
-
FollowerData.kt
data class FollowerData( val image: String, val name: String, val info: String, val detailInfo: String, )
-
FollowerAdapter.kt
fun onBind(data: FollowerData) { Glide.with(binding.imgFollowerProfile.context) .load(data.image) .circleCrop() .into(binding.imgFollowerProfile) binding.tvFollowerName.text = data.name binding.tvFollowerInfo.text = data.info binding.root.setOnClickListener { itemClick(data) } }
-
-
-
갤러리에서 받아온 이미지(uri)를 Glide로 화면에 띄워보기
-
인텐트를 이용해 갤러리에 접근
private fun openGallery() { val intent = Intent(Intent.ACTION_PICK) intent.type = MediaStore.Images.Media.CONTENT_TYPE activityLauncher.launch(intent) }
-
사진데이터를 uri 형식으로 받아온 이후 Glide로 이미지뷰에 띄우기
private val activityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if(it.resultCode == RESULT_OK && it.data != null) { val imageUri = it.data?.data runCatching { Glide.with(this) .load(imageUri) .into(binding.imgCamera) }.onFailure { makeToast("사진 첨부 실패") } } else if(it.resultCode == RESULT_CANCELED) { makeToast("사진 선택 취소") } }
-
4차 세미나 과제
-
GIF
-
로그인/회원가입 API 연동
-
SeminarService.kt
interface SeminarService { @POST("user/signup") fun postSignUp( @Body body: RequestSignUpData ) : Call<ResponseSignUpData> @POST("user/login") fun postSingIn( @Body body: RequestSignInData ) : Call<ResponseSignInData> }
-
RequestSignInData.kt
data class RequestSignInData( val email: String, val password: String, )
-
ResponseSignInData.kt
data class ResponseSignInData( val status: Int, val success: Boolean, val message: String, val data: Data ) { data class Data( val id: Int, val name: String, val email: String, ) }
-
RequestSignUpData.kt
data class RequestSignUpData( val email: String, val name: String, val password: String, )
-
ResponseSignUpData.kt
data class ResponseSignUpData( val status: Int, val success: Boolean, val message: String, val data: Data ) { data class Data( val id: Int, val name: String, val password: String, val email: String, ) }
-
SignUpActivity.kt
private fun clickSignUp() { if(!binding.etSignupName.text.isNullOrBlank() && !binding.etSignupId.text.isNullOrBlank() && !binding.etSignupPw.text.isNullOrBlank()) { val requestSignUpData = RequestSignUpData( binding.etSignupId.text.toString(), binding.etSignupName.text.toString(), binding.etSignupPw.text.toString() ) val call: Call<ResponseSignUpData> = ApiService.seminarService.postSignUp(requestSignUpData) call.enqueue(object: Callback<ResponseSignUpData> { override fun onResponse( call: Call<ResponseSignUpData>, response: Response<ResponseSignUpData> ) { if(response.isSuccessful) { val data = response.body() Toast.makeText(this@SignUpActivity, data?.message, Toast.LENGTH_SHORT).show() } else { Log.d("server connect : SignUp", "error") Log.d("server connect : SignUp", "$response.errorBody()") Log.d("server connect : SignUp", response.message()) Log.d("server connect : SignUp", "${response.code()}") Toast.makeText(this@SignUpActivity, "회원가입 실패", Toast.LENGTH_SHORT).show() } } override fun onFailure(call: Call<ResponseSignUpData>, t: Throwable) { Toast.makeText(this@SignUpActivity, "회원가입 실패", Toast.LENGTH_SHORT).show() } }) finish() } else { Toast.makeText(this, "이름/ID/PW를 확인해주세요.", Toast.LENGTH_SHORT).show() } }
-
SignInActivity.kt
private fun clickLogin() { if(!binding.etSigninId.text.isNullOrBlank() && !binding.etSigninPw.text.isNullOrBlank()) { val requestSignInData = RequestSignInData( binding.etSigninId.text.toString(), binding.etSigninPw.text.toString() ) val call: Call<ResponseSignInData> = ApiService.seminarService.postSingIn(requestSignInData) call.enqueue(object: Callback<ResponseSignInData> { override fun onResponse( call: Call<ResponseSignInData>, response: Response<ResponseSignInData> ) { if(response.isSuccessful) { val data = response.body()?.data Toast.makeText(this@SignInActivity, "안녕하세요 ${data?.name}!", Toast.LENGTH_SHORT).show() val intent = Intent(this@SignInActivity, MainActivity::class.java) intent.putExtra("name", data?.name) intent.putExtra("email", data?.email) startActivity(intent) } else { Log.d("server connect : SignIn", "error") Log.d("server connect : SignIn", "$response.errorBody()") Log.d("server connect : SignIn", response.message()) Log.d("server connect : SignIn", "${response.code()}") Toast.makeText(this@SignInActivity, "로그인 실패", Toast.LENGTH_SHORT).show() val intent = Intent(this@SignInActivity, MainActivity::class.java) intent.putExtra("name", "hansh0101") intent.putExtra("email", binding.etSigninId.text.toString()) startActivity(intent) } } override fun onFailure(call: Call<ResponseSignInData>, t: Throwable) { Toast.makeText(this@SignInActivity, "로그인 실패", Toast.LENGTH_SHORT).show() } }) } else { Toast.makeText(this, "ID/PW를 확인해주세요!", Toast.LENGTH_SHORT).show() } }
-
-
Github API 연동해서 리스트로 띄우기
-
유저 프로필, 팔로워 리스트, 레포지토리 리스트 불러오기
-
GithubService.kt
interface GithubService { @GET("/users/{username}") fun getUserInfo( @Path("username") username: String ): Call<ResponseUserInfoData> @GET("/users/{username}/followers") fun getFollowerList( @Path("username") username: String ): Call<List<ResponseFollowerData>> @GET("/users/{username}/repos") fun getRepoList( @Path("username") username: String ): Call<List<ResponseRepoData>> }
-
ResponseUserInfoData.kt
data class ResponseUserInfoData( val avatar_url: String, val bio: String?, val login: String, val name: String, )
-
ResponseFollowerData.kt
data class ResponseFollowerData( val login: String, )
-
ResponseRepoData.kt
data class ResponseRepoData( val name: String, val description: String, )
-
ProfileFragment.kt
private fun getServerData() { val call: Call<ResponseUserInfoData> = ApiService.githubService.getUserInfo(username) call.enqueue(object : Callback<ResponseUserInfoData> { override fun onResponse( call: Call<ResponseUserInfoData>, response: Response<ResponseUserInfoData> ) { if (response.isSuccessful) { val data = response.body() data?.avatar_url?.let { initProfilePicture(it) } binding.tvProfileName.text = data?.name binding.tvProfileId.text = data?.login data?.bio?.let { binding.tvProfileIntro.text = it } initTransaction() } else { Log.d("server connect : Profile Fragment", "error") Log.d("server connect : Profile Fragment", "$response.errorBody()") Log.d("server connect : Profile Fragment", response.message()) Log.d("server connect : Profile Fragment", "${response.code()}") } } override fun onFailure(call: Call<ResponseUserInfoData>, t: Throwable) { Log.d("server connect : Profile Fragment", "error: ${t.message}") } }) }
-
FollowerFragment.kt
private fun getFollowerList() { val call: Call<List<ResponseFollowerData>> = ApiService.githubService.getFollowerList(username) call.enqueue(object : Callback<List<ResponseFollowerData>> { override fun onResponse( call: Call<List<ResponseFollowerData>>, response: Response<List<ResponseFollowerData>> ) { if (response.isSuccessful) { val data = response.body() if (data != null) { getFollowerInfo(data) } } else { Log.d("server connect : Follower Fragment", "error") Log.d("server connect : Follower Fragment", "$response.errorBody()") Log.d("server connect : Follower Fragment", response.message()) Log.d("server connect : Follower Fragment", "${response.code()}") } } override fun onFailure(call: Call<List<ResponseFollowerData>>, t: Throwable) { Log.d("server connect : Follower Fragment", "error: ${t.message}") } }) }
private fun getFollowerInfo(list: List<ResponseFollowerData>) { list.forEach { val call: Call<ResponseUserInfoData> = ApiService.githubService.getUserInfo(it.login) call.enqueue(object : Callback<ResponseUserInfoData> { override fun onResponse( call: Call<ResponseUserInfoData>, response: Response<ResponseUserInfoData> ) { if (response.isSuccessful) { val data = response.body() adapter.itemList.add( FollowerData( data?.avatar_url, data?.login, data?.name, data?.bio ) ) adapter.notifyItemInserted(adapter.itemList.size - 1) Log.d("server connect : Follower Fragment", "success") Log.d("server connect : Follower Fragment", it.login) } else { Log.d("server connect : Follower Fragment", "error") Log.d("server connect : Follower Fragment", "$response.errorBody()") Log.d("server connect : Follower Fragment", response.message()) Log.d("server connect : Follower Fragment", "${response.code()}") } } override fun onFailure(call: Call<ResponseUserInfoData>, t: Throwable) { Log.d("server connect: FollowerFragment", "error: ${t.message}") } }) } initRecyclerView() }
-
RepoFragment.kt
private fun getRepoList() { val call: Call<List<ResponseRepoData>> = ApiService.githubService.getRepoList(username) call.enqueue(object : Callback<List<ResponseRepoData>> { override fun onResponse( call: Call<List<ResponseRepoData>>, response: Response<List<ResponseRepoData>> ) { if (response.isSuccessful) { val data = response.body() data?.forEach { adapter.itemList.add( RepoData( it.name, it.description ) ) adapter.notifyItemInserted(adapter.itemList.size - 1) } } else { Log.d("server connect : Repo Fragment", "error") Log.d("server connect : Repo Fragment", "$response.errorBody()") Log.d("server connect : Repo Fragment", response.message()) Log.d("server connect : Repo Fragment", "${response.code()}") } } override fun onFailure(call: Call<List<ResponseRepoData>>, t: Throwable) { Log.d("server connect : Repo Fragment", "error: ${t.message}") } }) }
-
-
-
OkHttp 활용해보기
-
ApiService.kt
private val soptRetrofit: Retrofit = Retrofit.Builder() .baseUrl(BASE_URL_SOPT) .client(provideSoptOkHttpClient(SoptInterceptor())) .addConverterFactory(GsonConverterFactory.create()) .build() private fun provideSoptOkHttpClient( interceptor: SoptInterceptor ): OkHttpClient = OkHttpClient.Builder() .run { addInterceptor(interceptor) build() } class SoptInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response = with(chain) { val newRequest = request().newBuilder() .addHeader("Content-Type", "application/json") .build() proceed(newRequest) } }
-
7차 세미나 과제
-
GIF
-
온보딩 화면 만들기
-
OnBoardingActivity 위에 OnBoardingOneFragment, OnBoardingTwoFragment, OnBoardingThreeFragment 띄움
-
OnBoardingOneFragment.kt
private fun skipOnBoarding() { binding.btnOnboardingOne.setOnClickListener { findNavController().navigate(R.id.action_frg_onboarding_one_to_frg_onboarding_two) } }
-
OnBoardingTwoFragment.kt
private fun skipOnBoarding() { binding.btnOnboardingTwo.setOnClickListener { findNavController().navigate(R.id.action_frg_onboarding_two_to_frg_onboarding_three) } }
-
OnBoardingThreeFragment.kt
private fun skipOnBoarding() { binding.btnOnboardingThree.setOnClickListener { val intent = Intent(requireContext(), SignInActivity::class.java) startActivity(intent) (activity as OnBoardingActivity).finish() } }
-
-
SharedPreferences 활용해서 자동로그인 / 자동로그인 해제 구현하기
-
SharedPreference.kt
object SharedPreference { private const val STORAGE_KEY = "USER_AUTH" private const val AUTO_LOGIN = "AUTO_LOGIN" private const val USER_ID = "USER_ID" private const val USER_EMAIL = "USER_EMAIL" fun getAutoLogin(context: Context): Boolean { return getSharedPreference(context).getBoolean(AUTO_LOGIN, false) } fun getUserId(context: Context): String? { return getSharedPreference(context).getString(USER_ID, "") } fun getUserEmail(context: Context): String? { return getSharedPreference(context).getString(USER_EMAIL, "") } fun setAutoLogin(context: Context, value: Boolean, userId: String, userEmail: String) { getSharedPreference(context).edit() .putBoolean(AUTO_LOGIN, value) .putString(USER_ID, userId) .putString(USER_EMAIL, userEmail) .apply() } fun removeAutoLogin(context: Context) { getSharedPreference(context).edit() .remove(AUTO_LOGIN) .apply() } fun clearAutoLogin(context: Context) { getSharedPreference(context).edit() .clear() .apply() } fun getSharedPreference(context: Context): SharedPreferences { return context.getSharedPreferences(STORAGE_KEY, Context.MODE_PRIVATE) } }
-
SignInActivity.kt
private fun clickLogin() { if(!binding.etSigninId.text.isNullOrBlank() && !binding.etSigninPw.text.isNullOrBlank()) { val requestSignInData = RequestSignInData( binding.etSigninId.text.toString(), binding.etSigninPw.text.toString() ) val call = ApiService.seminarService.postSingIn(requestSignInData) call.enqueueUtil( onSuccess = { simpleToast("안녕하세요 ${it.data.name}") val intent = Intent(this@SignInActivity, MainActivity::class.java) SharedPreference.setAutoLogin(this@SignInActivity, true, "hansh0101", it.data.email) startActivity(intent) }, onError = { simpleToast("로그인 실패") SharedPreference.setAutoLogin(this@SignInActivity, true, "hansh0101", "[email protected]") val intent = Intent(this@SignInActivity, MainActivity::class.java) startActivity(intent) } ) } else { simpleToast("ID/PW를 확인해주세요!") } } private fun isAutoLogin() { if(SharedPreference.getAutoLogin(this)) { simpleToast("자동 로그인") val intent = Intent(this, MainActivity::class.java) startActivity(intent) } }
-
SettingActivity.kt
private fun noAutoLogin() { binding.tvSettingAutoLogin.setOnClickListener { SharedPreference.removeAutoLogin(this) simpleToast("자동로그인 해제") } } private fun deleteLoginCache() { binding.tvSettingDeleteCache.setOnClickListener { SharedPreference.clearAutoLogin(this) simpleToast("로그인 캐시 삭제") } }
-
-
본인이 사용하는 Util 클래스 코드 및 패키징 방식 리드미에 정리하기
-
MyUtil.kt
package co.kr.soptandroidseminar.util import android.content.Context import android.util.Log import android.widget.Toast import retrofit2.Call import retrofit2.Callback import retrofit2.Response fun Context.simpleToast(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } fun <ResponseType> Call<ResponseType>.enqueueUtil( onSuccess: (ResponseType) -> Unit, onError: ((stateCode: Int) -> Unit)? = null ) { this.enqueue(object : Callback<ResponseType> { override fun onResponse(call: Call<ResponseType>, response: Response<ResponseType>) { if (response.isSuccessful) { onSuccess.invoke(response.body() ?: return) } else { onError?.invoke(response.code()) Log.d("server connect", "error") Log.d("server connect", "$response.errorBody()") Log.d("server connect", response.message()) Log.d("server connect", "${response.code()}") } } override fun onFailure(call: Call<ResponseType>, t: Throwable) { Log.d("Network", "error:$t") } }) }
-
패키징
📂SoptAndroidSeminar ┣ 📂 api ┣ 📂 data ┃ ┣ 📂 local ┃ ┣ 📂 main ┃ ┃ ┗ 📂profile ┃ ┣ 📂 signin ┃ ┗ 📂 signup ┣ 📂 util ┗ 📂 view ┣ 📂 adapter ┣ 📂 detail ┣ 📂 main ┃ ┣ 📂 camera ┃ ┣ 📂 home ┃ ┗ 📂 profile ┣ 📂 onboarding ┣ 📂 signin ┗ 📂 signup
-
-
GIF
-
NavigationComponent에서 BackStack 관리
-
nav_onboarding.xml
<?xml version="1.0" encoding="utf-8"?> <navigation 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:id="@+id/nav_onboarding" app:startDestination="@id/frg_onboarding_one"> <fragment android:id="@+id/frg_onboarding_one" android:name="co.kr.soptandroidseminar.view.onboarding.OnBoardingOneFragment" android:label="fragment_onboarding_one" tools:layout="@layout/fragment_on_boarding_one"> <action android:id="@+id/action_frg_onboarding_one_to_frg_onboarding_two" app:popUpTo="@id/frg_onboarding_one" app:destination="@id/frg_onboarding_two" /> </fragment> <fragment android:id="@+id/frg_onboarding_two" android:name="co.kr.soptandroidseminar.view.onboarding.OnBoardingTwoFragment" android:label="fragment_onboarding_two" tools:layout="@layout/fragment_on_boarding_two"> <action android:id="@+id/action_frg_onboarding_two_to_frg_onboarding_three" app:popUpTo="@id/frg_onboarding_two" app:popUpToInclusive="true" app:destination="@id/frg_onboarding_three" /> </fragment> <fragment android:id="@+id/frg_onboarding_three" android:name="co.kr.soptandroidseminar.view.onboarding.OnBoardingThreeFragment" android:label="fragment_onboarding_three" tools:layout="@layout/fragment_on_boarding_three"> </fragment> </navigation>
-