Skip to content

29th-WE-SOPT-Android-Part/Android-Changhwan

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

49 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Android-Changhwan

github_์ด์ฐฝํ™˜_ver1-24

1์ฃผ์ฐจ

-์‹คํ–‰ํ™”๋ฉด

bandicam.2021-10-10.11-58-38-060.mp4

-์ฝ”๋“œ์„ค๋ช…

signin

binding.loginButton.setOnClickListener {
            if (binding.inEditId.text.toString() != "" && binding.inEditPw.text.toString() != "") {
                val intent = Intent(this,HomeActivity::class.java)
                startActivity(intent)
                Toast.makeText(this, "์ด์ฐฝํ™˜๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค", Toast.LENGTH_SHORT).show()
            } else {
                binding.inEditId.text.clear()
                binding.inEditPw.text.clear()
                Toast.makeText(this, "๋กœ๊ทธ์ธ์‹คํŒจ", Toast.LENGTH_SHORT).show()
            }
        }

edit text์— ๋‚ด์šฉ์ด ์žˆ๋‚˜ ํ™•์ธํ›„ ์žˆ๋‹ค๋ฉด ํ† ์ŠคํŠธ๋ฉ”์„ธ์ง€๋ฅผ ๋„์šฐ๋ฉฐ ์ธํ…ํŠธ ํ•˜๋Š”๋ถ€๋ถ„

 getResult = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()){
            if(it.resultCode == RESULT_OK) {
                binding.inEditId.text.clear()
                binding.inEditId.text.append(it.data?.getStringExtra("Id"))
                binding.inEditPw.text.clear()
                binding.inEditPw.text.append(it.data?.getStringExtra("Pw"))
            }
        }

signup์œผ๋กœ ๋„˜์–ด๊ฐ”๋‹ค ๋Œ์•„์™”์„๋•Œ ๋ฐ์ดํ„ฐ ๋ฐ›์•„์™€์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ํŒŒํŠธ

signup

binding.signUpDone.setOnClickListener{
            if(binding.upEditId.text.toString() != "" && binding.upEditPw.text.toString() != "" && binding.upEditName.text.toString() != ""){
                val intent = Intent(this,SignInActivity::class.java).apply {
                    this.putExtra("Id",binding.upEditId.text.toString())
                    this.putExtra("Pw",binding.upEditPw.text.toString())
                }
                setResult(RESULT_OK,intent)
                finish()
            }else{
                Toast.makeText(this,"์ž…๋ ฅ๋˜์ง€์•Š์€ ์ •๋ณด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค",Toast.LENGTH_SHORT).show()
            }
        }

finish๋กœ ๋Œ์•„๊ฐ€๋ฉด์„œ ๊ฐ€์ ธ๊ฐ€์•ผํ•˜๋Š” ๋ฐ์ดํ„ฐ๋“ค putExtra๋กœ ๊ฐ€์ ธ๊ฐ€๋Š”๋ถ€๋ถ„

home

binding.homeToGit.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/2chang5"))
            startActivity(intent)
        }

์•”์‹œ์  ์ธํ…ํŠธ๋กœ ์›น์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š”๋ถ€๋ถ„

-์ด๋ฒˆ ๊ณผ์ œ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šด๋‚ด์šฉ

level1

1. Editable

edittext ๋ฅผ ์ฝ”๋“œ๋‹จ์—์„œ ํ…์ŠคํŠธ๋ฅผ ๋„ฃ์–ด์ฃผ๊ธฐ์œ„ํ•ด

image

์ด๋Ÿฐ์‹์œผ๋กœ ํ…์ŠคํŠธ์— ์ง์ ‘ ๋ฌธ์ž์—ด์„ ๋„ฃ์–ด์คฌ๋Š”๋ฐ ์ž๋ฃŒํ˜•์ด ์•ˆ๋งž์•„์„œ ์ ์šฉํ• ์ˆ˜๊ฐ€ ์—†์—ˆ๋‹ค. edittext์˜ ํ…์ŠคํŠธ๋Š” Editable TYPE์ด์˜€๋Š”๋ฐ Editable๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด์ด๋ฏ€๋กœ Editable ์•ˆ์— ์ •์˜๋œ clear() append()๊ฐ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์กฐ์ž‘ํ• ์ˆ˜์žˆ์—ˆ๋‹ค.

image

2.finish

๊ธฐ์กด์— ๋งจ๋‚  ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ• ๋•Œ startActivity๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์›€์ง์˜€๊ณ  ๋ญ”๊ฐ€ ์ด์ƒํ–ˆ๋‹ค. ์Šคํƒ์— ์Œ“์—ฌ์žˆ๋Š” ๊ฑฐ์ณ์™”๋˜ ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐˆ๋•Œ๋Š” back ๋ฒ„ํŠผ์„ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋ˆŒ๋Ÿฌ์„œ ๋Œ์•„๊ฐ”๋Š”๋ฐ ์ด๋ ‡๊ฒŒ ๊ธฐ์กด ์Šคํƒ์— ์ตœ์ƒ๋‹จ์—์žˆ๋Š” ํ™”๋ฉด์—์„œ ์Šคํƒ์— ์Œ“์—ฌ์žˆ๋Š” ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ ์œ„ํ•ด ๋ฐฑ๋ฒ„ํŠผ๊ณผ ๊ฐ™์€ ํšจ๊ณผ๋ฅผ ๊ฐ€์ง„ finish() ๋ฅผ ํ†ตํ•ด์„œ ํ™”๋ฉด์„ ์ข…๋ฃŒํ•˜๊ณ  ๊ธฐ์กด์— ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐˆ์ˆ˜์žˆ๋‹ค.

image

์‚ฌ์šฉ์˜ˆ์‹œ

3.imageView

image view๋ฅผ ์‚ฌ์šฉํ• ๋•Œ ๊ธฐ์กด์—๋Š” srcCompat ์†์„ฑ์— ์†Œ์Šค๋ฅผ ์„ค์ •ํ–ˆ๋Š”๋ฐ ์ด๋ฒˆ์— ์ด์ƒํ•˜๊ฒŒ ์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋˜์ง€ ์•Š์•˜๋‹ค ๊ตฌ๊ธ€๋งํ•ด๋ณธ๊ฒฐ๊ณผ

srcCompat์€ Android Support Library์— ํฌํ•จ๋œ ๋ฐฉ์‹(method of work)์ด๋‹ค. (AppCompat์— ์žˆ์Œ) ์•ˆ๋“œ๋กœ์ด๋“œ ์„œํฌํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์˜๋ฏธํ•˜๋ฉด '์–ด๋А ๋ฒ„์ „์—์„œ๋‚˜ ๋˜‘๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”'์„ ํ–‰ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ๊ณ  ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. srcCompat์€ vector Drawables(์ฆ‰, ๊ทธ๋ฆผ)๋ฅผ ๋ชจ๋“  ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ํ‘œํ˜„ํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ์•ˆ๋“œ ์„œํฌํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์•ˆ์— ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ์ด๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋‚ด๊ฐ€ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ๋‹น์—ฐํžˆ srcCompat์œผ๋กœ ๋ฆฌ์†Œ์Šค๋ฅผ ์ง€์ •ํ•ด๋ดค์ž ์—๋ฎฌ๋ ˆ์ดํ„ฐ๊ฐ€ ์ œ๋Œ€๋กœ ๊ทธ๋ ค์ค„ ๋ฆฌ ์—†๋‹ค. ๊ทธ๋Ÿฌ๋ฏ€๋กœ srcCompat์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๊ณ  ์‹ถ๋‹ค๋ฉด ImageViewํƒœ๊ทธ๊ฐ€ ์•„๋‹Œ android.support.v7.widget.AppCompatImageView๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๋˜ํ•œ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋„ ์ง€์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค (xmlns:app="http://schemas.android.com/apk/res-auto")

minSDKversion์ด ๋กค๋ฆฌํŒ(5.0, level 21)์ด์ƒ์ด๋ผ๋ฉด src๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๋•Œ๋ถ€ํ„ฐ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๋จธํ…Œ๋ฆฌ์–ผ ๋””์ž์ธ์ด ์ƒ๊ฒผ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. src์˜ ๊ฒฝ์šฐ xml์—์„œ ImageViewํƒœ๊ทธ์ผ ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด๋•Œ๋ถ€ํ„ฐ src ์„ค์ •์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค. srcCompat๊ณผ ๋‹ฌ๋ฆฌ ๋กค๋ฆฌํŒ ์ด์ƒ๋ถ€ํ„ฐ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›ํ•˜๋ฉฐ ๋กค๋ฆฌํŒ ์ด์ „ ๋ฒ„์ „์„ ์ง€์›ํ•ด์•ผ ํ•  ๊ฒฝ์šฐ์—๋Š” ๋ฐ˜๋“œ์‹œ srcCompat์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

๊ณผ๊ฑฐ์—๋Š” ๋ชจ๋“  ๋ฒ„์ „์„ ์ปค๋ฒ„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์  ๋•Œ๋ฌธ์— srcCompat์ด ๋‚˜๋ฆ„ ๊ฐ€์น˜๊ฐ€ ์žˆ์—ˆ์œผ๋‚˜ ํ˜„์žฌ๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ๋žŒ๋“ค์ด ์ตœ์†Œ ๋กค๋ฆฌํŒ ์ด์ƒ์˜ ๋ฒ„์ „์„ ์“ฐ๊ธฐ ๋•Œ๋ฌธ์— ์žฅ์ ์ด ์ƒ๋‹นํžˆ ํฌ์„๋˜์—ˆ๋‹ค.

์ถœ์ฒ˜: https://ammff.tistory.com/100 [์•„๋ฉ”๋ฆฌ์นด๋…ธ๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ๋ง›์žˆ๋‹ต๋‹ˆ๋‹ค ์—ฌ๋Ÿฌ๋ถ„]

์ด๋ ‡๋‹ค๊ณ ํ•œ๋‹ค.

๊ทธ๋ž˜์„œ src ์†์„ฑ์œผ๋กœ ๋ฐ”๊พธ๋‹ˆ ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค.

level2

1.registerForActivityResult

(์ถ”ํ›„์— ํ•œ๋ฒˆ ์‚ดํŽด๋ณผ๊ฒƒ: parcelable๋กœ ๊ฐ์ฒด ์ „๋‹ฌํ•ด๋ณด์ž)

์›๋ž˜๋Š” ๊ธฐ์กด์— startActivityForResult() ์™€ onActivityResult()์„ ์‚ฌ์šฉํ–ˆ์—ˆ๋Š”๋ฐ deprecated ๋˜๊ณ  ๊ทธ ๋Œ€์šฉ์œผ๋กœ registerForActivityResult๊ฐ€ ๋“ค์–ด์™”๋‹ค.

์šฉ๋„๋ฅผ ์‚ดํŽด๋ณด์ž๋ฉด

startActivity : ์ƒˆ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์—ด์–ด์คŒ (๋‹จ๋ฐฉํ–ฅ)

registerForActivityResult : ์ƒˆ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์—ด์–ด์คŒ + ๊ฒฐ๊ณผ๊ฐ’ ์ „๋‹ฌ (์Œ๋ฐฉํ–ฅ)

๊ฐ„๋‹จํžˆ ๋งํ•ด์„œ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์—ด๋˜ ๊ฐˆ๋•Œ๋Š” ํ•˜๋˜๋Œ€๋กœ putExtra() ๋ฅผ ์ด์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌํ•˜๊ณ  ์—ด๋ฆฐ ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ finish๋กœ ์ข…๋ฃŒ๋˜์—ˆ์„๋•Œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ๊ธฐ์กด ์•กํ‹ฐ๋น„ํ‹ฐ๋กœ ๋Œ์•„์˜ฌ์ˆ˜์žˆ๊ฒŒ ํ•˜๋Š”๊ฒƒ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ข…๋ฃŒ์‹œ์ ์— ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์–ด ๊ฐ€์ง€๊ณ ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ• ์ˆ˜์žˆ๋‹ค.

๊ธฐ์กด startActivityForResult() ์™€ ๋น„๊ตํ•˜์—ฌ ์žฅ์ ์€ ์ด๋Ÿฌํ•˜๋‹ค.

-๋””์ปคํ”Œ๋ง ๋ฐ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ : ๊ธฐ์กด ์•กํ‹ฐ๋น„ํ‹ฐ ๋˜๋Š” ํ”„๋ ˆ๊ทธ๋จผํŠธ์˜ onActivityResult์—์„œ if์™€ else if๋กœ ๋„๋ฐฐ๋˜๋˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋“ค์ด ์ฝœ๋ฐฑ๋ฉ”์„œ๋“œ ๋˜๋Š” ๋ถ„๋ฆฌ๋œ ํด๋ž˜์Šค ๋‹จ์œ„๋กœ ์ชผ๊ฐœ์–ด์ ธ์„œ ๊ด€๋ฆฌ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š” ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ , ์œ ๋‹›ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜์›”ํ•˜๊ฒŒ ํ•˜๋ฉฐ, ์œ ์ง€๋ณด์ˆ˜์ธก๋ฉด์—์„œ๋„ ๋งŽ์€ ๋„์›€์ด ๋œ๋‹ค.

-Type-Safety : ActivityResultContract๋Š” ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์™€ ์ถœ๋ ฅ ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์„ ๊ฐ•์ œํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ž˜๋ชป๋œ ํƒ€์ž…์œผ๋กœ ์บ์ŠคํŒ…ํ•˜๋Š” ์‚ฌ์†Œํ•œ ์‹ค์ˆ˜๋ฅผ ๋ฏธ์—ฐ์— ๋ฐฉ์ง€์‹œ์ผœ์ค€๋‹ค.

-NPE ๋ฐฉ์ง€ : Intent๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์–ป์œผ๋ ค๊ณ  ํ•  ๋•Œ NullPointerException์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝํ—˜์„ ๋ˆ„๊ตฌ๋‚˜ ํ•œ๋ฒˆ์ฏค์€ ํ•ด๋ณด์•˜์„ ๊ฒƒ์ด๋‹ค. ์ƒˆ๋กœ์šด API๋Š” NPE๊ฐ€ ๋ฐœ์ƒํ•  ํ™•๋ฅ ์„ ์ค„์—ฌ์ค„ ๊ฒƒ์ด๋‹ค

์ถœ์ฒ˜ : https://charlezz.medium.com/%EC%95%A1%ED%8B%B0%EB%B9%84%ED%8B%B0-%EA%B2%B0%EA%B3%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-good-bye-startactivityforresult-onactivityresult-82bafc50edac

์œ„์˜ ๋งํฌ์— ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋Œ์•„๊ฐ€๋Š”์ง€์—๋Œ€ํ•ด ์ž์„ธํžˆ ๋‚˜์™€์žˆ๋‹ค. contract๋ฅผ ์ง์ ‘ ์ •์˜ํ•˜๊ณ  ๋“ฑ๋กํ•˜์—ฌ ์‚ฌ์šฉํ• ์ˆ˜๋„์žˆ์ง€๋งŒ ์ด๋ฏธ ์ •์˜๋œ contract๋ฅผ ์‚ฌ์šฉํ• ์ˆ˜๋„์žˆ๋‹ค.

image

์ด๋ ‡๊ฒŒ ์ •์˜๋œ contract๋ฅผ ์ ์ ˆํ•œ ์‹œ๊ธฐ์— ์‚ฌ์šฉํ•˜๋ฉด๋œ๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด ์ด๋ฏธ์ง€ ๋“ฑ์˜ ์ปจํ…์ธ ์˜ uri๋ฅผ ๋ฐ›์•„์˜ค๊ณ ์‹ถ๋‹ค๋ฉด getContent๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ๋˜๋Š”์‹์ด๋‹ค.

์ด์ œ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฒ•์„ ๋ณด์ž๋ฉด

a์—‘ํ‹ฐ๋น„ํ‹ฐ์—์„œ b ์—‘ํ‹ฐ๋น„ํ‹ฐ๋กœ ๋„˜์–ด๊ฐ”๋‹ค๊ฐ€ ์ •๋ณด๋ฅผ ๋“ค๊ณ  a๋กœ ๋‹ค์‹œ ๋„˜์–ด์˜ค๋Š” ์ƒํ™ฉ์ด๋‹ค ์šฐ์„  a์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ๋Š” registerForActivityResultํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด์„œ callback์„ ๋“ฑ๋กํ•ด์ค€๋‹ค. result๋ฅผ ๋ฐ›๊ธฐ์œ„ํ•ด StartActivityForResult๋ฅผ ์ด์šฉํ•œ๋‹ค.

image

๋žŒ๋‹ค์‹ ์•ˆ์—์„œ ๊ทธ๋ž˜์„œ result๋กœ ๋„˜์–ด์˜ค๋Š” ๊ฒฐ๊ณผ๋ฅผ resultCode๋ฅผ ํ™•์ธํ•˜๊ณ  data๋ฅผ ๋ฐ›์•„์„œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ๋ฉด๋œ๋‹ค. data์—๋Š” intent์— putExtra๋กœ ๋„ฃ์–ด๋†“์€๊ฒƒ๋“ค์„ ๋ฝ‘์•„์„œ์“ธ์ˆ˜์žˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด์ œ lacuch๋ฅผ ์‹œ์ผœ์ค˜์•ผํ•˜๋Š”๋ฐ intent์— ๋„˜์–ด๊ฐ€๋ ค๋Š”๊ฒƒ๋“ค ์š”์†Œ ๋„ฃ์–ด์ฃผ๊ณ  ์ธ์ž์— Intent๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉฐ launch๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค.

image

๋‹ค์Œ ์—‘ํ‹ฐ๋น„ํ‹ฐb์—์„œ ํ•ด์•ผํ• ์ผ์„๋ณด์ž

์—‘ํ‹ฐ๋น„ํ‹ฐ b์—์„œ๋Š” intent์— ์›ํ•˜๋Š” ์ •๋ณด putExtra๋กœ ๋„ฃ๊ณ  setResultํ•จ์ˆ˜์— ์ธ์ž๋กœ resultcode์™€ intent๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ  finish()๋กœ a๋กœ ๋„˜์–ด๊ฐ€๋ฉด๋œ๋‹ค.

image

2.๋ช…์‹œ์ ,์•”์‹œ์  ์ธํ…ํŠธ

์ธํ…ํŠธ๋Š” ํ•œ๋งˆ๋””๋กœ ํ™”๋ฉด์ด ์˜ฎ๊ฒจ์ง€๊ฑฐ๋‚˜ ์ „ํ™”๋ฅผ ๊ฑธ๊ฑฐ๋‚˜ ์›นํŽ˜์ด์ง€๋“ค์„ ์—ด๊ฑฐ๋‚˜ ํ• ๋•Œ ์ •๋ณด๋ฅผ 4๋Œ€ ์ปดํฌ๋„ŒํŠธ๋ผ๋ฆฌ ์œ ๊ธฐ์ ์œผ๋กœ ์ •๋ณด์ „๋‹ฌ์„ํ•˜๋ฉฐ ์ž‘๋™ํ• ์ˆ˜์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์š”์†Œ์ด๋‹ค.

๋˜ํ•œ ์ธํ…ํŠธ๋Š” ์ž์‹ ์ด ๋งŒ๋“  ์•ฑ์•ˆ์—์„œ ํ™œ๋™ํ•˜๋Š” ๊ฒƒ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‚ด๊ฐ€ ๋งŒ๋“ค์ง€ ์•Š์€ ํƒ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰ ์•ˆ๋“œ๋กœ์ด๋“œ ์‹œ์Šคํ…œ์€ ๋‚ด๊ฐ€ ๋งŒ๋“  ์ธํ…ํŠธ์˜ ์ •๋ณด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ ๋‚ด๊ฐ€ ๋งŒ๋“  ์•กํ‹ฐ๋น„ํ‚ค๋‚˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ตฌ์„ฑ์š”์†Œ๊ฐ€ ํ•ด์•ผํ•  ์ผ์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ ์ด์™ธ์—๋„ ํƒ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋“ฑ ํ›จ์”ฌ ์œ ์—ฐํ•œ ๊ธฐ๋Šฅ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

์ด์ œ ๋ช…์‹œ์  ์ธํ…ํŠธ์™€ ์•”์‹œ์  ์ธํ…ํŠธ์˜ ์ฐจ์ด์ ์„ ๋ด๋ณด์ž

๋ช…์‹œ์ ์ด๋ฒคํŠธ: ์ธํ…ํŠธ์— ํด๋ž˜์Šค ๊ฐ์ฒด๋‚˜ ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„์„ ์ง€์ •ํ•˜์—ฌ ํ˜ธ์ถœํ• ๋Œ€์ƒ์„ ํ™•์‹คํžˆ ์•Œ์ˆ˜์žˆ๋Š”๊ฒฝ์šฐ์— ์‚ฌ์šฉ,

์ฃผ๋กœ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•œ๋‹ค.

-> ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋‚˜ ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์‹คํ–‰๋˜์–ด์•ผํ• ๊ฒฝ์šฐ ์‚ฌ์šฉํ•œ๋‹ค. ์ฆ‰ ํ™”๋ฉด์ด๋™ ๋“ฑ ์•ฑ๋‚ด์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

์•”์‹œ์ ์ธํ…ํŠธ: ์ธํ…ํŠธ์˜ ์•ก์…˜๊ณผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ •ํ•ด์กŒ์ง€๋งŒ ํ˜ธ์ถœํ•  ๋Œ€์ƒ์ด ๋‹ฌ๋ผ์งˆ์ˆ˜์žˆ๋Š”๊ฒฝ์šฐ ์•”์‹œ์  ์ธํ…ํŠธ๋ฅผ ์‚ฌ์šฉ ์˜ˆ๋ฅผ๋“ค์–ด ์›น์„ ์—ฌ๋Š”๊ฒฝ์šฐ์— ์ง์ ‘ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  ์•ˆ๋“œ๋กœ์ด๋“œ ์‹œ์Šคํ…œ๋‚ด์— ์žˆ๋Š” ์›น๋ธŒ๋ผ์šฐ์ ธ๋ฅผ ๋Œ์–ด๋‹ค ์“ฐ๋Š”๋ฐ ๊ทธ๋•Œ ๋ธŒ๋ผ์šฐ์ ธ๋Š” ์—ฌ๋Ÿฌ๊ฐœ๊ฐ€ ๊น”๋ ค์žˆ์„์ˆ˜ ์žˆ๊ธฐ์— ์ •ํ™•ํžˆ ์–ด๋А๊ฑธ ํ˜ธ์ถœํ• ๊ฒƒ์ธ์ง€ ๋ชจ๋ฅด๋Š” ์ƒํ™ฉ์ด๋‹ค. ์ด๋Ÿฐ๊ฒฝ์šฐ ์•”์‹œ์  ์ธํ…ํŠธ๋ฅผ ํ†ตํ•ด ์ •๋ณด์ฒ˜๋ฆฌ๋ฅผ ํ• ์ˆ˜์žˆ๋Š” ์ ์ ˆํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์•„์™€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ณ ๋ฅด๊ฒŒํ•˜๊ณ  ๊ทธ์—์˜ํ•ด ์ฒ˜๋ฆฌํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š”๊ฒƒ์ด๋‹ค.

ํ•œ๋งˆ๋””๋กœ ์ •๋ฆฌํ•ด์„œ ์ผ๊ณผ ๋ฐ์ดํ„ฐ๋Š” ์ •ํ•ด์กŒ๋Š”๋ฐ ์™ธ๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•˜๋ คํ• ๋•Œ ๊ทธ ์ผ์„ ๋ˆ„๊ฐ€ํ• ์ง€๋Š” ์ •ํ•ด์ ธ์žˆ์ง€์•Š์•„ ๊ทธ๋ƒฅ ๋– ๋„˜๊ฒจ ๋ฒ„๋ฆฌ๊ณ  ๊ทธ์ผ์„ ํ•  ํ”„๋กœ๊ทธ๋žจ์€ ์•ˆ๋“œ๋กœ์ด๋“œ ๋‚ด๋ถ€์ ์œผ๋กœ ์ •ํ•ด์ง€๊ฑฐ๋‚˜ ์—ฌ๋Ÿฌ๊ฐœ์ผ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๊ฐ€ ์ •ํ•˜๋„๋ก ํ•˜๋Š”๊ฒƒ์ด๋‹ค.

3.ConstraintDimensionRatio

๋น„์œจ์„ ์ •ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ ์‹ถ์„๋•Œ weidth๋‚˜ height ๋‘˜์ค‘ํ•˜๋‚˜๋งŒ ์ •ํ•˜๊ณ  ํ•˜๋‚˜๋Š” 0dp๋กœ ์„ค์ •ํ•ด์ฃผ๊ณ  ๊ฐ€๋กœ ์„ธ๋กœ์˜ ๋น„์œจ์„ ConstraintDimensionRatio ์†์„ฑ์„ ํ†ตํ•ด์„œ ์ •ํ•ด์ค€๋‹ค

๋น„์œจ ํ‘œํ˜„๋ฐฉ๋ฒ•์€ ์ด๋Ÿฌํ•˜๋‹ค

  • app:layout_constraintDimensionRatio="1:1" (width:height๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•)

  • app:layout_constraintDimensionRatio="1.0" (width์™€ height์˜ ๋น„์œจ์„ float๊ฐ’์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•)

3-2

์ฝ”ํ‹€๋ฆฐ ํŠน์„ฑ์ƒ ๋งˆ์ง€๋ง‰ ์ธ์ž๊ฐ€ ๋žŒ๋‹ค์‹์ด๋ผ๋ฉด ๊ด„ํ˜ธ ๋ฐ–์œผ๋กœ ๋นผ์„œ ์ž‘์„ฑํ• ์ˆ˜์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

SAM ๋ณ€ํ™˜

์ฝ”ํ‹€๋ฆฐ์—์„œ๋Š” ์ถ”์ƒ ๋ฉ”์†Œ๋“œ ํ•˜๋‚˜๋ฅผ ์ธ์ˆ˜๋กœ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ•จ์ˆ˜๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•˜๋ฉด ํŽธํ•ฉ๋‹ˆ๋‹ค.

์ž๋ฐ”๋กœ ์ž‘์„ฑ๋œ ๋ฉ”์†Œ๋“œ๊ฐ€ ํ•˜๋‚˜์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ๋Œ€์‹  ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ SAM(Single Abstract Method) ๋ณ€ํ™˜ ์ด๋ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋žŒ๋‹ค๊ฐ€ ์–ด๋–ค ๋ฉ”์†Œ๋“œ์˜ ์œ ์ผํ•œ ์ธ์ˆ˜์ธ ๊ฒฝ์šฐ์—๋Š” ๋ฉ”์†Œ๋“œ์˜ ๊ด„ํ˜ธ๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์ถœ์ฒ˜: https://beomseok95.tistory.com/92

2์ฃผ์ฐจ

- ์‹คํ–‰ํ™”๋ฉด

bandicam.2021-10-22.17-29-56-316.mp4

- ์ฝ”๋“œ์„ค๋ช…

์ผ๋‹จ follower๋Š” ๋ฆฌ์ŠคํŠธํ˜•ํƒœ repository๋Š” ๊ทธ๋ฆฌ๋“œ ํ˜•ํƒœ๋กœํ•ด์„œ level2,3 ์ ์šฉ์€ ๋‹ค follower์—๋‹ค๊ฐ€๋งŒ ํ–ˆ๋‹ค.

level1์€ ๋‹น์—ฐํžˆ ์ˆ˜ํ–‰ํ–ˆ๊ณ 

level2

level 2-1์€

๋“ค์–ด๊ฐˆ๋•Œ ์ƒ์„ธํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๋Š”๊ฒƒ์€ DetailFragment๋ฅผ ๋งŒ๋“ค์–ด ๊ฑฐ๊ธฐ์— ๋ฐ์ดํ„ฐ๋ฅผ arguments๋กœ ์ „๋‹ฌํ•ด์„œ ๊ตฌ์„ฑํ–ˆ๊ณ 

๋ฐ‘์— ์„ค๋ช…์€ ์ด๋ฆ„์ด ๋ญ”๊ฐ€์— ๋”ฐ๋ผ์„œ ๊ทธ๋ƒฅ databinding์œผ๋กœ ์ง€๊ฐ€์•Œ์•„์„œ ๋ฐ”๋€Œ๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

๋ณด๊ธฐ์ข‹์œผ๋ผ๊ณ  ํ•„์š”ํ•œ๋ถ€๋ถ„๋งŒ ์ฝ”๋“œ๋ฅผ ์ž˜๋ผ์„œ ๋„ฃ์–ด๋†จ๋Š”๋ฐ ์ž˜ํ•œ๊ฑด์ง€๋Š” ๋ชจ๋ฅด๊ฒ ๋‹ค ํ”ผ๋“œ๋ฐฑ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

2-1

DetailActivity.kt


class DetailActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDetailBinding
    private lateinit var name: String
    private var src by Delegates.notNull<Int>()
    lateinit var detailIntroduce : MutableLiveData<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_detail)
        binding.detail = this
        binding.lifecycleOwner = this

        name = intent.getStringExtra("name")!!
        src = intent.getIntExtra("src",R.drawable.pig)

        siteFragment()
        decideDetailIntroduction()

    }

    fun siteFragment(){
        val detailFragment = DetailFragment()
        var bundle = Bundle()
        bundle.putString("name",name)
        bundle.putInt("src",src)
        detailFragment.arguments = bundle

        supportFragmentManager.beginTransaction().add(R.id.detailFragmentFrame,detailFragment).commit()
    }

    fun decideDetailIntroduction(){
        if (name == "๋ฌธ๋‹ค๋นˆ"){
            detailIntroduce=MutableLiveData<String>().apply { value = "๊ณ ํ–ฅ์€ ๊ฒฝ์ƒ๋‚จ๋„ ํ•ฉ์ฒœ์ด๊ณ ,ํ˜„์žฌ 24์‚ด์ด๋ฉฐ ์•ˆ๋“œ๋กœ์ด๋“œ ํŒŒํŠธ์žฅ์„ ๋งก๊ณ ์žˆ์Œ.. ์•ˆ๋“œ ์ข‹์•„. ์•ˆ๋“œ ์ข‹์•„. ์•ˆ๋“œ ์ข‹์•„.์•ˆ๋“œ ์ข‹์•„. ์•ˆ๋“œ ์ข‹์•„. ์•ˆ๋“œ ์ข‹์•„. ์•ˆ๋“œ ์ข‹... "}
        }else if (name == "์žฅํ˜œ๋ น"){
            detailIntroduce=MutableLiveData<String>().apply { value = "๋ˆ„๊ตฐ์ง€ ๋ชฐ๋ผ์š” ์ตœ์†กํ•ฉ๋‹ˆ๋‹ค"}
        }
   // ์ค‘๋žต

    }

}

DetailFragment.kt

package changhwan.experiment.sopthomework

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import changhwan.experiment.sopthomework.databinding.FragmentDetailBinding
import kotlin.properties.Delegates


class DetailFragment : Fragment() {

    private var _binding: FragmentDetailBinding? = null
    private val binding get() = _binding!!
    private lateinit var name : String
    private var src by Delegates.notNull<Int>()


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentDetailBinding.inflate(layoutInflater, container, false)
        return binding.root

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        name = arguments?.getString("name")!!
        src = arguments?.getInt("src",R.drawable.pig)!!
        binding.detailImage.setImageResource(src!!)
        binding.detailName.text = name
    }



    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

2-2

๊ณผ์ œ์ค‘ ์•Œ๊ฒŒ๋œ๊ฒƒ์— ์ž์„ธํžˆ ์„ค๋ช…ํ•ด๋†จ๋‹ค

CustomDividerDecoration.kt

package changhwan.experiment.sopthomework

import android.graphics.Canvas
import android.graphics.Paint
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView

class CustomDividerDecoration(private val height: Float,private val padding: Float, @ColorInt private val color: Int,private val margin : Int):RecyclerView.ItemDecoration() {

    private val paint = Paint()

    init{
        paint.color = color
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        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.toFloat() + margin)
            val bottom = child.bottom.toFloat() + height + margin

            c.drawRect(left, top, right, bottom, paint)

        }
    }
}

CustomMarginDecoration.kt

package changhwan.experiment.sopthomework

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class CustomMarginDecoration(private val padding: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.top = padding
        outRect.bottom = padding
        outRect.left = padding
        outRect.right = padding
    }
}

FollowerFragment.kt

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        siteFollowerRecycler()

        binding.followerRecycle.addItemDecoration(CustomMarginDecoration(50))
        binding.followerRecycle.addItemDecoration(CustomDividerDecoration(10f,10f, resources.getColor(R.color.main),40))

2-3

์ด๊ฒƒ๋„ ๊ณผ์ œ์ค‘ ์•Œ๊ฒŒ๋œ๊ฒƒ์— ์ž์„ธํžˆ ์„ค๋ช…ํ•ด๋†จ๋‹ค

itemActionListener.kt

package changhwan.experiment.sopthomework

interface ItemActionListener {
    fun onItemMoved(from: Int, to: Int)
    fun onItemSwiped(position: Int)
}

ItemDragListener.kt

package changhwan.experiment.sopthomework

interface ItemActionListener {
    fun onItemMoved(from: Int, to: Int)
    fun onItemSwiped(position: Int)
}

FollowerAdapter.kt

class FollowerAdapter(private val listener: ItemDragListener) :
    RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>(), ItemActionListener {
    //...
    
     override fun onItemMoved(from: Int, to: Int) {
        if (from == to) {
            return
        }

        val fromItem = followerData.removeAt(from)
        followerData.add(to, fromItem)
        notifyItemMoved(from, to)
    }

    override fun onItemSwiped(position: Int) {
        followerData.removeAt(position)
        notifyItemRemoved(position)
    }
    
    
    inner class FollowerViewHolder(
        private val binding: FollowerItemBinding,
        listener: ItemDragListener
    ) : RecyclerView.ViewHolder(binding.root) {
       
        init {
           
            binding.root.setOnTouchListener { v, event ->
                if (event.action == MotionEvent.ACTION_DOWN) {
                    listener.onStartDrag(this)
                }
                false
            }
        }

    }
}

ItemTouchHelperCallBack.kt

package changhwan.experiment.sopthomework

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ItemTouchHelperCallback(val listener: ItemActionListener) : ItemTouchHelper.Callback() {
    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val dragFlags = ItemTouchHelper.DOWN or ItemTouchHelper.UP
        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
        return makeMovementFlags(dragFlags,swipeFlags)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        listener.onItemMoved(viewHolder!!.adapterPosition, target!!.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        listener.onItemSwiped(viewHolder!!.adapterPosition)
    }

    override fun isLongPressDragEnabled(): Boolean = true
}

FollowerFragment.kt

class FollowerFragment : Fragment(), ItemDragListener {
	
    private lateinit var itemTouchHelper : ItemTouchHelper
    
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
       
        itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback(followerAdapter))
        itemTouchHelper.attachToRecyclerView(binding.followerRecycle)
    }
    
     override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
        //์ด๋ถ€๋ถ„ ์ฐธ๊ณ ํ•œ ๋ธ”๋กœ๊ทธ์™€ ๋‹ค๋ฅด๊ฒŒ ์•„๋ฌด๊ฒƒ๋„ ์—†์–ด์•ผ์ง€๋งŒ ๋Œ์•„๊ฐ„๋‹ค ์ดํ•ด์•ˆ๋จ ์ด๋ถ€๋ถ„์€ ์ถ”๊ฐ€์ ์ธ ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค
    }
}

level3

3-1

์€ ์ •๋ณด๋งŒ ๊ณผ์ œ์ค‘ ์•Œ๊ฒŒ๋œ๊ฒƒ์— ์ž‘์„ฑํ•˜๊ณ  ์ ์šฉํ•˜์ง€๋Š” ์•Š์•˜๋‹ค.

3-2

DiffUtil์€ oldList์™€ newList๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ฐจ์ด๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ , newList๋กœ ๊ฐฑ์‹ ํ•ด์ฃผ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ด๋‹ค.

์ฆ‰, ์ด ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•„์ดํ…œ ๋ณ€๊ฒฝ์˜ ๊ตฌ์ฒด์ ์ธ ์ƒํ™ฉ์— ๋”ฐ๋ผ Adapter์˜ ์ ์ ˆํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

ContactDiffUtill.kt

package changhwan.experiment.sopthomework

import androidx.recyclerview.widget.DiffUtil

class ContactDiffUtil(private val oldList: List<FollowerData>, private val currentList: List<FollowerData>):
    DiffUtil.Callback(){
    override fun getOldListSize(): Int =oldList.size

    override fun getNewListSize(): Int =currentList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].followerName==currentList[newItemPosition].followerName
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition]==currentList[newItemPosition]
    }

}

์ด 4๊ฐœ ๋ฉ”์†Œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ•ด์ค˜์•ผ ํ•œ๋‹ค. ๋ฉ”์†Œ๋“œ๋Š” ์ด๋ฆ„๋ช…๊ณผ ๋ฆฌํ„ด๊ฐ’์„ ๋ณด๋ฉด ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€ ์‰ฝ๊ฒŒ ์˜ˆ์ธกํ•  ์ˆ˜ ์žˆ๋‹ค.

FollowerAdapter.kt

//diffUtill ๋ถ€๋ถ„ ์ด์ƒํ•˜๋ฉด ๋‚˜์ค‘์— ๋ฐ”๊ฟ”์•ผํ•จ
fun setContact(contacts: List<FollowerData>){
    val diffResult= DiffUtil.calculateDiff(ContactDiffUtil(this.followerData, followerData), false)
    diffResult.dispatchUpdatesTo(this)
    this.followerData=followerData
}
//์—ฌ๊ธฐ๊นŒ์ง€ diffUtill

์ฝ”๋“œ์˜ ๋œป์€,

  1. calculateDiff()๋กœ oldList์™€ newList์˜ ์ฐจ์ด๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค.
  2. ์ฐจ์ด ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๊ณ , (notify~ ๊ธฐ๋Šฅ์™€ ๊ฐ™๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค).
  3. list๊ฐ€ ๊ฐฑ์‹ ๋˜์—ˆ์œผ๋ฏ€๋กœ ๊ธฐ์กด this.contacts๋ฅผ newList์ธ contacts๋กœ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

FollowerFragment.kt

//diffUtill๋ถ€๋ถ„ ์›๋ž˜๋Š” followerAdapter.notifyDataSetChanged()์˜€์Œ
followerAdapter.setContact(followerAdapter.followerData)
//์—ฌ๊ธฐ๊นŒ์ง€

-์ด๋ฒˆ ๊ณผ์ œ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šด๋‚ด์šฉ

level2

2-1

1.๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ํ•ญ๋ชฉ๋งˆ๋‹ค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋‹ฌ์•„์ฃผ๊ธฐ

๊ฐ ํ•ญ๋ชฉ๋งˆ๋‹ค ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋‹ฌ์•„์„œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ด๋ณด์ž

ViewHolder ํ˜น์€ onBindViewHolder() ํ•จ์ˆ˜ ๋‘๊ณณ์—์„œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ

์šฐ์„  viewholder์—์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š”๊ฒƒ๋ถ€ํ„ฐ ๋ถ€ํ„ฐ ๋ด๋ณผ๊ฒƒ์ด๋‹ค.

1-1ViewHolder์—์„œ ์ฒ˜๋ฆฌ

์šฐ๋ฆฌ๊ฐ€ viewbinding ์„ ์ด์šฉํ•ด์„œ viewHolder๋ฅผ ๋งŒ๋“ค์—ˆ๊ธฐ์— ViewHolder์˜ ์ƒ์„ฑ์ž๋กœ binding๊ฐ์ฒด๋ฅผ ๊ฝ‚์•„์คฌ๋‹ค.

๊ทธ๋ž˜์„œ ์ด binding๊ฐ์ฒด์˜ root๊ฐ€ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ๊ฐ€ ํ‘œํ˜„ํ•˜๋Š” ํ•ญ๋ชฉํ•˜๋‚˜ ์ฆ‰ item์˜ ๋ ˆ์ด์•„์›ƒ์— ์ ‘๊ทผํ• ์ˆ˜์žˆ๋‹ค

๊ทธ๋ž˜์„œ initํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด root์— onClicklistener๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

img

1-2 onBindViewHolder() ์—์„œ ์ฒ˜๋ฆฌ

์ด ํ•จ์ˆ˜ ์•ˆ์—์„œ๋„ item์— ๋Œ€ํ•œ ํด๋ฆญ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ •์˜ํ• ์ˆ˜์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‚œ ์•ž์ „์˜ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ–ˆ๋‹ค

img

๊ฒฐ๊ณผ์ ์œผ๋กœ Holder ํด๋ž˜์Šค ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ๊ณผ ๊ฐ™๋‹ค.

2.๋ฆฌ์‚ฌ์ดํด๋Ÿฌ ๋ทฐ ์–ด๋Œ‘ํ„ฐ์—์„œ startActivityํ•ด๋ณด๊ธฐ

๋ฆฌ์‚ฌ์ดํด๋Ÿฌ ๋ทฐ์—์„œ ์•„์ดํ…œ์„ ํด๋ฆญํ•ด์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ๊นŒ์ง€๋Š” ์‹œ์ผฐ๋Š”๋ฐ ์›ํ•˜๋Š” ์ด๋ฒคํŠธ๊ฐ€ ์ƒˆ๋กœ์šด ์•กํ‹ฐ๋น„ํ‹ฐ ์‹คํ–‰์ผ ๊ฒฝ์šฐ

์–ด๋Œ‘ํ„ฐ์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๊ณ ์žˆ๊ธฐ์— ๊ธฐ์กด์— ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ Intent์— ๋„ฃ์–ด์คฌ๋˜ ์ธ์ž๋“ค์„ ๊ทธ๋Œ€๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ์•ˆ๋œ๋‹ค.

์ผ๋‹จ clickListener์•ˆ์— intent์ถ”๊ฐ€ํ•˜๊ณ  ์ฒซ๋ฒˆ์งธ ์ธ์ž๋กœ binding.root์— context๊ฐ€ ์žˆ๊ธฐ์— ๊ทธ๊ฑธ ์‚ฌ์šฉํ•œ๋‹ค.

๊ทธ๋ž˜์„œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋งž์ถฐ์„œ ๋„ฃ์–ด์ฃผ๊ณ  ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ์ด๋™ํ•˜๋ ค๋Š” ์•กํ‹ฐ๋น„ํ‹ฐ ๋„ฃ๊ณ 

startActivityํ•จ์ˆ˜์— ์ฒซ๋ฒˆ์งธ ์ธ์ž๋กœ binding.root์˜ context, ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ intent, ์„ธ๋ฒˆ์งธ๋กœ ๋ณ„๋‹ค๋ฅธ ์˜ต์…˜์ด์—†๋‹ค๋ฉด null์„ ์ž…๋ ฅํ•˜๋ฉด๋œ๋‹ค.

img

3. activity์—์„œ activity๋กœ ๋ฐ์ดํ„ฐ ์˜ฎ๊ธฐ๊ธฐ

๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ

์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•

// ์ œ์ผ ๋‹จ์ˆœํ•˜๊ณ  ์‰ฌ์šด ๋ฐฉ๋ฒ•
val intent = Intent(this,์˜ฎ๊ฒจ๊ฐˆ ์•กํ‹ฐ๋น„ํ‹ฐ::class.java)
intent.putExtra("num1",1) //๋ฐ์ดํ„ฐ ๋„ฃ๊ธฐ
intent.putExtra("num2",2) //๋ฐ์ดํ„ฐ ๋„ฃ๊ธฐ
startActivity(intent)

๋‘๋ฒˆ์งธ ๋ฐฉ๋ฒ•

val intent = Intent(this@Intent1,Intent2::class.java).apply {
	this.putExtra("num1",1) // ๋ฐ์ดํ„ฐ ๋„ฃ๊ธฐ
   	this.putExtra("num2",2) // ๋ฐ์ดํ„ฐ ๋„ฃ๊ธฐ
}
startActivity(intent)
//์ฝ”ํ‹€๋ฆฐ์˜ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ!๐Ÿคฉ apply
//ํ•œ๋ˆˆ์— ๋ชจ์•„์„œ ๋ณผ ์ˆ˜ ์žˆ์–ด์„œ ์œ ์šฉํ•œ ๋“ฏ

๋ฐ์ดํ„ฐ ๋ฐ›๊ธฐ

val number1 = intent.getIntExtra("num1", 0)
val number2 = intent.getIntExtra("num2", 0)

4. Activity์—์„œ Fragment๋กœ ๋ฐ์ดํ„ฐ ์ฃผ๊ณ  ๋ฐ›๊ธฐ

๋ฐ์ดํ„ฐ ๋ณด๋‚ด๊ธฐ

var fragment2 = Fragment2()
var bundle = Bundle()
bundle.putInt("num1",1)
bundle.putInt("num2",2)
fragment2.arguments = bundle //fragment์˜ arguments์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ bundle์„ ๋„˜๊ฒจ์คŒ

activity?.supportFragmentManager!!.beginTransaction()
                        .replace(R.id.view_main, fragment2)
                        .commit()

๋ฐ์ดํ„ฐ ๋ฐ›๊ธฐ

val num1 = arguments?.getInt("num1")
val num2 = arguments?.getInt("num2")

2-2

itemDecoration ํ™œ์šฉํ•ด์„œ ๊ตฌ๋ถ„์„ ๊ณผ ๊ฐ„๊ฒฉ์ฃผ๊ธฐ

xmlํŒŒ์ผ์—์„œ margin์ด๋‚˜ ๊ตฌ๋ถ„์„ ์„ ์–ด๋А์ •๋„ ๋งŒ๋“ค์ˆ˜์žˆ์ง€๋งŒ ์ด๊ฑฐ๋Š” ์ •ํ™•ํžˆ๋งํ•˜์ž๋ฉด ์ƒํ•˜๋‹จ ๋์ชฝ์— margin์€ ํ•œ๋ฒˆ๋งŒ ๋“ค์–ด๊ฐ€๊ณ  ๋‚˜๋จธ์ง€ ์ค‘๊ฐ„๋ถ€๋ถ„์€ ๋‘๋ฒˆ์”ฉ ๋“ค์–ด๊ฐ€๋Š”๋ฌธ์ œ

๊ตฌ๋ถ„์„ ์€ xml๋‚ด์—์„œ view๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ๋„ฃ์œผ๋ฉด ๋ ˆ์ด์•„์›ƒ์— ๋ถˆํ•„์š”ํ•œ ๋ทฐ๋ฅผ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ ๋ ˆ์ด์•„์›ƒ ๊ณ„์ธต์ด ์ฆ๊ฐ€ํ•˜๊ณ  ๊ทธ์—๋”ฐ๋ผ ์„ฑ๋Šฅ์— ์•ˆ์ข‹์€ ์˜ํ–ฅ์„ ๋ฏธ์น˜๋ฉฐ

์ขŒ์šฐ๋กœ ์Šค์™€์ดํ”„ ํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์ด์žˆ๋‹ค๋ฉด ๊ตฌ๋ถ„์„ ์ด ๊ฐ™์ด ์›€์ง์ธ๋‹ค.

๊ทธ๋ž˜์„œ itemDecoration์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ itemDecoration ํด๋ž˜์Šค๋Š” Recyclerview ๋‚ด๋ถ€์— ์žˆ๋Š” ์ถ”์ƒ ํด๋ž˜์Šค์ด๋‹ค.

์ด๋ฆ„์ฒ˜๋Ÿผ RectclerView์˜ ์•„์ดํ…œ๋“ค์„ ๊พธ๋ฏธ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

์‚ฌ์‹ค ์ปค์Šคํ…€ ํ•˜๋Š”๋Œ€๋กœ ๋งŽ์€ ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ•˜๊ณ ์‹ถ์€๊ฒŒ ์žˆ์œผ๋ฉด ๊ตฌ๊ธ€๋งํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ๊ฒ ๋‹ค ๊ทผ๋ฐ ์™œ ์ฃ„๋‹ค ์˜ˆ์ œ์ฝ”๋“œ๊ฐ€ ์ž๋ฐ”๋ƒ๊ณ !!!!!!!!!!!!!

๋Œ€ํ‘œ์ ์œผ๋กœ ๊ตฌ๋ถ„์„ ์ด๋‚˜ ์—ฌ๋ฐฑ์„ ๋„ฃ๋Š”๋ฐ ๋งŽ์ด๋“ค ์‚ฌ์šฉํ•œ๋‹ค.

๋‚ด๋ถ€ ํ•จ์ˆ˜๊ฐ€ 3๊ฐ€์ง€๊ฐ€ ์žˆ๋Š”๋ฐ

1.onDraw

์•„์ดํ…œ์ด ๊ทธ๋ ค์ง€๊ธฐ ์ „์— ํ˜ธ์ถœ๋จ์œผ๋กœ ์•„์ดํ…œ(viewholder)๋ณด๋‹ค ์•„๋ž˜์— ์œ„์น˜ํ•˜๊ฒŒ๋œ๋‹ค ์•„์ดํ…œ๊ณผ onDraw๊ฐ€ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์ด ๊ฒน์นœ๋‹ค๋ฉด ์•„์ดํ…œ์ด ๋ฎ์–ด์”Œ์›Œ์„œ onDraw๊ฐ€ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์ด ์•ˆ๋ณด์ธ๋‹ค.

2.onDrawOver

์•„์ดํ…œ์ด ๊ทธ๋ ค์ง€๊ณ  ๋‚œ๋‹ค์Œ์— ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋กœ ์ด๊ฑฐ๋Š” ๊ฒน์นœ๋‹ค๋ฉด ์•„์ดํ…œ์„ ๊ฐ€๋ฆด์ˆ˜์žˆ๋‹ค.

3.getItemOffsets

๊ฐ ํ•ญ๋ชฉ์„ ๋ฐฐ์น˜ํ• ๋•Œ ํ˜ธ์ถœํ•œ๋‹ค -> margin์„ ์ค„๋•Œ ์‚ฌ์šฉ

outRect์— ์›ํ•˜๋Š” ํฌ๊ธฐ์— ๊ฐ„๊ฒฉ์„ (์™ผ์ชฝ, ์œ„์ชฝ, ์˜ค๋ฅธ์ชฝ, ์•„๋ž˜์ชฝ) ์˜ 4๊ฐœ ํ•„๋“œ์— ์„ค์ •ํ•ด์ค€๋‹ค.

๊ทธ๋ž˜์„œ ์ผ๋‹จ ๋งŒ๋“ค์–ด๋ณด์ž.

๊ตฌ๊ธ€๋งํ•˜๋ฉด ์˜ˆ์ œ ๊ฒ๋‚˜๊ฒŒ ๋งŽ๋‹ค ๋ณต์žกํ•œ๊ฑฐ๋Š” ๋”๋”์šฑ๋งŽ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ญ”์†Œ๋ฆฌ์ธ์ง€ ์ดํ•ดํ•˜๊ธฐ ์ข€ ๋‚œํ•ดํ•˜๋‹ค

๊ทธ๋ƒฅ ์‰ฌ์šด ๊ฒƒ๋“ค๋กœ ์ ์šฉํ•ด๋ณด๋ฉฐ ํ•„์š”ํ• ๋•Œ ๋งˆ๋‹ค ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•ด ๋‚˜๊ฐ€๊ณ  ๋‚œ์ด๋„๋ฅผ ์กฐ๊ธˆ์”ฉ ๋†’์—ฌ์•ผ๊ฒ ๋‹ค.

๊ฐ€์žฅ์‰ฌ์šด margin๋งŒ๋“ค๊ธฐ

package changhwan.experiment.sopthomework

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class CustomMarginDecoration(private val padding: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.top = padding
        outRect.bottom = padding
        outRect.left = padding
        outRect.right = padding
    }
}

๋”ฐ๋กœ classํŒŒ์ผ์„ ํ•˜๋‚˜ํŒŒ์„œ itemDecoration์„ ์ƒ์†๋ฐ›์€ํ›„

์ƒ์„ฑ์ž๋กœ ๋„์šธ ๊ฐ’์„ ๋ฐ›์•„์„œ

getItemOffsets๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ• ๋•Œ ์‚ฌ๋ฐฉ top,bottom,left,right์— ์ƒ์„ฑ์ž๋กœ ๋ฐ›์€ ๊ฐ’์„ ๋„ฃ์–ด์„œ margin์„ ํ™•๋ณดํ–ˆ๋‹ค.

์ ์šฉ์€ ์ ์šฉ์‹œํ‚ฌ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ์—๋‹ค

 binding.followerRecycle.addItemDecoration(CustomMarginDecoration(50))

์ด๋Ÿฐ์‹์œผ๋กœ addItemDecoration์œผ๋กœ ๋„ฃ์–ด์ฃผ๋ฉด๋œ๋‹ค.(์ƒ๋ช…์ฃผ๊ธฐ์ƒ ํ™”๋ฉด์ด ๊ทธ๋ ค์ง€๊ณ  ๋‚œํ›„์— ์‹คํ–‰์‹œ์ผœ์•ผํ•˜๋Š”๊ฒƒ ๊ฐ™๋‹ค)

๊ฐ€์žฅ์‰ฌ์šด ๊ตฌ๋ถ„์„  ๋งŒ๋“ค๊ธฐ

package changhwan.experiment.sopthomework

import android.graphics.Canvas
import android.graphics.Paint
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView

class CustomDividerDecoration(private val height: Float,private val padding: Float, @ColorInt private val color: Int,private val margin : Int):RecyclerView.ItemDecoration() {

    private val paint = Paint()

    init{
        paint.color = color
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        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.toFloat() + margin)
            val bottom = child.bottom.toFloat() + height + margin

            c.drawRect(left, top, right, bottom, paint)

        }
    }
}

๊ตฌ๋ถ„์„ ์ด ์ข€๋นก์„ธ๋‹ค

for๋ฌธ์— ๋“ค์–ด๊ฐ€๋Š”๊ฒƒ๋“ค์ด ๋ญ”์†Œ๋ฆฌ์ธ๊ฐ€ ์‹ถ์€๋ฐ ์ข€ ๊ณตํ†ต์ ์œผ๋กœ ์˜ˆ์ œ๋งˆ๋‹ค ๊ฑฐ์˜ ๊ฒน์นœ๋‹ค.

๋ช‡๊ฐ€์ง€ ์˜ˆ์ œ๋“ค์˜ ๋ถ€๋ถ„๋ถ€๋ถ„์„ ๋”ฐ์„œ ์„ž์–ด์„œ ์‚ฌ์šฉํ–ˆ๋‹ค

์ƒ์„ฑ์ž์— ๋“ค์–ด๊ฐ€๋Š” height์— ๋”ฐ๋ผ ๊ตฌ๋ถ„์„ ์˜ ๊ตต๊ธฐ๊ฐ€ ๋‹ฌ๋ผ์ง€๊ณ 

padding์— ๋”ฐ๋ผ ์ขŒ์šฐ์— ์—ฌ๋ฐฑ์ด์ƒ๊ธฐ๋ฉฐ

color์€ ์ƒ‰๊น” ์„ค์ •์ด๊ณ 

margin์€ ์–ผ๋งˆ๋‚˜ ๋„์šธ์ง€ ๊ฑฐ๋ฆฌ์„ค์ •์ด๋‹ค.

๊ทผ๋ฐ ์ด๋Ÿฐ๊ฒƒ๋„ ๋‚ด๊ฐ€ ๋ญ๋„ฃ์„์ง€ ์•Œ์•„์„œ ๊ฒฐ์ •์ด๋‹ค ์–ด์งœํ”ผ.

2-3

recyclerview์˜ drag&drop swipe to Dismiss ๊ตฌํ˜„

ItemTouchHelper๋Š” RecyclerView.ItemDecoration์˜ ์„œ๋ธŒ ํด๋ž˜์Šค์ด๋‹ค. RecyclerView ๋ฐ Callback ํด๋ž˜์Šค์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋Ÿฌํ•œ ์•ก์…˜์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ์— ๋”ฐ๋ผ ๋ฉ”์„œ๋“œ๋ฅผ ์žฌ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

ItemTouchHelper.Callback์€ ์ถ”์ƒ ํด๋ž˜์Šค๋กœ ์ถ”์ƒ ๋ฉ”์„œ๋“œ์ธ getMovementFlags(), onMove(), onSwiped()๋ฅผ ํ•„์ˆ˜๋กœ ์žฌ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค. ์•„๋‹ˆ๋ฉด Wrapper ํด๋ž˜์Šค์ธ ItemTouchHelper.SimpleCallback์„ ์ด์šฉํ•ด๋„ ๋œ๋‹ค.

์ด์ œ ์ˆœ์„œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋Š”๊ฒƒ์„ ์ซ’์•„๊ฐ€๋ณด์ž

1.ItemDragListener.kt ๋งŒ๋“ค๊ธฐ

interface ItemDragListener {
    fun onStartDrag(viewHolder :RecyclerView.ViewHolder)
}

์‚ฌ์šฉ์ž๊ฐ€ Drag ์•ก์…˜์„ ์‹œ์ž‘ํ•  ๋•Œ itemTouchHelper์— ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.

2.ItemActionListener.kt ๋งŒ๋“ค๊ธฐ

interface ItemActionListener {
    fun onItemMoved(from: Int, to: Int)
    fun onItemSwiped(position: Int)
}

์•„์ดํ…œ์ด Drag & Drop ๋๊ฑฐ๋‚˜ Swiped ๋์„ ๋•Œ ์–ด๋Œ‘ํ„ฐ์— ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.

3.adapter์—์„œ ItemActionListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„

class FollowerAdapter(private val listener: ItemDragListener) :
    RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>(), ItemActionListener {
    //...
    
     override fun onItemMoved(from: Int, to: Int) {
        if (from == to) {
            return
        }

        val fromItem = followerData.removeAt(from)
        followerData.add(to, fromItem)
        notifyItemMoved(from, to)
    }

    override fun onItemSwiped(position: Int) {
        followerData.removeAt(position)
        notifyItemRemoved(position)
    }
}

์–ด๋Œ‘ํ„ฐ์—์„œ๋Š” ItemActionListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. onItemMoved(), onItemSwiped()์„ ์žฌ์ •์˜ํ•˜์—ฌ ์•„์ดํ…œ ์ด๋™๊ณผ ์ œ๊ฑฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. ์ด๋•Œ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์•„์ดํ…œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก notifyItemMoved(), notifyItemRemoved()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

4.viewAdapter์—์„œ OnTouchListener๋‹ฌ์•„์ฃผ๊ธฐ

inner class FollowerViewHolder(
        private val binding: FollowerItemBinding,
        listener: ItemDragListener
    ) : RecyclerView.ViewHolder(binding.root) {
       
        init {
           
            binding.root.setOnTouchListener { v, event ->
                if (event.action == MotionEvent.ACTION_DOWN) {
                    listener.onStartDrag(this)
                }
                false
            }
        }

    }

์–ด๋Œ‘ํ„ฐ ์ƒ์„ฑ์ž์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ ItemDragListener๋Š” ๋ทฐํ™€๋”์—์„œ ์‚ฌ์šฉ๋œ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค์„ ํ†ตํ•œ ์•„์ดํ…œ ์ด๋™์„ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค ๋ทฐ์— ํ„ฐ์น˜ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋‹ฌ์•„์ค€๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ACTION_DOWN ์•ก์…˜์„ ์ทจํ–ˆ์„ ๋•Œ listener.onStartDrag()๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

5.ItemTouchHelperCallback.kt ์ž‘์„ฑ

package changhwan.experiment.sopthomework

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ItemTouchHelperCallback(val listener: ItemActionListener) : ItemTouchHelper.Callback() {
    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val dragFlags = ItemTouchHelper.DOWN or ItemTouchHelper.UP
        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
        return makeMovementFlags(dragFlags,swipeFlags)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        listener.onItemMoved(viewHolder!!.adapterPosition, target!!.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        listener.onItemSwiped(viewHolder!!.adapterPosition)
    }

    override fun isLongPressDragEnabled(): Boolean = true
}

ItemTouchHelper.Callback์„ ์ƒ์†๋ฐ›๋Š” ItemTouchHelperCallback ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์ƒ์„ฑ์ž์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ItemActionListener๋ฅผ ๋ฐ›๋Š”๋‹ค.

5-1 ์šฐ์„  getMovementFlags()๋ฅผ ์žฌ์ •์˜ํ•ด Drag ๋ฐ Swipe ์ด๋ฒคํŠธ์˜ ๋ฐฉํ–ฅ์„ ์ง€์ •ํ•œ๋‹ค.

5-2 ์•„์ดํ…œ์ด Drag ๋˜๋ฉด ItemTouchHelper๋Š” onMove()๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์ด๋•Œ ItemActionListener๋กœ ์–ด๋Œ‘ํ„ฐ์—

fromPosition๊ณผ toPosition์„ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•œ๋‹ค.

5-3 ์•„์ดํ…œ์ด Swipe ๋˜๋ฉด ItemTouchHelper๋Š” ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ๊นŒ์ง€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•œ ํ›„ onSwiped()๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

์ด๋•Œ ItemActionListener๋กœ ์–ด๋Œ‘ํ„ฐ์— ์ œ๊ฑฐํ•  ์•„์ดํ…œ์˜ position์„ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ•จ๊ป˜ ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•œ๋‹ค.

5-4 isLongPressDragEnabled()์€ ์•„์ดํ…œ์„ ๊ธธ๊ฒŒ ๋ˆ„๋ฅด๋ฉด Drag & Drop ์ž‘์—…์„ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋””ํดํŠธ๋Š”

true์ด๋‹ค

6.์‚ฌ์šฉ์ฒ˜(activity ํ˜น์€ fragment) ์—์„œ ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ๋Š” ItemDragListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„

class FollowerFragment : Fragment(), ItemDragListener {
	
    private lateinit var itemTouchHelper : ItemTouchHelper
    
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
       
        itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback(followerAdapter))
        itemTouchHelper.attachToRecyclerView(binding.followerRecycle)
    }
    
     override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
        //์ด๋ถ€๋ถ„ ์ฐธ๊ณ ํ•œ ๋ธ”๋กœ๊ทธ์™€ ๋‹ค๋ฅด๊ฒŒ ์•„๋ฌด๊ฒƒ๋„ ์—†์–ด์•ผ์ง€๋งŒ ๋Œ์•„๊ฐ„๋‹ค ์ดํ•ด์•ˆ๋จ ์ด๋ถ€๋ถ„์€ ์ถ”๊ฐ€์ ์ธ ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค
    }
}

์ด๋ ‡๊ฒŒํ•˜๋ฉด ์›ํ–ˆ๋˜ ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ• ์ˆ˜์žˆ๋‹ค.

Level3

3-1

๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ์–ด๋–ป๊ฒŒ ์žก์„๊ฒƒ์ธ๊ฐ€?

๋”ฑํžˆ ๋‹น์žฅ ๋งŽ์ด ๊ฐœ์„ ํ• ๋ถ€๋ถ„์ด ์—†๋Š”๋ฐ ๋ณต์žกํ•ด์„œ ๋ฐฉ๋ฒ•๋งŒ ์•Œ์•„๋‘๊ณ  ๊ฐ€์•ผ๊ฒ ๋‹ค.

์–ด๋…ธํ…Œ์ด์…˜ ํ”„๋กœ์„ธ์„œ ๊ฐ™์€๊ฑธ ์‚ฌ์šฉํ•ด์„œ ์ž๋™ํ™” ์ž‘์—…์„ ํ•˜๋Š”๊ฒƒ์ด ์ข‹๋‹ค์ง€๋งŒ ๋„ˆ๋ฌด ๋ณต์žกํ•˜๋‹ค ๋‹น์žฅ ์ด๊ฑฐํ•˜๋‹ค ๋จธ๋ฆฌํ„ฐ์ง„๋‹ค.

๊ทธ๋ž˜์„œ ๋ณต์žกํ•˜์ง€ ์•Š์€ ๋ฐฉ๋ฒ•์„ ๋ดค๋‹ค

base์ฝ”๋“œ ์ฆ‰ BaseActivity, BaseFragment๊ฐ™์€ ์ฝ”๋“œ๋“ค์„ ๋งŒ๋“ค์–ด๋†“๊ณ  ์ƒ์†ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด๋‹ค.

์žฅ๋‹จ์ ์„ ์‚ดํŽด๋ณด๋ฉด

์žฅ์ 

-๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ Activity๋“ค์ด ๊ณตํ†ต์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด BaseActivity๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.๋”ฐ๋ผ์„œ

-Bolierplate ์ฝ”๋“œ๊ฐ€ ์ค„์–ด๋“ ๋‹ค.

๋‹จ์ 

-AppCompatActivity๊ฐ€ ์•„๋‹ˆ๋ผ ํŠน์ •ํ•œ Activity๋ฅผ ์ƒ์†ํ•ด์•ผ๋งŒ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค. => ์–ด์ฉ” ์ˆ˜ ์—†์ด BaseActivity๋ฅผ ์“ฐ์ง€ ๋ชปํ•œ๋‹ค.

-BaseActivity๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ด๋ฅผ ์ƒ์†ํ•œ ๋ชจ๋“  Activity๋“ค์ด ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ๋ถ€๋‹ด์ด ํฌ๋‹ค. (Side Effect)

-๊ณต๋™์ž‘์—…์„ ํ•  ๋•Œ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

์ด์ œ basecode์˜ˆ์‹œ ์ž˜ ๋“ค์–ด๋†“์€ ๋ธ”๋กœ๊ทธ ๋งํฌ๋ฅผ ๊ฑธ์–ด๋†“๊ฒ ๋‹ค ๋‚˜์ค‘์— ์‹œ๋„ํ•ด๋ด์•ผ๊ฒ ๋‹ค ใ…Žใ…Ž

์•กํ‹ฐ๋น„ํ‹ฐ

ํ”„๋ž˜๊ทธ๋จผํŠธ

3-2

notifyDataSetChanged์˜ ๋ฌธ์ œ์ !!

๋ฆฌ์ŠคํŠธ ์—…๋ฐ์ดํŠธ ํ•˜๋Š”๋ฐ 5๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด์žˆ๋‹ค

๋ฆฌ์ŠคํŠธ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋Š”๋ฐฉ๋ฒ•์ค‘ ๊ฐ€์žฅ ํฐ๋ฒ”์œ„์ธ ๋ฆฌ์ŠคํŠธ์˜ ํฌ๊ธฐ์™€ ์•„์ดํ…œ์ด ๋‘˜๋‹ค ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒฝ์šฐ์— ์‚ฌ์šฉํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ์ธ

notifyDataSetChanged๋ฅผ ๋ฌด์ง€์„ฑ์œผ๋กœ ์“ฐ๋ฉด ๋ชจ๋“ ๊ฒฝ์šฐ์— ๋‹ค ์ ์šฉ์ด์•ผ ๋˜๊ฒ ์ง€๋งŒ ๋น„ํšจ์œจ์ ์œผ๋กœ ์›€์ง์ผ๊ฒƒ์ด๋‹ค.

๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋‚˜๋จธ์ง€ 4๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์ ์žฌ์ ์†Œ์— ์ด์šฉํ•ด์„œ ์ž์›์„ ์•„๋ผ์ž

๊ด€๋ จ ๋ฐฉ๋ฒ•๋“ค์„ ์ž˜์ •๋ฆฌํ•ด ๋†“์€ ๋ธ”๋กœ๊ทธ๊ฐ€์žˆ์–ด ๋งํฌ๋กœ ๋‚จ๊ฒจ๋†“๊ฒ ๋‹ค

๊ด€๋ จ ํ•จ์ˆ˜์„ค๋ช… ๋งํฌ

์ž๊ทผ๋ฐ ์ด๋ฐฉ๋ฒ• ๋ง๊ณ  ๋” ์ฐธ์‹ ํ•œ๊ฑฐ ์จ๋ณด์ž

๋ฐ”๋กœ DiffUtil ์ด๋‹ค.

https://velog.io/@deepblue/RecyclerView%EC%9D%98-notifyDataSetChanged

์ด๋ธ”๋กœ๊ทธ์— ์žˆ๋Š”๊ฑฐ ๊ทธ๋Œ€๋กœ ๊ตฌํ˜„ํ•ด๋ดค๋Š”๋ฐ ๋ญ”๊ฐ€ ํ‹€๋ฆฐ๋ถ€๋ถ„์ด ๋ถ„๋ช…์žˆ์„๊ฑฐ๊ฐ™๋‹ค ๊ธ‰ํ•˜๊ฒŒํ•ด์„œ ๋Œ์•„๋Š”๊ฐ€๋Š”๋ฐ

์–ด์จ‹๋“  ์ถ”ํ›„์— ๋‹ค๋ฅธ์ž๋ฃŒ๋“ค๊ณผ ๋น„๊ตํ•ด๋ณด๋ฉด์„œ ์ฒดํฌํ•ด๋ด์•ผ๊ฒ ๋‹ค

์ถœ์ฒ˜:

https://kumgo1d.tistory.com/44

https://velog.io/@jinny_0422/Android-Fragment-Activity%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%A0%84%EB%8B%AC

https://dudmy.net/android/2018/05/02/drag-and-swipe-recyclerview/

https://seunghyun.in/android/1/

https://youngest-programming.tistory.com/285

https://todaycode.tistory.com/55

https://velog.io/@deepblue/RecyclerView%EC%9D%98-notifyDataSetChanged

3์ฃผ์ฐจ # -์‹คํ–‰ํ™”๋ฉด
bandicam.2021-10-30.03-25-12-833.mp4

-์ฝ”๋“œ์„ค๋ช…

level1๋‹คํ–ˆ๊ณ 

level2,3 ๋‹คํ–ˆ๋‹ค.

level2

2-1

NestedScrollableHost.kt

package changhwan.experiment.sopthomework

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign

/**
 * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
 * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
 * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
 *
 * This solution has limitations when using multiple levels of nested scrollable elements
 * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
 */
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)
                    }
                }
            }
        }
    }
}

๊ตฌ๊ธ€์—์„œ ๋ณต๋ถ™ํ–ˆ๋‹ค

<changhwan.experiment.sopthomework.NestedScrollableHost
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/tl_fragment_home">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/vp_fragment_home"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</changhwan.experiment.sopthomework.NestedScrollableHost>

๊ทธํ›„ NestedScrollableHost๋กœ ๊ฐ์‹ธ์คฌ๋‹ค ๋‚ด๋ถ€์—์žˆ๋Š” viewpager2์—

level2-2

์ฒ˜์Œ์— ๊ทธ๋ƒฅ data์— ์ด๋ ‡๊ฒŒ uri์ถ”๊ฐ€ํ•ด์„œ viewhilder๋กœ Glide๋กœ ๋„ฃ์—ˆ์—ˆ๋Š”๋ฐ databindingํ•˜๋ฉด์„œ bindingadapter ๋กœ ํ•ด๊ฒฐํ–ˆ๋‹ค

image

BindingAdapters.kt

package changhwan.experiment.sopthomework


import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import com.bumptech.glide.Glide


object BindingAdapters {

    @JvmStatic
    @BindingAdapter("recyclerGlide")
    fun setImage (imageview : ImageView, url : MutableLiveData<String>){
        Glide.with(imageview.context)
            .load(url.value)
            .circleCrop()
            .into(imageview)
    }
}

follower_item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="profileRecycler"
            type="changhwan.experiment.sopthomework.FollowerData" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/followerLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="49dp"
            android:layout_height="0dp"
            android:layout_marginLeft="21dp"
            android:layout_marginTop="24dp"
            android:layout_marginBottom="24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:recyclerGlide="@{profileRecycler.followerImg}" />

        <TextView
            android:id="@+id/followerName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="22dp"
            android:layout_marginTop="25dp"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:text="@{profileRecycler.followerName}"
            android:textFontWeight="700"
            android:textSize="16sp"
            android:textStyle="normal"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="์ด๋ฆ„" />

        <TextView
            android:id="@+id/followerIntro"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="24sp"
            android:ellipsize="end"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:maxLines="1"
            android:text="@{profileRecycler.followerIntro}"
            android:textFontWeight="400"
            android:textSize="14sp"
            android:textStyle="normal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="@+id/followerName"
            app:layout_constraintTop_toBottomOf="@+id/followerName"
            tools:text="์ž๊ธฐ์†Œ๊ฐœ" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

level3

3-1

์„ค๋ช…์€ ๊ณผ์ œ์—์„œ ๋ฐฐ์šด๊ฑฐ์—์„œ ๋‹คํ–ˆ์Šต๋‹ˆ๋‹ค.

follower_Item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="profileRecycler"
            type="changhwan.experiment.sopthomework.FollowerData" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/followerLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="49dp"
            android:layout_height="0dp"
            android:layout_marginLeft="21dp"
            android:layout_marginTop="24dp"
            android:layout_marginBottom="24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:recyclerGlide="@{profileRecycler.followerImg}" />

        <TextView
            android:id="@+id/followerName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="22dp"
            android:layout_marginTop="25dp"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:text="@{profileRecycler.followerName}"
            android:textFontWeight="700"
            android:textSize="16sp"
            android:textStyle="normal"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="์ด๋ฆ„" />

        <TextView
            android:id="@+id/followerIntro"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="24sp"
            android:ellipsize="end"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:maxLines="1"
            android:text="@{profileRecycler.followerIntro}"
            android:textFontWeight="400"
            android:textSize="14sp"
            android:textStyle="normal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="@+id/followerName"
            app:layout_constraintTop_toBottomOf="@+id/followerName"
            tools:text="์ž๊ธฐ์†Œ๊ฐœ" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

FollowerAdapter.kt

package changhwan.experiment.sopthomework

import android.annotation.SuppressLint
import android.content.Intent
import android.view.KeyEvent.ACTION_DOWN
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import changhwan.experiment.sopthomework.databinding.FollowerItemBinding
import com.bumptech.glide.Glide
import kotlinx.coroutines.processNextEventInCurrentThread

class FollowerAdapter(private val listener: ItemDragListener) :
    RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>(), ItemActionListener {

    var followerData = mutableListOf<FollowerData>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerViewHolder {
        val binding =
            FollowerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return FollowerViewHolder(binding, listener)
    }

    override fun onBindViewHolder(holder: FollowerViewHolder, position: Int) {
        holder.onBind(followerData[position])
    }

    override fun getItemCount(): Int = followerData.size

    //diffUtill ๋ถ€๋ถ„ ์ด์ƒํ•˜๋ฉด ๋‚˜์ค‘์— ๋ฐ”๊ฟ”์•ผํ•จ
    fun setContact(contacts: List<FollowerData>) {
        val diffResult =
            DiffUtil.calculateDiff(ContactDiffUtil(this.followerData, followerData), false)
        diffResult.dispatchUpdatesTo(this)
        this.followerData = followerData
    }
    //์—ฌ๊ธฐ๊นŒ์ง€ diffUtill

    //์•„์ดํ…œ ๋“œ๋ž˜๊ทธ ๋“œ๋กญ
    override fun onItemMoved(from: Int, to: Int) {
        if (from == to) {
            return
        }

        val fromItem = followerData.removeAt(from)
        followerData.add(to, fromItem)
        notifyItemMoved(from, to)
    }

    override fun onItemSwiped(position: Int) {
        followerData.removeAt(position)
        notifyItemRemoved(position)
    }


    @SuppressLint("ClickableViewAccessibility")
    inner class FollowerViewHolder(
        private val binding: FollowerItemBinding,
        listener: ItemDragListener
    ) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(data: FollowerData) {
            binding.profileRecycler = data
            // binding.executePendingBindings() -> ์—†์–ด๋„ ๋œ๋‹จ๋‹ค ์ด๊ฑฐ ๋ฐ”์ธ๋”ฉํ• ๋•Œ ์ž‘์—…๋“ค ๋‹น์žฅ๋‹น์žฅ ์ˆ˜ํ–‰ํ•˜๋ผ๊ณ  ๊ฐ•์š”ํ•˜๋Š” ํ•จ์ˆ˜. ๊ทธ๋ฆฌ๊ณ  lifecycle owner์–ด๋”ฐ๋„ฃ๋ƒ ์•ˆ๋„ฃ์–ด ๋งํ• 
        }

        init {
            binding.root.setOnClickListener {
                val intent = Intent(binding.root?.context, DetailActivity::class.java).apply {
                    this.putExtra("name", followerData[adapterPosition].followerName.value)
                    this.putExtra("src", R.drawable.pig)
                }
                startActivity(binding.root.context, intent, null)
            }

            binding.root.setOnTouchListener { v, event ->
                if (event.action == MotionEvent.ACTION_DOWN) {
                    listener.onStartDrag(this)
                }
                false
            }
        }

    }
}

bindingadapter.kt

package changhwan.experiment.sopthomework


import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import com.bumptech.glide.Glide


object BindingAdapters {

    @JvmStatic
    @BindingAdapter("recyclerGlide")
    fun setImage (imageview : ImageView, url : MutableLiveData<String>){
        Glide.with(imageview.context)
            .load(url.value)
            .circleCrop()
            .into(imageview)
    }
}

3-2

์ด๊ฒƒ๋„ ์„ค๋ช…์€ ์ด๋ฒˆ๊ณผ์ œ์—์„œ ๋ฐฐ์šด๊ฒƒ์— ๋‹คํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์ง„์€ ๋ฐ์ดํ„ฐ๋ฐ”์ธ๋”ฉ์œผ๋กœ ๋„ฃ์—ˆ๋‹ค glide์•ˆ์“ฐ๊ณ  ๊ทธ๋ƒฅ uri๋ณ€์ˆ˜์— ๋‹ด๊ณ  ๋ณ€์ˆ˜๋ฐ”๋กœ ์ด๋ฏธ์ง€๋ทฐ์— ์—ฐ๊ฒฐํ–ˆ๋‹ค

fragment _camera.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="camera"
            type="changhwan.experiment.sopthomework.CameraData" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".CameraFragment">

        <ImageView
            android:id="@+id/camera_img"
            android:layout_width="160dp"
            android:layout_height="0dp"
            android:layout_marginTop="90dp"
            android:src="@{camera.picUri}"
            app:layout_constraintDimensionRatio="1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@tools:sample/avatars" />

        <TextView
            android:id="@+id/camera_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:text="์‚ฌ์ง„์„ ์ฒจ๋ถ€ํ•ด์ฃผ์„ธ์š”"
            android:textColor="#333333"
            android:textFontWeight="700"
            android:textSize="20sp"
            app:layout_constraintEnd_toEndOf="@+id/camera_img"
            app:layout_constraintStart_toStartOf="@+id/camera_img"
            app:layout_constraintTop_toBottomOf="@+id/camera_img" />

        <Button
            android:id="@+id/camera_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dp"
            android:layout_marginTop="30dp"
            android:layout_marginRight="30dp"
            android:background="@drawable/button_border_pink"
            android:text="์ฒจ๋ถ€ํ•˜๊ธฐ"
            android:textColor="#FFFFFF"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/camera_text" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

CameraFragment.kt

package changhwan.experiment.sopthomework

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import changhwan.experiment.sopthomework.databinding.FragmentCameraBinding


class CameraFragment : Fragment() {

    private var _binding : FragmentCameraBinding? = null
    private val binding get() = _binding!!
    private lateinit var getContent: ActivityResultLauncher<Intent>
    private lateinit var fContext : Context

    override fun onAttach(context: Context) {
        super.onAttach(context)
        fContext = context
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = DataBindingUtil.inflate(inflater,R.layout.fragment_camera, container, false)




        binding.lifecycleOwner = viewLifecycleOwner

        initPicUri()
        initIntent()

        return binding.root
    }

    private fun initPicUri(){


        getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            val cameraData = CameraData(picUri = MutableLiveData<Uri>().apply { value = it.data?.data })
            binding.camera = cameraData
        }
    }

    private fun initIntent(){
        val intent = Intent(Intent.ACTION_PICK).apply {
            type = MediaStore.Images.Media.CONTENT_TYPE
            type = "image/*"
        }




        binding.cameraButton.setOnClickListener{
            var permission = ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE)
            if(permission == PackageManager.PERMISSION_DENIED) {
                ActivityCompat.requestPermissions(requireActivity(),arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE)
            } else {
                getContent.launch(intent)
            }
        }
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    companion object{
        const val REQUEST_CODE = 1
    }
}

-์ด๋ฒˆ๊ณผ์ œ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šด๋‚ด์šฉ

๋ผ๋””์˜ค ๋ฒ„ํŠผ ์ปค์Šคํ…€

selector์˜ state_checked ์†์„ฑ์„ ์ด์šฉํ•˜๊ธฐ์œ„ํ•ด ๊ทธ๋ƒฅ ๋ฒ„ํŠผ์ด์•„๋‹Œ ๋ผ๋””์˜ค ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด์„œ ๋ฒ„ํŠผ์„ ๋งŒ๋“ค์–ด์คฌ๋‹ค.

- android:button="@null"

๋ผ๋””์˜ค ๋ฒ„ํŠผ์— ๋™๊ทธ๋ผ๋ฏธ ๋ฒ„ํŠผ ๋ถ€๋ถ„ ์—†์• ๋ ค๋ฉด ์†์„ฑ์— ์ด๋ ‡๊ฒŒ ๋„ฃ์–ด์ฃผ๋ฉด๋œ๋‹ค.


fragment์•ˆ์˜ fragment ์ฒ˜๋ฆฌ

fragment์•ˆ์—์„œ fragment ์ฒ˜๋ฆฌํ• ๋•Œ๋Š” activity์—์„œ ์ฒ˜๋ฆฌํ• ๋•Œ์™€ ๋‹ค๋ฅด๊ฒŒ supportFragmentManager ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด ์•„๋‹Œ

childFragmentManager๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

๋˜ํ•œ ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ ๋ถ€๋ชจ์˜ ํ”„๋ž˜๊ทธ๋จผํŠธ ๋งค๋‹ˆ์ €๋ฅผ ์ ‘๊ทผํ•˜๋ ค๋ฉด ex)fragment1์—์„œ activity์˜ fragment๋กœ ์ ‘๊ทผ

์ด๋Ÿด๊ฒฝ์šฐ์—๋Š” parentFragmentManager๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

https://ddangeun.tistory.com/127

์ž์„ธํ•œ ์„ค๋ช…์€ ์ด ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•˜์ž


glide ์ถ”๊ฐ€ ์‚ฌํ•ญ

dependency๋ฅผ ์ถ”๊ฐ€

implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

์˜ต์…˜์—†๋Š” ์ด๋ฏธ์ง€๋กœ๋“œ

/* Activity์—์„œ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ */

Glide.with(this)
    .load(R.drawable.img_file_name)
    .into(imageView)
/* ViewHolder์—์„œ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ */

Glide.with(itemView)
    .load(R.drawable.img_file_name)
    .into(itemView.imageView)

๋ทฐํ™€๋”์—์„œ๋Š” itemview ์„ binding.root๋กœ ๋Œ€์ฒดํ•˜๋ฉด๋œ๋‹ค.

level 2-2 ์ฒ˜๋Ÿผ ๊ทธ๋ƒฅ ์ด๋ฏธ์ง€ url๋„ฃ์–ด์ฃผ๋ฉด ์•Œ์•„์„œ ํ‘œ์‹œ๋จglide

๊ฐํ•จ์ˆ˜ ์„ค๋ช…

  • with() : View, Fragment ํ˜น์€ Activity๋กœ๋ถ€ํ„ฐ Context๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  • load() : ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•œ๋‹ค. ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. (Bitmap, Drawable, String, Uri, File, ResourId(Int), ByteArray)
  • into() : ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ค„ View๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  • placeholder() : Glide ๋กœ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • error() : ๋ฆฌ์†Œ์Šค๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋‹ค๊ฐ€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • fallback() : loadํ•  url์ด null์ธ ๊ฒฝ์šฐ ๋“ฑ ๋น„์–ด์žˆ์„ ๋•Œ ๋ณด์—ฌ์ค„ ์ด๋ฏธ์ง€๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • skipMemoryCache() : ๋ฉ”๋ชจ๋ฆฌ์— ์บ์‹ฑํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด true๋กœ ์ค€๋‹ค.
  • diskCacheStrategy() : ๋””์Šคํฌ์— ์บ์‹ฑํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด DiskCacheStrategy.NONE๋กœ ์ค€๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ต์…˜์ด ์žˆ๋‹ค. (ALL, AUTOMATIC, DATA, NONE, RESOURCE)

gif๋กœ๋”ฉ๊ธฐ๋Šฅ ์žˆ๋Š”๋ฐ ์ด๊ฑด ๋กœํ‹ฐ์“ฐ๋Š”๊ฒŒ ๋” ์ข‹์€๊ฑฐ์•„๋‹˜?

https://blog.yena.io/studynote/2020/06/10/Android-Glide.html

๊ธฐํƒ€ ์ฐธ๊ณ ์‚ฌํ•ญ์€ ์ด๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•˜์ž


ViewPager2 ์ค‘์ฒฉ์Šคํฌ๋กค ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ

๊ตฌ๊ธ€ ๊ณต์‹๋ฌธ์„œ์— ๋‚˜์™€์žˆ๋Š” ๋ฐฉ๋ฒ•๋Œ€๋กœํ–ˆ๋‹ค

https://developer.android.com/training/animation/vp2-migration?hl=ko

๊ตฌ์ฒด์ ์ธ ๋ฐฉ๋ฒ•์€

1.NestedScrollableHost.kt ํŒŒ์ผ์ถ”๊ฐ€

https://github.com/android/views-widgets-samples/blob/master/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt

๋งํฌ์˜ ๋‚ด์šฉ์„ ๊ธ์–ด์„œ NestedScrollableHost.kt ๋ฅผ ์ถ”๊ฐ€์‹œ์ผœ์ค€๋‹ค.

2.xml ์—์„œ ์ค‘์ฒฉ๋˜๋Š” ์ฆ‰ ๋‚ด๋ถ€์˜ ์Šคํฌ๋กค๋ทฐ(viewpager2) ์— NestedScrollableHost ์”Œ์›Œ์ฃผ๊ธฐ

    <androidx.coordinatorlayout.widget.CoordinatorLayout>

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appBarLayout">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar">

                <com.google.android.material.tabs.TabLayout
                    android:id="@+id/chipsLayout" />

            </androidx.appcompat.widget.Toolbar>

        </com.google.android.material.appbar.AppBarLayout>

        <com.nasrabadiam.widget.widget.NestedScrollableHost
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/chipsViewPager"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </com.nasrabadiam.widget.NestedScrollableHost>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

์ด๋Ÿฐ์‹์œผ๋กœ

๋‚ด๋ถ€์— viewpager2๋ฅผ NestedScrollableHost๋กœ ๊ฐ์‹ธ์ค€๋‹ค.

์ด๋Ÿฌ๋ฉด ๋!!!!

์ฐธ๊ณ ๋ธ”๋กœ๊ทธ:

https://medium.com/@nasrabadiam/support-nested-scrollable-elements-inside-viewpager2-59fa34978899


dataBinding์„ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ ๋ทฐ์— ์ ์šฉํ•˜๊ธฐ

๋ฆฌํŽ™ํ† ๋ง์„ ํ•ด๋ณด์ž

\1. ๋‹น์—ฐํžˆ gradle์ถ”๊ฐ€ํ•ด์ฃผ๊ณ 

android {
    ...
    dataBinding {
        enabled = true
    }
}

2.๋ฆฌ์‚ฌ์ดํด๋Ÿฌ์˜ ์•„์ดํ…œ๋ทฐ ์˜ xml์„ ์œผ๋กœ ๊ฐ์‹ผ๋‹ค.

img

์•„์ดํ…œ์„ ๊ฐ์ŒŒ๋‹ค.

3.data variable ์ถ”๊ฐ€

layout์•ˆ์— ์ถ”๊ฐ€ํ•ด์ค€๋‹ค

<data>

        <variable
            name="profileRecycler"
            type="changhwan.experiment.sopthomework.FollowerData" />
    </data>

4.view์™€ data๋ฅผ @{}๋กœ bindํ•ด์ค€๋‹ค.

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="profileRecycler"
            type="changhwan.experiment.sopthomework.FollowerData" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/followerLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="49dp"
            android:layout_height="0dp"
            android:layout_marginLeft="21dp"
            android:layout_marginTop="24dp"
            android:layout_marginBottom="24dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:recyclerGlide="@{profileRecycler.followerImg}" />

        <TextView
            android:id="@+id/followerName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="22dp"
            android:layout_marginTop="25dp"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:text="@{profileRecycler.followerName}"
            android:textFontWeight="700"
            android:textSize="16sp"
            android:textStyle="normal"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="์ด๋ฆ„" />

        <TextView
            android:id="@+id/followerIntro"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="24sp"
            android:ellipsize="end"
            android:fontFamily="@font/noto_sans_kr"
            android:includeFontPadding="false"
            android:maxLines="1"
            android:text="@{profileRecycler.followerIntro}"
            android:textFontWeight="400"
            android:textSize="14sp"
            android:textStyle="normal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="@+id/followerName"
            app:layout_constraintTop_toBottomOf="@+id/followerName"
            tools:text="์ž๊ธฐ์†Œ๊ฐœ" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

5.viewholder ํด๋ž˜์Šค์—์„œ binding๊ฐ์ฒด์— ๋ฐ์ดํ„ฐ ๋‹ด์•„์ค€๋‹ค.

onbind ํ•จ์ˆ˜์—์„œ ์ผ์ผํžˆ ๋‹ค ๋„ฃ์—ˆ๋˜๊ฑฐ ๊ทธ๋ƒฅ itemview์—์„œ ์„ค์ •ํ•ด๋†“์€ ๋ฐ์ดํ„ฐ ๋ณ€์ˆ˜์— ๋งค๋ฒˆ ๋“ค์–ด์˜ค๋Š” data๋ฅผ ๋„ฃ์–ด์ฃผ๋Š”๊ฑฐ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

 inner class FollowerViewHolder(
        private val binding: FollowerItemBinding,
        listener: ItemDragListener
    ) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(data: FollowerData) {
            binding.profileRecycler = data
            // binding.executePendingBindings() -> ์—†์–ด๋„ ๋œ๋‹จ๋‹ค ์ด๊ฑฐ ๋ฐ”์ธ๋”ฉํ• ๋•Œ ์ž‘์—…๋“ค ๋‹น์žฅ๋‹น์žฅ ์ˆ˜ํ–‰ํ•˜๋ผ๊ณ  ๊ฐ•์š”ํ•˜๋Š” ํ•จ์ˆ˜. 
        }
        //์ค‘๋žต
 }

6.์ด๋ฏธ์ง€ ๊ฐ™์€๊ฑฐ ์ฒ˜๋ฆฌ๋ฅผ์œ„ํ•ด bindingadpter ๋งŒ๋“ค์–ด์ฃผ๊ธฐ

package changhwan.experiment.sopthomework


import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import com.bumptech.glide.Glide


object BindingAdapters {

    @JvmStatic
    @BindingAdapter("recyclerGlide")
    fun setImage (imageview : ImageView, url : MutableLiveData<String>){
        Glide.with(imageview.context)
            .load(url.value)
            .circleCrop()
            .into(imageview)
    }
}

๊ธฐ์กด์— bindingadapter ๊ณต๋ถ€ํ–ˆ๋˜๊ฒƒ์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€๋Šฅํ•œ๊ฑฐ ๋งŒ๋“ค์–ด์ค€๋‹ค. -> ์ด๋ฏธ์ง€์ฒ˜๋ฆฌ ์ด์ œ glide ์ถ”๊ฐ€ํ–ˆ๊ธฐ์— ๊ทธ๊ฑธ๋กœ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฏธ์ง€๋ทฐ์—์„œ ์ด๋ฏธ์ง€ ๋„ฃ๋Š”๋ถ€๋ถ„์ด ์ด๋ ‡๋‹ค

app:recyclerGlide="@{profileRecycler.followerImg}"

๊ฒฐ๋ก ์ ์œผ๋กœ layout ๊ฐ์‹ธ์ฃผ๊ณ  ๋ฐ์ดํ„ฐ ๋งŒ๋“ค์–ด์ฃผ๋Š”๊ณณ์€ item์ด๊ณ 

viewholder์—์„œ ๋ฐ์ดํ„ฐ ์ง‘์–ด๋„ฃ์–ด์ฃผ๋Š”ํ˜•ํƒœ์ด๋‹ค

์ฐธ๊ณ ๋ธ”๋กœ๊ทธ:

https://salix97.tistory.com/244


๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ์ง€ ๋ฐ›์•„์˜ค๊ธฐ

์˜›๋‚ ์— ํ”„๋กœ์ ํŠธ ํ• ๋•Œ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ด์šฉํ•ด์„œ ํฌ๋กญ๊ธฐ๋Šฅ๊ณผ ์•จ๋ฒ”์ ‘๊ทผ๊นŒ์ง€ํ•ด์„œ ๊ถŒํ•œ์„ค์ •๊นŒ์ง€ ๋‹คํ•ด์„œ ํŽธํ–ˆ๋Š”๋ฐ ๊ทธ๋ƒฅ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•˜๋ ค๊ณ  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ด์šฉ ์•ˆํ•˜๋ ค๋‹ˆ ์˜คํžˆ๋ ค ๋”๋ณต์žกํ•˜๋‹ค. ์–ด์จ‹๋“  ์ด๋ฏธ์ง€ ๋Œ๊ณ ์˜ค๋Š”๊ฑฐ ์ž์ฒด๋Š” ๊ฐ„ํŽธํ•˜๋‹ˆ ๊ฐค๋Ÿฌ๋ฆฌ๋กœ intent๋ฅผ ํ†ตํ•ด์ ‘๊ทผํ•ด์„œ 1์žฅ uri๋กœ ๋ฐ›์•„์˜ค๋Š” ๊ฒƒ์„ ๋‹ค๋ค„๋ณด๋ คํ•œ๋‹ค.

package changhwan.experiment.sopthomework

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import changhwan.experiment.sopthomework.databinding.FragmentCameraBinding


class CameraFragment : Fragment() {

    private var _binding : FragmentCameraBinding? = null
    private val binding get() = _binding!!
    private lateinit var getContent: ActivityResultLauncher<Intent>
    private lateinit var fContext : Context

    override fun onAttach(context: Context) {
        super.onAttach(context)
        fContext = context
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = DataBindingUtil.inflate(inflater,R.layout.fragment_camera, container, false)




        binding.lifecycleOwner = this

        initPicUri()
        initIntent()

        return binding.root
    }

    private fun initPicUri(){


        getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            val cameraData = CameraData(picUri = MutableLiveData<Uri>().apply { value = it.data?.data })
            binding.camera = cameraData
        }
    }

    private fun initIntent(){
        val intent = Intent(Intent.ACTION_PICK).apply {
            type = MediaStore.Images.Media.CONTENT_TYPE
            type = "image/*"
        }




        binding.cameraButton.setOnClickListener{
            var permission = ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE)
            if(permission == PackageManager.PERMISSION_DENIED) {
                ActivityCompat.requestPermissions(requireActivity(),arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE)
            } else {
                getContent.launch(intent)
            }
        }
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    companion object{
        const val REQUEST_CODE = 1
    }
}

์šฐ์„  ์ „์ฒด ์ฝ”๋“œ๋Š” ์ด๋ ‡๊ณ  ์ด๋ฏธ์ง€๋Š” uriํ˜•ํƒœ๋กœ ๋ฐ›์•„์™€์„œ ๋ณ€์ˆ˜์— ๋„ฃ๊ณ  ๊ทธ๋ณ€์ˆ˜๋ฅผ ๊ทธ๋ƒฅ ๋ฐ”๋กœ databinding์„ ํ†ตํ•ด์„œ ๋„ฃ์–ด์คฌ๋‹ค.

์ชผ๊ฐœ์„œ ๋ณด์ž๋ฉด

 private lateinit var getContent: ActivityResultLauncher<Intent>

\1. getContent๋ผ๋Š” ๋ณ€์ˆ˜ ์ƒ์„ฑ

๋‚˜์ค‘์— ์ด๋ ‡๊ฒŒ ์ดˆ๊ธฐํ™”ํ•ด์ค€๋‹ค

private fun initPicUri(){


        getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            val cameraData = CameraData(picUri = MutableLiveData<Uri>().apply { value = it.data?.data })
            binding.camera = cameraData
        }
    }

์›๋ž˜ ๊ตฌ๊ธ€์—์„œ๋Š” startActivityForResult๊ฐ€ ์•„๋‹Œ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ๋Š” Contrat ํ•จ์ˆ˜์ธ GetContent()๋ฅผ ์“ฐ๋ผ ํ•˜์ง€๋งŒ ์ด๊ฑธ ์ด์šฉํ•˜๋ฉด ๋ถˆ๋ณ€ํ•œ UI IMAGE PICKER ๊ฐ€ ๋œจ๊ฒŒ๋œ๋‹ค ๊ทธ๋ž˜์„œ ๊ทธ๋ƒฅ ActivityResultContracts.StartActivityForResult() ๋ฅผ ์ด์šฉํ•ด์„œ ์•จ๋ฒ”์— ์ ‘๊ทผํ•˜์˜€๋‹ค.

2.์ธํ…ํŠธ ๋ณ€์ˆ˜ ๋งŒ๋“ค๊ธฐ

private fun initIntent(){
        val intent = Intent(Intent.ACTION_PICK).apply {
            type = MediaStore.Images.Media.CONTENT_TYPE
            type = "image/*"
        }

์ด๋ ‡๊ฒŒ intent๋ฅผ ์„ค์ •ํ•ด์ค€๋‹ค.

3.button์— ๋ˆŒ๋ฆฌ๋ฉด launch๋˜๋„๋ก ์„ค์ •

 binding.cameraButton.setOnClickListener{
            var permission = ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE)
            if(permission == PackageManager.PERMISSION_DENIED) {
                ActivityCompat.requestPermissions(requireActivity(),arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE)
            } else {
                getContent.launch(intent)
            }
        }

getContent.Launch(intent)๋ฅผ ํ†ตํ•ด์„œ ์•จ๋ฒ”์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.

๋‚˜๋จธ์ง€ ์ฝ”๋“œ๋Š” ๊ถŒํ•œ ๊ด€๋ จ์ฝ”๋“œ์ด๋‹ค ๊ถŒํ•œ์— ๋Œ€ํ•œ๊ฒƒ์€ ๋ฐ‘์— ์•Œ์•„๋ณผ๊ฒƒ์ด๋‹ค.

์ฐธ๊ณ ๋ธ”๋กœ๊ทธ:

https://youngest-programming.tistory.com/517


๊ฐค๋Ÿฌ๋ฆฌ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค์ •

์ฐธ๊ณ  ์•กํ‹ฐ๋น„ํ‹ฐ์™€ ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ ๊ถŒํ•œ ๋ฐ›์•„์˜ค๋Š”๊ฒŒ ์€๊ทผ ๋งŽ์ด ๋‹ค๋ฅด๋‹ค ์ด๊ฑธ ์ข€ ์ธ์ง€ํ•˜๊ณ  ๊ฐ€์ž ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ๋Š” ๊ณ ๋ คํ•ด์•ผํ• ๊ฒƒ๋“ค์ด์žˆ๋‹ค -> ์ปจํ…์ŠคํŠธ, ์•กํ‹ฐ๋น„ํ‹ฐ

๊ฐค๋Ÿฌ๋ฆฌ ์ ‘๊ทผํ•˜๋ ค๋ฉด ๊ถŒํ•œ์„ ์–ป์–ด์•ผํ•œ๋‹ค.

์šฐ์„ manifest์— storage ์ฝ๊ธฐ์“ฐ๊ธฐ ๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

์ผ๋‹จ ์ž์„ธํ•˜๊ฒŒ ํ•ด์•ผํ• ๊ฒƒ ๋ชจ๋‘ ๋‚˜์™€์žˆ๋Š” ๋ธ”๋กœ๊ทธ๊ธ€๊ณผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌ๋œ ๋ธ”๋กœ๊ทธ๊ธ€ ํ•˜๋‚˜์”ฉ ๋งํฌ๋ฅผ ๋‚จ๊ฒจ๋†“๋Š”๋‹ค.

๋ณต์žก:

https://manorgass.tistory.com/74

๊ฐ„๋‹จ:

https://superwony.tistory.com/101

์ ์šฉ์€ ๊ฐ„๋‹จํ•œ๊ฑธ๋กœ ํ–ˆ๋‹ค.

์ฝ”๋“œ๋ฅผ ๋ด๋ณด์ž

 binding.cameraButton.setOnClickListener{
            var permission = ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE)
            if(permission == PackageManager.PERMISSION_DENIED) {
                ActivityCompat.requestPermissions(requireActivity(),arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE)
            } else {
                getContent.launch(intent)
            }
        }
    }

ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE)

์ด๋ถ€๋ถ„์€ ์ฝ๊ธฐ ๊ถŒํ•œ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ›์•˜๋Š”์ง€ ์•ˆ๋ฐ›์•˜๋Š”์ง€ ๊ฐ€์ ธ์˜ค๋Š” ๋ถ€๋ถ„์ธ๋ฐ ์ฒซ๋ฒˆ์งธ ์ธ์ž๋กœ context๋ฅผ ๋„ฃ์–ด์ค˜์•ผํ•œ๋‹ค.

๊ทผ๋ฐ fragment๋Š” context๊ฐ€ ์—†๊ธฐ์— ๋ถ€๋ชจ ์•กํ‹ฐ๋น„ํ‹ฐ์˜ context๋ฅผ ๊ฐ€์ ธ์™€์•ผํ•˜๋Š”๋ฐ 2๊ฐ€์ง€๋ฐฉ๋ฒ•์ด์žˆ๋‹ค


-๋ถ€์ œ fragment์—์„œ context ๊ฐ€์ ธ์˜ค๊ธฐ

1.requireContext() ํ•จ์ˆ˜์ด์šฉ ์ดํ•จ์ˆ˜๋ฅผ ์“ฐ๋ฉด getcontext ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ notnullํ•œ context๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

2.onAttach ํ•จ์ˆ˜ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์„œ context ๋ฐ›์•„์˜ค๊ธฐ

onAttach์˜ ์ธ์ž๋กœ ๋ถ€๋ชจ์˜ context๊ฐ€ ๋“ค์–ด์˜ค๊ธฐ์— ๊ฑฐ๊ธฐ์„œ ์ „์—ญ๋ณ€์ˆ˜์— ๋‹ด์•„์„œ ์‚ฌ์šฉํ•ด๋„๋œ๋‹ค.

img


์–ด์จ‹๋“  ์ด๋ ‡๊ฒŒ context ๊ฐ€์ ธ์™€์„œ ์ฒซ๋ฒˆ์งธ ์ธ์ž์—๋„ฃ๊ณ  ๋‘๋ฒˆ์จฐ ์ธ์ž์— ๋ฌด์Šจ ๊ถŒํ•œ์ธ์ง€ ๋„ฃ์–ด์ฃผ๋ฉด ๊ถŒํ•œ์„ ๋ฐ›์€์ง€ ์•ˆ๋ฐ›์€์ง€ ์—ฌ๋ถ€๋ฅผ ์•Œ๋ ค์ค€๋‹ค.

๊ทธ๊ฑฐ๋ฅผ ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๊ถŒํ•œ ์•ˆ๋ฐ›์•˜์œผ๋ฉด ๋ฐ›๋Š” ์ฝ”๋“œ๋ฅผ

์ด๋ฏธ ๋ฐ›์•„์ ธ์žˆ๋‹ค๋ฉด ๋ฐ”๋กœ ์‹คํ–‰ํ•ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์งœ๊ณ 

๊ถŒํ•œ ๋ฐ›์•„์˜ค๋Š” ๋ถ€๋ถ„์„ ๋ด๋ณด์ž

 ActivityCompat.requestPermissions(requireActivity(),arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE)

์ด๋ ‡๊ฒŒ

ActivityCompat.requestPermissions ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ฒซ๋ฒˆ์งธ ์ธ์ž๊ฐ€ activity๋ฅผ ์š”๊ตฌํ•œ๋‹ค.

๊ทธ๋ž˜์„œ ํ”„๋ž˜๊ทธ ๋จผํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์„ ๊ฒ€์ƒ‰ํ•ด๋ณด๋‹ˆ ๊ทธ๋ƒฅ requestPermissions๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‚˜์˜ค๋Š”๋ฐ ์‹คํ–‰์€ ๋˜์ง€๋งŒ ์ด๊ฑด deprecated ๋˜์—ˆ๊ธฐ์—

ํ”„๋ž˜๊ทธ๋จผํŠธ์˜ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ์ˆ˜์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ๋ด๋ณด์ž


-๋ถ€์ œ fragment์—์„œ ๋ถ€๋ชจ activity๊ฐ€์ ธ์˜ค๊ธฐ

requireActivity() ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•œ๋‹ค๋ฉด ๋ถ€๋ชจ Activity๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

getActivity์™€ ๋‹ค๋ฅธ์ ์€ notNullํ•œ Activity๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.


๊ทธ๋ž˜์„œ ์ด๋ ‡๊ฒŒ ActivityCompat.requestPermissions ๋ฅผ ์ด์šฉํ•ด์„œ ๊ถŒํ•œ์„ ๋ฐ›์•„์˜จ๋‹ค.

4์ฃผ์ฐจ

-์‹คํ–‰ํ™”๋ฉด

bandicam.2021-11-12.16-58-11-620.mp4

-ํฌ์ŠคํŠธ๋งจ

๋กœ๊ทธ์ธ

ํšŒ์›๊ฐ€์ž…

ํšŒ์›์กฐํšŒ email

ํšŒ์›์กฐํšŒ userid

-์ฝ”๋“œ์„ค๋ช…

level1 ๋‹คํ–ˆ๊ณ  ์•„๋ฌผ๋ก  ๋กœ๊ทธ์ธ๊ณผ ํšŒ์›๊ฐ€์ž…๋งŒํ•จ

level2,3๋‹คํ•จ

์„ค๋ช…์€ ์ด๋ฒˆ๊ณผ์ œ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šด๋‚ด์šฉ์— ๋‹ค์ ์–ด๋†จ๋‹ค.

level1 sign in์ด ์ฃผ๋ผ sign in๋งŒ ๋„ฃ๊ฒ ์Šต๋‹ˆ๋‹ค ใ…Žใ…Ž ์ง€์†ก

RequestSignInData.kt

package changhwan.experiment.sopthomework

data class RequestSignInData(
    val email : String,
    val password : String
)

ResponseSigninData

package changhwan.experiment.sopthomework

import android.provider.ContactsContract

data class ResponseSignInData(
    val id: Int,
    val name: String,
    val email: String
)

SignInService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

interface SignInService {
    @POST("user/login")
    suspend fun postSignIn(
        @Body body: RequestSignInData
    ):Response<ResponseWrapper<ResponseSignInData>>
}

serviceCreator.kt

package changhwan.experiment.sopthomework

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ServiceCreator {

    private val headerInterceptor = Interceptor{
        val request = it.request()
            .newBuilder()
            .addHeader("Content-Type","application/json")
            .build()
        return@Interceptor it.proceed(request)
    }

    val client: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(headerInterceptor)
        .build()

    private const val  SOPT_BASE_URL= "https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/"

    private val SoptRetrofit :Retrofit = Retrofit.Builder()
        .baseUrl(SOPT_BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val signUpService :SignUpService = SoptRetrofit.create(SignUpService::class.java)
    val signInService :SignInService = SoptRetrofit.create(SignInService::class.java)


    private const val  GITHUB_BASE_URL="https://api.github.com/"

    private  val GitHubRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(GITHUB_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()


    val gitHubService :GitHubService = GitHubRetrofit.create(GitHubService::class.java)
}

SignViewModel.kt

package changhwan.experiment.sopthomework


import android.net.Network
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.CoroutineContext


class SignViewModel : ViewModel() {

    private val _viewEmail = MutableLiveData<String>()
    private val _viewName = MutableLiveData<String>()
    private val _viewPassword = MutableLiveData<String>()
    private val _conSuccess = MutableLiveData<Boolean>()

    val viewEmail: LiveData<String>
        get() = _viewEmail
    val viewName: LiveData<String>
        get() = _viewName
    val viewPassword: LiveData<String>
        get() = _viewPassword
    val conSuccess: LiveData<Boolean>
        get() = _conSuccess


    fun getEmail(email: String) {
        _viewEmail.value = email
    }

    fun getName(name: String) {
        _viewName.value = name
    }

    fun getPassword(password: String) {
        _viewPassword.value = password
    }

    fun startSignUp() {
        val requestSignUpData = RequestSignUpData(
            email = _viewEmail.value!!,
            name = _viewName.value!!,
            password = _viewPassword.value!!
        )

        val call: Call<ResponseSignUpData> =
            ServiceCreator.signUpService.postSignUp(requestSignUpData)

        call.enqueue(object : Callback<ResponseSignUpData> {
            override fun onResponse(
                call: Call<ResponseSignUpData>,
                response: Response<ResponseSignUpData>
            ) {
                if (response.isSuccessful) {
                    val data = response.body()?.data

                    if (data != null) {
                        _viewName.value = data.name
                        _viewEmail.value = data.email
                    }

                    _conSuccess.value = true

                } else {
                    _conSuccess.value = false
                }
            }

            override fun onFailure(call: Call<ResponseSignUpData>, t: Throwable) {
                _conSuccess.value = false
            }
        })
    }


    fun startSignIn() {
        val requestSignInData = RequestSignInData(
            email = _viewEmail.value!!,
            password = _viewPassword.value!!
        )

//        val call : Call<ResponseSignInData> = ServiceCreator.signInService.postSignIn(requestSignInData)
//
//        call.enqueue(object : Callback<ResponseSignInData>{
//            override fun onResponse(
//                call: Call<ResponseSignInData>,
//                response: Response<ResponseSignInData>
//            ) {
//                val data = response.body()?.data
//
//                if(response.isSuccessful){
//                    if (data != null) {
//                        _viewName.value = data.name
//                    }
//                    _conSuccess.value = true
//                } else {
//                    _conSuccess.value = false
//                }
//            }
//
//            override fun onFailure(call: Call<ResponseSignInData>, t: Throwable) {
//                _conSuccess.value = false
//            }
//
//        })

        viewModelScope.launch {
            val response = ServiceCreator.signInService.postSignIn(requestSignInData)
            val data = response.body()?.data

            if (response.isSuccessful) {
                if (data != null) {
                    _viewName.value = data.name
                }
                _conSuccess.value = true
            } else {
                _conSuccess.value = false
            }
        }
    }
}

SignInActivity

package changhwan.experiment.sopthomework

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import changhwan.experiment.sopthomework.databinding.ActivitySignInBinding

class SignInActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySignInBinding
    private lateinit var  getResult : ActivityResultLauncher<Intent>
    private val signInViewModel by viewModels<SignViewModel>()
    val signInEmail = MutableLiveData<String>()
    val signInPassword = MutableLiveData<String>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_sign_in)
        binding.signInData = this
        binding.lifecycleOwner = this

        startLogin()
        startSignUp()
        observeSuccess()
    }

    private fun startLogin(){
        binding.loginButton.setOnClickListener {
            signInEmail.value?.let { signInViewModel.getEmail(it) }
            signInPassword.value?.let { signInViewModel.getPassword(it) }
            signInViewModel.startSignIn()

        }
    }

    private fun observeSuccess(){
        signInViewModel.conSuccess.observe(this, Observer {
            if (signInViewModel.conSuccess.value == true) {
                val intent = Intent(this,HomeActivity::class.java)
                startActivity(intent)
                Toast.makeText(this, "${signInViewModel.viewName.value}๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค", Toast.LENGTH_SHORT).show()
            } else {
                binding.inEditId.text.clear()
                binding.inEditPw.text.clear()
                Toast.makeText(this, "๋กœ๊ทธ์ธ์‹คํŒจ", Toast.LENGTH_SHORT).show()
            }
        })
    }

    private fun startSignUp(){
        binding.signUpButton.setOnClickListener {
            val intent = Intent(this,SignUpActivity::class.java)
            getResult.launch(intent)
        }

        getResult = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()){
            if(it.resultCode == RESULT_OK) {
                binding.inEditId.text.clear()
                binding.inEditId.text.append(it.data?.getStringExtra("Id"))
                binding.inEditPw.text.clear()
                binding.inEditPw.text.append(it.data?.getStringExtra("Pw"))
            }
        }
    }



}

level2-1

ResponseGitHubFollowerData.kt

package changhwan.experiment.sopthomework

data class ResponseGitHubFollowerData(
    val login: String,
    val id: Int,
    val node_id: String,
    val avatar_url: String,
    val gravatar_id: String,
    val url: String,
    val html_url: String,
    val followers_url: String,
    val following_url: String,
    val gists_url: String,
    val starred_url: String,
    val subscriptions_url: String,
    val organizations_url: String,
    val repos_url: String,
    val events_url: String,
    val received_events_url: String,
    val type: String,
    val site_admin: Boolean,
)

ResponseGitHubUserData.kt

package changhwan.experiment.sopthomework

data class ResponseGithubUserData(
    val login: String,
    val id: Int,
    val node_id: String,
    val avatar_url: String,
    val gravatar_id: String,
    val url: String,
    val html_url: String,
    val followers_url: String,
    val following_url: String,
    val gists_url: String,
    val starred_url: String,
    val subscriptions_url: String,
    val organizations_url: String,
    val repos_url: String,
    val events_url: String,
    val received_events_url: String,
    val type: String,
    val site_admin: Boolean,
    val name: String,
    val company: String?,
    val blog: String?,
    val location: String?,
    val email: String?,
    val hireable: String?,
    val bio: String?,
    val twitter_username: String?,
    val public_repos: Int?,
    val public_gists: Int?,
    val followers: Int,
    val following: Int,
    val created_at: String,
    val updated_at: String
)

GitHubService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path

interface GitHubService {

    @GET("users/{userId}/followers")
    fun getGitHubFollowers(
        @Path("userId") userId:String
    ): Call<List<ResponseGitHubFollowerData>>

    @GET("users/{userId}")
    fun getGitHubUsers(
        @Path("userId") userId:String
    ): Call<ResponseGithubUserData>
}

serviceCreator.kt

package changhwan.experiment.sopthomework

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ServiceCreator {

    private val headerInterceptor = Interceptor{
        val request = it.request()
            .newBuilder()
            .addHeader("Content-Type","application/json")
            .build()
        return@Interceptor it.proceed(request)
    }

    val client: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(headerInterceptor)
        .build()

    private const val  SOPT_BASE_URL= "https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/"

    private val SoptRetrofit :Retrofit = Retrofit.Builder()
        .baseUrl(SOPT_BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val signUpService :SignUpService = SoptRetrofit.create(SignUpService::class.java)
    val signInService :SignInService = SoptRetrofit.create(SignInService::class.java)


    private const val  GITHUB_BASE_URL="https://api.github.com/"

    private  val GitHubRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(GITHUB_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()


    val gitHubService :GitHubService = GitHubRetrofit.create(GitHubService::class.java)
}

GitHubViewModel.kt

package changhwan.experiment.sopthomework

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class GitHubViewModel : ViewModel() {

    private val _followerList = mutableListOf<MutableLiveData<String>>()
    private val _followerAvatarUrl = mutableListOf<MutableLiveData<String>>()
    private val _bio = mutableListOf<MutableLiveData<String>>()
    private val _conclusionData = mutableListOf<FollowerData>()
    private val _getFollowerDataDone = MutableLiveData<Event<String>>()
    private val _getUserDataDone = MutableLiveData<Event<String>>()
    private val _getConclusionDataDone = MutableLiveData<Event<String>>()

    val followerList: List<MutableLiveData<String>>
        get() = _followerList
    val followerAvatarUrl: List<MutableLiveData<String>>
        get() = _followerAvatarUrl
    val bio: List<MutableLiveData<String>>
        get() = _bio
    val conclusionData: List<FollowerData>
        get() = _conclusionData
    val getFollowerDataDone: LiveData<Event<String>>
        get() = _getFollowerDataDone
    val getUserDataDone: LiveData<Event<String>>
        get() = _getUserDataDone
    val getConclusionDataDone: LiveData<Event<String>>
        get() = _getConclusionDataDone

    fun getGitHubFollowerData() {
        val call: Call<List<ResponseGitHubFollowerData>> =
            ServiceCreator.gitHubService.getGitHubFollowers("2chang5")

        call.enqueue(object : Callback<List<ResponseGitHubFollowerData>> {
            override fun onResponse(
                call: Call<List<ResponseGitHubFollowerData>>,
                response: Response<List<ResponseGitHubFollowerData>>
            ) {
                if (response.isSuccessful) {
                    val data = response.body()
                    if (data != null) {
                        _followerList.clear()
                        _followerAvatarUrl.clear()
                        for (i in data) {
                            _followerList.add(MutableLiveData<String>().apply { value = i.login })
                            _followerAvatarUrl.add(MutableLiveData<String>().apply { value = i.avatar_url })
                        }
                    }
                    _getFollowerDataDone.value = Event("followerDone")
                }
            }

            override fun onFailure(call: Call<List<ResponseGitHubFollowerData>>, t: Throwable) {

            }

        })
    }

    fun getGitHubUserData() {
        for (i in _followerList) {
            val call: Call<ResponseGithubUserData>? = i.value?.let {
                ServiceCreator.gitHubService.getGitHubUsers(
                    it
                )
            }

            if (call != null) {
                call.enqueue(object : Callback<ResponseGithubUserData> {
                    override fun onResponse(
                        call: Call<ResponseGithubUserData>,
                        response: Response<ResponseGithubUserData>
                    ) {
                        if (response.isSuccessful) {
                            val data = response.body()?.bio

                            _bio.add(MutableLiveData<String>().apply { value = data })
                            if(i == _followerList.last()){
                                _getUserDataDone.value = Event("UserDone")
                            }
                        } else{

                        }

                    }

                    override fun onFailure(call: Call<ResponseGithubUserData>, t: Throwable) {

                    }
                })
            }
        }
    }


    fun getConclusionData() {
        _conclusionData.clear()
        for (i in _followerList.indices) {
            _conclusionData.add(
                FollowerData(
                    followerName = _followerList[i],
                    followerImg = _followerAvatarUrl[i],
                    followerIntro = _bio[i]
                    )
            )
        }
        _getConclusionDataDone.value = Event("ConclusionDone")
    }
}

FollowerFragmnet.kt

package changhwan.experiment.sopthomework

import android.graphics.Color
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import changhwan.experiment.sopthomework.databinding.FragmentFollowerBinding


class FollowerFragment : Fragment(), ItemDragListener {

    private var _binding: FragmentFollowerBinding? = null
    private val binding get() = _binding!!
    private lateinit var followerAdapter: FollowerAdapter
    private lateinit var itemTouchHelper : ItemTouchHelper
    private val gitHubViewModel by activityViewModels<GitHubViewModel>()



    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        gitHubViewModel.getGitHubFollowerData()
        gitHubViewModel.getFollowerDataDone.observe(viewLifecycleOwner, EventObserver{
            gitHubViewModel.getGitHubUserData()
        })
        gitHubViewModel.getUserDataDone.observe(viewLifecycleOwner, EventObserver{
            gitHubViewModel.getConclusionData()
        })

        _binding = FragmentFollowerBinding.inflate(layoutInflater, container, false)
        return binding.root


    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        gitHubViewModel.getConclusionDataDone.observe(viewLifecycleOwner,EventObserver{
            siteFollowerRecycler()

            binding.followerRecycle.addItemDecoration(CustomMarginDecoration(24))
            binding.followerRecycle.addItemDecoration(CustomDividerDecoration(1f,10f, resources.getColor(R.color.divider),40))

            itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback(followerAdapter))
            itemTouchHelper.attachToRecyclerView(binding.followerRecycle)
        })

    }

    fun siteFollowerRecycler(){
        followerAdapter = FollowerAdapter(this)

        binding.followerRecycle.adapter = followerAdapter

        followerAdapter.followerData.clear()

        followerAdapter.followerData.addAll(
            gitHubViewModel.conclusionData
        )



        //diffUtill๋ถ€๋ถ„ ์›๋ž˜๋Š” followerAdapter.notifyDataSetChanged()์˜€์Œ
        followerAdapter.setContact(followerAdapter.followerData)
        //์—ฌ๊ธฐ๊นŒ์ง€
    }

    override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {

    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

2-2

ServiceCreator.kt

package changhwan.experiment.sopthomework

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ServiceCreator {

    private val headerInterceptor = Interceptor{
        val request = it.request()
            .newBuilder()
            .addHeader("Content-Type","application/json")
            .build()
        return@Interceptor it.proceed(request)
    }

    val client: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(headerInterceptor)
        .build()

    private const val  SOPT_BASE_URL= "https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/"

    private val SoptRetrofit :Retrofit = Retrofit.Builder()
        .baseUrl(SOPT_BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val signUpService :SignUpService = SoptRetrofit.create(SignUpService::class.java)
    val signInService :SignInService = SoptRetrofit.create(SignInService::class.java)


    private const val  GITHUB_BASE_URL="https://api.github.com/"

    private  val GitHubRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(GITHUB_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()


    val gitHubService :GitHubService = GitHubRetrofit.create(GitHubService::class.java)
}

2-3

ResponseWrapper.kt

package changhwan.experiment.sopthomework

data class ResponseWrapper<T>(
    val status: Int,
    val success: Boolean,
    val message: String,
    val data: T?
)

ResponseSignInData.kt

package changhwan.experiment.sopthomework

import android.provider.ContactsContract

data class ResponseSignInData(
    val id: Int,
    val name: String,
    val email: String
)

SignInService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

interface SignInService {
    @POST("user/login")
    suspend fun postSignIn(
        @Body body: RequestSignInData
    ):Response<ResponseWrapper<ResponseSignInData>>
}

3

SignInService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

interface SignInService {
    @POST("user/login")
    suspend fun postSignIn(
        @Body body: RequestSignInData
    ):Response<ResponseWrapper<ResponseSignInData>>
}

SignViewModel.kt

package changhwan.experiment.sopthomework


import android.net.Network
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.CoroutineContext


class SignViewModel : ViewModel() {

    private val _viewEmail = MutableLiveData<String>()
    private val _viewName = MutableLiveData<String>()
    private val _viewPassword = MutableLiveData<String>()
    private val _conSuccess = MutableLiveData<Boolean>()

    val viewEmail: LiveData<String>
        get() = _viewEmail
    val viewName: LiveData<String>
        get() = _viewName
    val viewPassword: LiveData<String>
        get() = _viewPassword
    val conSuccess: LiveData<Boolean>
        get() = _conSuccess


    fun getEmail(email: String) {
        _viewEmail.value = email
    }

    fun getName(name: String) {
        _viewName.value = name
    }

    fun getPassword(password: String) {
        _viewPassword.value = password
    }

    fun startSignUp() {
        val requestSignUpData = RequestSignUpData(
            email = _viewEmail.value!!,
            name = _viewName.value!!,
            password = _viewPassword.value!!
        )

        val call: Call<ResponseSignUpData> =
            ServiceCreator.signUpService.postSignUp(requestSignUpData)

        call.enqueue(object : Callback<ResponseSignUpData> {
            override fun onResponse(
                call: Call<ResponseSignUpData>,
                response: Response<ResponseSignUpData>
            ) {
                if (response.isSuccessful) {
                    val data = response.body()?.data

                    if (data != null) {
                        _viewName.value = data.name
                        _viewEmail.value = data.email
                    }

                    _conSuccess.value = true

                } else {
                    _conSuccess.value = false
                }
            }

            override fun onFailure(call: Call<ResponseSignUpData>, t: Throwable) {
                _conSuccess.value = false
            }
        })
    }


    fun startSignIn() {
        val requestSignInData = RequestSignInData(
            email = _viewEmail.value!!,
            password = _viewPassword.value!!
        )

//        val call : Call<ResponseSignInData> = ServiceCreator.signInService.postSignIn(requestSignInData)
//
//        call.enqueue(object : Callback<ResponseSignInData>{
//            override fun onResponse(
//                call: Call<ResponseSignInData>,
//                response: Response<ResponseSignInData>
//            ) {
//                val data = response.body()?.data
//
//                if(response.isSuccessful){
//                    if (data != null) {
//                        _viewName.value = data.name
//                    }
//                    _conSuccess.value = true
//                } else {
//                    _conSuccess.value = false
//                }
//            }
//
//            override fun onFailure(call: Call<ResponseSignInData>, t: Throwable) {
//                _conSuccess.value = false
//            }
//
//        })

        viewModelScope.launch {
            val response = ServiceCreator.signInService.postSignIn(requestSignInData)
            val data = response.body()?.data

            if (response.isSuccessful) {
                if (data != null) {
                    _viewName.value = data.name
                }
                _conSuccess.value = true
            } else {
                _conSuccess.value = false
            }
        }
    }
}

-์ด๋ฒˆ๊ณผ์ œ๋ฅผ ํ†ตํ•ด ๋ฐฐ์šด๋‚ด์šฉ

ViewModel๊ด€๋ จ

๊ณผ์ œ 4์ฃผ์ฐจ ๋ณด๋ฉด ์•Œ๋“ฏ์ด

๋ทฐ๋ชจ๋ธ์—์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ •๋ณด ๋Œ์–ด์˜ค๋Š”๊ฑฐ ์ฒ˜๋ฆฌํ•ด์„œ ๊ฐ€์ ธ์™€์„œ ์ €์žฅํ–ˆ์—ˆ๊ณ  ๊ทธ์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ

์ธํ…ํŠธ๋‚˜ ํ† ์ŠคํŠธ ๋ฉ”์„ธ์ง€ ๋“ฑ๋“ฑ์€ ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ ์‹คํ–‰ํ–ˆ๋‹ค

๊ทผ๋ฐ ์ •๋ณด๊ฐ€์ ธ์˜ค๋Š” ๋ฒ„ํŠผ ๋ฆฌ์Šค๋„ˆ ์—์„œ ๋ทฐ๋ชจ๋ธ์˜ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผํ•˜๊ณ  ๋™์‹œ์—

์ •๋ณด๊ฐ€์ ธ์˜ค๋Š”๊ฑฐ ๋ฟŒ๋ ค์ฃผ๋Š”๊ฑธํ•˜๋ฉด ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๊ธฐ๋„ ์ „์— ์‹คํ–‰ํ•ด์„œ

nullpointexception์ด๋‚˜ ๋‚ด๊ฐ€ ์›ํ•˜๋Š”๊ฑธ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•˜๋Š” ์ผ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

๊ทธ๋ž˜๋„ ํ† ์ŠคํŠธ๋ฉ”์„ธ์ง€๋‚˜ ์ธํ…ํŠธ๊ฐ™์€ ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผํ• ๊ฒƒ๋“ค์€ ๋”ฐ๋กœ์žˆ๊ธฐ์— ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•ด์„œ

๋น„๋™๊ธฐ ์™„๋ฃŒ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ณ€์ˆ˜๋ฅผ booleanํ˜•ํƒœ๋กœ ๋ผ์ด๋ธŒ๋ฐ์ดํ„ฐ๋กœ ๋†“๊ณ  ์˜ต์ €๋ฒ„๋ฅผ ๋‹ฌ์•„์„œ ๋ณ€ํ™”๊ฐ€์žˆ์„๊ฒฝ์šฐ ์ฒ˜๋ฆฌํ•˜๋„๋กํ–ˆ๋‹ค ๊ทธ๋ฆฌ๊ณ  ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ ์„ฑ๊ณต์—ฌ๋ถ€ 200์ธ์ง€ 400์ธ์ง€๋Š” ๋ณ€์ˆ˜์— true/false์—ฌ๋ถ€๋กœ ํŒ๋‹จํ•˜์˜€๋‹ค.

๊ทธ๋ž˜์„œ ํ•ด๊ฒฐ์€ ํ–ˆ์ง€๋งŒ ๋ญ”๊ฐ€ ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—†๋‚˜์‹ถ์–ด ๋ฌธ๋‹ค๋นˆ์—๊ฒŒ ๋ฌผ์–ด๋ดค๋Š”๋ฐ

๋งž๋Š” ๋ฐฉ๋ฒ•์ธ๋ฐ booleanํ˜•ํƒœ๋กœ ์‹ค์ œ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ ๊ฐ’์ด์•„๋‹Œ ์ด๋ฒคํŠธ์ฒ˜๋ฆฌ์—๋งŒ ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋Š” Event wapper๊ฐœ๋…์„

์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด ๋งž๋‹ค๊ณ ํ•œ๋‹ค

-> ์ถ”ํ›„์— ๊ด€๋ จ ์ •๋ฆฌ๊ธ€์„ ์จ์•ผ๊ฒ ๋‹ค.

https://medium.com/prnd/mvvm%EC%9D%98-viewmodel%EC%97%90%EC%84%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-6%EA%B0%80%EC%A7%80-31bb183a88ce

๊ด€๋ จ ๋ธ”๋กœ๊ทธ๊ธ€์ด๋‹ค ๋˜ํ•œ ๋”์šฑ ๋ฐœ์ „ํ•œ ๋‚ด์šฉ๋“ค๋„์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜์ž

๋˜ํ•œ ๋กœ๊ทธ์ธํ• ๋•Œ edittext์— ๋‹ด๊ธด text๊ฐ™์€๊ฒƒ๋“ค์€ sharedPreference์— ์ €์žฅํ•ด์„œ ๊ฐ€์ง€๊ณ  ์˜ค๋Š”๊ฒƒ์ด ์ข‹๋‹ค๊ณ ํ•œ๋‹ค.

์ด๋ถ€๋ถ„๋„ ์ฐธ๊ณ ํ•ด์„œ ์ˆ˜์ •ํ•ด๋ณด์ž

๋˜ํ•œ ์ด๋Ÿฐ๋ถ€๋ถ„์—์„œ ๋ทฐ๋ชจ๋ธ์—์„œ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚œํ›„ ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ ์ฝ”๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฒƒ์— ๋‹ค๋ฅธ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์—†๋‚˜ ๋ฌผ์–ด๋ณธ๊ฒฐ๊ณผ

StateFlow๋ผ๋Š” ์ฝ”๋ฃจํ‹ด ๊ฐœ๋…์ค‘์— ํ•˜๋‚˜๊ฐ€ ์žˆ๋Š”๋ฐ ๋„ˆ๋ฌด์–ด๋ ค์šธ๊ฒƒ์ด๋‹ˆ ๋‚˜์ค‘์— ์‚ฌ์šฉํ•ด๋ณด๋ผ๋Š” ์กฐ์–ธ์ด์žˆ์—ˆ๋‹ค.


2-1 github api ์ ์šฉํ•˜๊ธฐ

์‹œํ–‰ ์ฐฉ์˜ค๊ฐ€ ์ •๋ง ๋งŽ์•˜๋‹ค ์—ฌ๊ธฐ์„œ ๊ฐ€์ ธ๊ฐ€์•ผํ• ๊ฒƒ๋“ค์„ ์‚ดํŽด๋ณด์ž

1.get์‚ฌ์šฉ๋ฐฉ๋ฒ•

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path

interface GitHubService {

    @GET("users/{userId}/followers")
    fun getGitHubFollowers(
        @Path("userId") userId:String
    ): Call<List<ResponseGitHubFollowerData>>

    @GET("users/{userId}")
    fun getGitHubUsers(
        @Path("userId") userId:String
    ): Call<ResponseGithubUserData>
}

์ด๋Ÿฐ์‹์œผ๋กœ ์ธํ„ฐํŽ˜์ด์Šค ํ•˜๋‚˜์— ์—ฌ๋Ÿฌ๊ฐœ์˜ http๋ฉ”์„œ๋“œ ๋„ฃ์„์ˆ˜์žˆ๊ณ  ์ค‘๊ฐ„์— path ๋ถ€๋ถ„์„ ๋ผ์›Œ๋„ฃ์„์ˆ˜์žˆ๋‹ค

"users/{userId}/followers" ์˜ˆ์‹œ์ฒ˜๋Ÿผ

2.์—ฌ๋Ÿฌ๊ฐœ์˜ api์‚ฌ์šฉํ•ด์„œ baseurl์—ฌ๋Ÿฌ๊ฐœ์ธ๊ฒฝ์šฐ

package changhwan.experiment.sopthomework

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ServiceCreator {
    private const val  SOPT_BASE_URL= "https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/"

    private val SoptRetrofit :Retrofit = Retrofit.Builder()
        .baseUrl(SOPT_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val signUpService :SignUpService = SoptRetrofit.create(SignUpService::class.java)
    val signInService :SignInService = SoptRetrofit.create(SignInService::class.java)


    private const val  GITHUB_BASE_URL="https://api.github.com/"

    private  val GitHubRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(GITHUB_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()


    val gitHubService :GitHubService = GitHubRetrofit.create(GitHubService::class.java)
}

retrofit ๊ฐ์ฒด ๋‘๊ฐœ๋งŒ๋“ค์–ด์„œ ๊ทธ๋ƒฅ ์“ฐ๋ฉด๋œ๋‹ค.

3.์„œ๋ฒ„ํ†ต์‹ ํ• ๋•Œ๋Š” ์•ž์ „ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ž‘์—…์ด ๋๋‚˜๊ณ  ๋‹ค์Œ๊ฒƒ์„ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก(์•ž๋ฐ์ดํ„ฐ๊ฐ€ ๋’ค์ชฝ์— ๋ถˆ๋Ÿฌ์˜ค๋Š”๊ฒƒ์„ ๊ด€์—ฌํ• ๋•Œ) ์ž˜ ๋กœ์ง์„ ์งœ์•ผํ•œ๋‹ค ๋จธ๋ฆฌ๋ฅผ ๊ตด๋ฆฌ์ž

Log

-> ๋˜ํ•œ ์ด๋Ÿด๋•Œ ์ž˜์•ˆ๋˜๋ฉด

img

Log.d๋ฅผ ์ด์šฉํ•ด์„œ ์ฐ์–ด๋ณด์ž

img

์ด๋ ‡๊ฒŒ ๊ฒ€์ƒ‰ํ•ด์„œ ๋ด๋ผ


์˜ค๋ฅ˜์ฒ˜๋ฆฌ

๊ทธ๋ฆฌ๊ณ  ์˜ค๋ฅ˜๋‚˜๋ฉด

logcat์—์„œ

img

error๋กœ ์„ ํƒํ•˜๊ณ  exception๋†“์œผ๋ฉด ์˜ค๋ฅ˜ ๋ญ”์ง€ ๋‚˜์˜จ๋‹ค ๊ทธ๊ฑฐ๋ณด๊ณ  ํ•ด๊ฒฐํ•ด๋ณด์ž


์ด๋ฒˆ์— ์„œ๋ฒ„ํ†ต์‹ ํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋ฉด์„œ ๊ณจ์น˜ ์•„ํŒ ๋˜๊ฑฐ

fun getGitHubUserData() {
        for (i in _followerList) {
            val call: Call<ResponseGithubUserData>? = i.value?.let {
                ServiceCreator.gitHubService.getGitHubUsers(
                    it
                )
            }

            if (call != null) {
                call.enqueue(object : Callback<ResponseGithubUserData> {
                    override fun onResponse(
                        call: Call<ResponseGithubUserData>,
                        response: Response<ResponseGithubUserData>
                    ) {
                        if (response.isSuccessful) {
                            val data = response.body()?.bio

                            _bio.add(MutableLiveData<String>().apply { value = data })
                            if(i == _followerList.last()){
                                _getUserDataDone.value = Event("UserDone")
                            }
                        } else{

                        }

                    }

                    override fun onFailure(call: Call<ResponseGithubUserData>, t: Throwable) {

                    }
                })
            }
        }
    }

getUserDataDone ์—…๋ฐ์ดํŠธ ์‹œ๊ธฐ ์ฒซ๋ฒˆ์งธ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋‚˜ ์•„๋‹ˆ๋ฉด for๋ฌธ ๋์— ๋ถ™์—ฌ๋†“์œผ๋ฉด ๋ฌธ์ œ์žˆ์—ˆ์Œ ๋น„๋™๊ธฐ๋ผ ๋จผ์ € ์ฒ˜๋ฆฌ๋˜์„œ

๋’ค์ชฝ ์ฝ”๋“œ์—์„œ ์˜ค๋ฅ˜๋‚ฌ์Œ ๊ทธ๋ž˜์„œ ๋งˆ์ง€๋ง‰ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์—์„œ ๋ฐ”๊ฟ€์ˆ˜์žˆ๋„๋ก

if(i == _followerList.last()){
   _getUserDataDone.value = Event("UserDone")
}

์ด๋ถ€๋ถ„์„ ์ถ”๊ฐ€ํ•ด์„œ ๋งˆ์ง€๋ง‰ ์›์†Œ์ผ๋•Œ Event์˜ ๋ณ€ํ™”๊ฐ€ ์˜ค๋„๋กํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Event๋ฅผ ์“ด๋‹ค๊ณ  ๋ผ์ด๋ธŒ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€ํ™”๋ฅผ ์•ˆํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค null๊ฐ’์œผ๋กœ ๊ณ„์† ์—…๋ฐ์ดํŠธ๋˜๊ณ 

๊ทธ์—๋”ฐ๋ผ ์ด๋ฒคํŠธ ๊ณ„์† ๋ฐœ์ƒํ•œ๋‹ค.

-> ๋ฆฌ์ŠคํŠธ๊ฐ€ ๊ณ„์† ์ถ”๊ฐ€๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์—ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

๋งค๋ฒˆ list์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ์ „ clearํ•˜๋„๋ก ์ฃ„๋‹ค clear๋ถ™์—ฌ์คฌ๋‹ค. ๊ทธ๋Ÿผ ์ดˆ๊ธฐํ™”ํ›„ ๋“ค์–ด๊ฐ€๋ฏ€๋กœ ๊ดœ์ฐฎ์•„์กŒ๋‹ค.

img


OKHTTP3๋กœ header ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ

okhttp์— interceptor๋กœ ์ค‘๊ฐ„์—์„œ ์ฒ˜๋ฆฌ๋ฅผํ•ด์„œ ์„œ๋ฒ„๋กœ ๋ณด๋‚ผ์ˆ˜์žˆ๋‹ค.

์šฐ์„  gradle์„ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ 

//okhttp3
    implementation 'com.squareup.okhttp3:okhttp:3.14.9'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'

๋‹ค์Œ์€ ์‹ค์ œ ๊ฐ์ฒด ๊ตฌํ˜„ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์—์„œ

okhttp ๊ฐ์ฒด๋„ ๊ตฌํ˜„ํ•ด์ฃผ๊ณ 

interceptor๋„ ๋งŒ๋“ค์–ด์„œ

retrofit ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๋•Œ client์—๋‹ค๊ฐ€ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด๋œ๋‹ค.

header์ถ”๊ฐ€์™ธ์—๋„ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์ด์žˆ๋‹ค ์ถ”ํ›„์— ์ฐพ์•„๋ด์•ผ๊ฒ ๋‹ค.

object ServiceCreator {

    private val headerInterceptor = Interceptor{
        val request = it.request()
            .newBuilder()
            .addHeader("Content-Type","application/json")
            .build()
        return@Interceptor it.proceed(request)
    }

    val client: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(headerInterceptor)
        .build()

    private const val  SOPT_BASE_URL= "https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/"

    private val SoptRetrofit :Retrofit = Retrofit.Builder()
        .baseUrl(SOPT_BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val signUpService :SignUpService = SoptRetrofit.create(SignUpService::class.java)
    val signInService :SignInService = SoptRetrofit.create(SignInService::class.java)

interceptor์— addHeader๋ฅผ ํ†ตํ•ด ํ—ค๋”๋ฅผ ๋‹ค๋Š”๋ถ€๋ถ„์„ ๋งŒ๋“ค์–ด์ฃผ๊ณ  ๋นŒ๋“œํ•œํ›„

OkHttpClient๋„ ์•„๊นŒ ๋งŒ๋“  interceptor๋ฅผ addInterceptor๋ฅผ ํ†ตํ•ด ์ถ”๊ฐ€ํ•ด์„œ buildํ•ด์ค€ํ›„

retrofit ๊ฐ์ฒด๋งŒ๋“ค๋•Œ client์— ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

์ฐธ๊ณ  ๋ธ”๋กœ๊ทธ

https://hwanine.github.io/android/Retrofit-OkHttp/

Android - Retrofit2, OkHttp๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋”์šฑ ๊ฐ„๊ฒฐํ•œ RestAPI ์—ฐ๋™ (Kotlin) (2)Retrofit๊ณผ OkHttp๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋”์šฑ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋„ค์ด๋ฒ„ RestAPI์™€์˜ ์—ฐ๋™ํ•˜๋Š” ๊ณผ์ •์„ ์†Œ๊ฐœํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.hwanine.github.io


2-3 Wrapper ํด๋ž˜์Šค

์•ž์ชฝ์— ๊ณ„์† ์ค‘๋ณต๋˜๋Š” ๋ถ€๋ถ„๋“ค ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ˆ˜์žˆ๋Š”๊ฐ€?

์–ด์งœํ”ผ ์—†์• ๋ ค๋Š” ๋ถ€๋ถ„์€ ๊ณ„์†ํ•ด์„œ ๊ฐ™์€๊ฒŒ ๋‚˜์˜ฌ๊ฒƒ์ด๋‹ค ๊ทธ๊ฑฐ ๋ฏธ๋ฆฌ wrapper class ๋ฅผ ํ†ตํ•ด์„œ ์ž‘์„ฑํ•ด๋†“๊ณ 

๋‚˜๋จธ์ง€ ๋‹ค๋ฅธ๋ถ€๋ถ„๋งŒ ์ƒˆ๋กœ ์ž‘์„ฑํ•ด์„œ ๋„ฃ์–ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

๊ณผ์ œ์—์„œ๋Š” signin ๋ถ€๋ถ„๋งŒ ์ ์šฉํ–ˆ๋‹ค.

img

์ด๊ฑธ ๊ณ ์น˜๋Š”๊ฒƒ์ด๋‹ค.

ResponseWrapper.kt

package changhwan.experiment.sopthomework

data class ResponseWrapper<T>(
    val status: Int,
    val success: Boolean,
    val message: String,
    val data: T?
)

์ด๋ ‡๊ฒŒ ๋ฏธ๋ฆฌ wrapper class๋กœ ์ค‘๋ณต๋œ๋ถ€๋ถ„์„ ์ž‘์„ฑํ•ด๋†“๋Š”๋‹ค.

ResponseSignInData.kt

package changhwan.experiment.sopthomework

import android.provider.ContactsContract

data class ResponseSignInData(
    val id: Int,
    val name: String,
    val email: String
)

๋‹ค์Œ ์ด๋ ‡๊ฒŒ ์˜ค๋Š” ์ •๋ณด๋“ค ์•ˆ์—๋‹ค ๋„ฃ์–ด์ค„๊บผ ์ž‘์„ฑํ•ด์„œ

์‚ฌ์šฉํ• ๋•Œ๋Š” ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ๋„ฃ์–ด์ค€๋‹ค.

SignInService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

interface SignInService {
    @POST("user/login")
    suspend fun postSignIn(
        @Body body: RequestSignInData
    ):Response<ResponseWrapper<ResponseSignInData>>
}
<ResponseWrapper<ResponseSignInData>>

์š”๋Ÿฐ์‹์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด๋‹ค.


3-3 coroutine

์ง„์งœ์ง„์งœ ๋ง›๋ฐฐ๊ธฐ๋กœ๋งŒํ•ด์„œ ๋ญ๊ฐ€ ๋งž๋Š”์ง€๋„ ์ž˜๋ชจ๋ฅธ๋‹ค. ๋‚˜์ค‘์— ์ถ”ํ›„ ๊ณต๋ถ€๋ฅผ ๋”ํ• ๊ฑฐ์ง€๋งŒ

์‹คํ–‰๋„ ์ž˜๋˜๊ธดํ•˜๊ณ  ์—ฌ๊ธฐ์ €๊ธฐ ๋ฌผ์–ด๋ณธ๊ฒฐ๊ณผ ๋งž๋Š”๋ฐฉ์‹์ธ๊ฑฐ๊ฐ™๋‹ค.

์‚ฌ์šฉ๋ฐฉ๋ฒ•์€ ์˜์™ธ๋กœ ๊ฐ„๋‹จํ•˜๋‹ค view model์„ ์จ์„œ viewmodelscope๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์žก๋‹คํ•œ๊ฑฐ ์•Œ์•„์„œ ๋˜์„œ ์‰ฌ์› ๋˜๊ฒƒ๋„ ์žˆ๋Š”๊ฑฐ๊ฐ™๋‹ค.

\1. gradle ์ถ”๊ฐ€

// ViewModel coroutine ์Šค์ฝ”ํ”„๋ฅผ ์œ„ํ•œ๊ฑฐ
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0"
     
//coroutine
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'

2.suspendํ‚ค์›Œ๋“œ ์‚ฌ์šฉํ•  ํ•จ์ˆ˜์— ๋‹ฌ์•„์ฃผ๊ธฐ + Call๊ฐ์ฒด๊ฐ€์•„๋‹Œ Response๊ฐ์ฒด ๊ฐ€์ ธ์˜ค๋Š”๊ฑธ๋กœ ๋ฐ”๊ฟ”์ฃผ๊ธฐ

SignInService.kt

package changhwan.experiment.sopthomework

import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST

interface SignInService {
    @POST("user/login")
    suspend fun postSignIn(
        @Body body: RequestSignInData
    ):Response<ResponseWrapper<ResponseSignInData>>
}

postSignIn ํ•จ์ˆ˜์— suspend ํ‚ค์›Œ๋“œ๋ฅผ ๋‹ฌ์•„์คฌ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Call๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๊ฒƒ์ด์•„๋‹Œ Response๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๊ฑธ๋กœ ๋ฐ”๊ฟ”์คฌ๋‹ค.

3.์ด์ œ callback์—์„œ ์ฒ˜๋ฆฌํ•˜๋˜๊ฑฐ ๋Œ€์ฒดํ•˜๊ธฐ

์›๋ž˜ callback ์œผ๋กœ ์ฐจ๋ฆฌํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ์ด๋žฌ๋‹ค.

 fun startSignIn() {
        val requestSignInData = RequestSignInData(
            email = _viewEmail.value!!,
            password = _viewPassword.value!!
        )

        val call : Call<ResponseSignInData> = ServiceCreator.signInService.postSignIn(requestSignInData)

        call.enqueue(object : Callback<ResponseSignInData>{
            override fun onResponse(
                call: Call<ResponseSignInData>,
                response: Response<ResponseSignInData>
            ) {
                val data = response.body()?.data

                if(response.isSuccessful){
                    if (data != null) {
                        _viewName.value = data.name
                    }
                    _conSuccess.value = true
                } else {
                    _conSuccess.value = false
                }
            }

            override fun onFailure(call: Call<ResponseSignInData>, t: Throwable) {
                _conSuccess.value = false
            }

        })
}

๊ทผ๋ฐ ์ด๊ฑฐ ์•กํ‹ฐ๋น„ํ‹ฐ๋ฉด ๊ธฐํƒ€๋“ฑ๋“ฑ ์‚ฌ์ „์ž‘์—…๋“ค์„ ํ•˜๊ณ  ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š”๋ฐ

๊ทธ๋ƒฅ viewmodel๋‚ด๋ผ์„œ viewModelScope๋ฅผ ์‚ฌ์šฉํ•ด์„œ

๊ทธ๋ƒฅ viewModelScope.launch{}๋‚ด๋ถ€์— ๋ถˆ๋Ÿฌ์˜ค๋Š”๊ฑฐ๋ž‘ ๊ทธ๋‹ค์Œ์— ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜จ๊ฑฐ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง ๋„ฃ์–ด์ฃผ๋ฉด ๋น„๋™๊ธฐ๋กœ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.

viewModelScope.launch {
            val response = ServiceCreator.signInService.postSignIn(requestSignInData)
            val data = response.body()?.data

            if (response.isSuccessful) {
                if (data != null) {
                    _viewName.value = data.name
                }
                _conSuccess.value = true
            } else {
                _conSuccess.value = false
            }
        }

์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋ฉด๋œ๋‹ค ์ฝ”๋“œ ๊ฐ„๊ฒฐํ•ด์ง„๋‹ค ์ง„์งœ.

๊ทธ๋ฆฌ๊ณ  response๋Š” ์•„๊นŒ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š”๊ฑฐ Response๋กœ ๋ฐ”๊ฟ”๋†จ์œผ๋‹ˆ ์ด์ œ ์ €๋ ‡๊ฒŒ ๋ฐ›์•„์˜ค๋ฉด Response๋ฅผ ๋ฐ›์•„์™€์ง„๋‹ค

๊ทธ๋ž˜์„œ ๊ทธ์ค‘์›ํ•˜๋Š”๊ฑฐ body๊ฐ’ ์— data ๋นผ์™€์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฉด๋œ๋‹ค.

์ฐธ๊ณ ๋ธ”๋กœ๊ทธ

https://enant.tistory.com/24

https://enant.tistory.com/23 <-๊ทผ๋ฐ ์ด๋ถ€๋ถ„์€ ์“ธ๋ชจ๊ฐ€์—†์—ˆ๋‹ค ์‚ฌ์‹ค ๊ทธ๋ƒฅ ์ด๋Ÿฐ๋А๋‚Œ๋งŒ ๊ฐ€์ง€๋Š”์šฉ๋„๋กœ ๋ณด๋ฉด๋œ๋‹ค.

https://developer88.tistory.com/214

7์ฃผ์ฐจ # ์‹คํ–‰ํ™”๋ฉด
bandicam.2021-12-17.19-20-51-312.mp4

์ฝ”๋“œ์„ค๋ช…

level1,2 ํ–ˆ๊ณ  3์€ ํ•˜๋‹ค๋ชปํ•จ

level1-1

OnboardingActivity.kt

package changhwan.experiment.sopthomework.ui.view.onboarding

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.navigation.Navigation.findNavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import changhwan.experiment.sopthomework.R
import changhwan.experiment.sopthomework.databinding.ActivityOnBoardingBinding
import org.koin.android.ext.android.bind

class OnBoardingActivity : AppCompatActivity() {



    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityOnBoardingBinding.inflate(layoutInflater)
        setContentView(binding.root)


        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.container_on_boarding) as NavHostFragment
        val navController = navHostFragment.navController
        val appBarConfiguration = AppBarConfiguration(
            topLevelDestinationIds = setOf(),
            fallbackOnNavigateUpListener = ::onSupportNavigateUp
        )
        findViewById<Toolbar>(R.id.tb_on_boarding)
            .setupWithNavController(navController, appBarConfiguration)
    }


}

OnboardingFirstFragment.kt

package changhwan.experiment.sopthomework.ui.view.onboarding.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.navigation.fragment.findNavController
import changhwan.experiment.sopthomework.R
import changhwan.experiment.sopthomework.databinding.FragmentOnBoardingFirstBinding
import org.koin.android.ext.android.bind


class OnBoardingFirstFragment : Fragment() {

    private var _binding: FragmentOnBoardingFirstBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentOnBoardingFirstBinding.inflate(inflater, container, false)


        binding.btnOnBoardFirst.setOnClickListener{
            findNavController().navigate(R.id.action_onBoardingFirstFragment_to_onBoardingSecondFragment)
        }

        return binding.root
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

ํ”„๋ž˜๊ทธ๋จผํŠธ๋Š” ์˜ˆ์‹œ๋กœ ํ•˜๋‚˜๋งŒ ์˜ฌ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

<?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_on_boarding"
    app:startDestination="@id/onBoardingFirstFragment">

    <fragment
        android:id="@+id/onBoardingFirstFragment"
        android:name="changhwan.experiment.sopthomework.ui.view.onboarding.fragment.OnBoardingFirstFragment"
        android:label="์ฒซ๋ฒˆ์จฐ"
        tools:layout="@layout/fragment_on_boarding_first" >
        <action
            android:id="@+id/action_onBoardingFirstFragment_to_onBoardingSecondFragment"
            app:destination="@id/onBoardingSecondFragment" />
    </fragment>
    <fragment
        android:id="@+id/onBoardingSecondFragment"
        android:name="changhwan.experiment.sopthomework.ui.view.onboarding.fragment.OnBoardingSecondFragment"
        android:label="๋‘๋ฒˆ์จฐ"
        tools:layout="@layout/fragment_on_boarding_second" >
        <action
            android:id="@+id/action_onBoardingSecondFragment_to_onBoardingThirdFragment"
            app:destination="@id/onBoardingThirdFragment" />
    </fragment>
    <fragment
        android:id="@+id/onBoardingThirdFragment"
        android:name="changhwan.experiment.sopthomework.ui.view.onboarding.fragment.OnBoardingThirdFragment"
        android:label="์„ธ๋ฒˆ์งธ"
        tools:layout="@layout/fragment_on_boarding_third" >
        <action
            android:id="@+id/action_pop_onBoardingThirdFragment_to_onBoardingFirstFragment"
            app:destination="@id/onBoardingFirstFragment"
            app:popUpTo="@id/onBoardingFirstFragment"
            app:popUpToInclusive="true"/>
    </fragment>

</navigation>

๋„ค๋น„๊ฒŒ์ด์…˜์— ๋ณด๋ฉด level 2์— ๋ฐฑ์Šคํƒ ๊ฐ€๋Š”๊ฒƒ๊นŒ์ง€ ๋„ฃ์–ด๋†จ์Šต๋‹ˆ๋‹ค.

level1-2

MainActivity.kt

package changhwan.experiment.sopthomework

import android.app.Application
import changhwan.experiment.sopthomework.data.remote.api.SignInService
import changhwan.experiment.sopthomework.data.remote.api.SignUpService
import changhwan.experiment.sopthomework.di.HeaderInterceptor
import changhwan.experiment.sopthomework.ui.viewmodel.SignViewModel
import changhwan.experiment.sopthomework.util.PreferenceUtil
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainApplication : Application() {

    companion object{
        lateinit var  prefs: PreferenceUtil
    }

    override fun onCreate() {
        super.onCreate()

        //shared preferences
        prefs = PreferenceUtil(applicationContext)

        startKoin {
            androidContext(this@MainApplication)
            modules(soptNetworkModule,viewModelModule)
        }
    }
}

val soptNetworkModule = module {
    single {
        OkHttpClient.Builder()
            .addInterceptor(HeaderInterceptor())
            .build()
    }
    single {
        GsonConverterFactory.create() as Converter.Factory
    }

    single<Retrofit> {
        Retrofit.Builder()
            .client(get())
            .addConverterFactory(get())
            .baseUrl("https://asia-northeast3-we-sopt-29.cloudfunctions.net/api/")
            .build()
    }

    single<SignUpService> {
        get<Retrofit>().create(SignUpService::class.java)
    }

    single<SignInService> {
        get<Retrofit>().create(SignInService::class.java)
    }
}

val viewModelModule = module {
    viewModel {
        SignViewModel(get(),get())
    }
}

์—ฌ๊ธฐ์— shared preference ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

PreferenceUtill.kt

package changhwan.experiment.sopthomework.util

import android.content.Context
import android.content.SharedPreferences

class PreferenceUtil(context: Context) {

    private val prefs: SharedPreferences =
        context.getSharedPreferences("main_prefs",Context.MODE_PRIVATE)


    fun getBoolean (key:String,defvalue:Boolean): Boolean{
        return prefs.getBoolean(key,defvalue)
    }

    fun setBoolean (key:String, value:Boolean){
        return prefs.edit().putBoolean(key,value).apply()
    }
}

์‚ฌ์šฉํ•œ์˜ˆ์‹œ

SigninActivity์—์„œ sharedpreference๋ฅผ ํ†ตํ•ด ์ž๋™๋กœ๊ทธ์ธ ์—ฌ๋ถ€ ์ €์žฅ

private fun startLogin() {
        binding.loginButton.setOnClickListener {
            signInViewModel.getEmail("")
            signInViewModel.getPassword("")
            signInEmail.value?.let { signInViewModel.getEmail(it) }
            signInPassword.value?.let { signInViewModel.getPassword(it) }
            signInViewModel.startSignIn()
            if(binding.cbAutoLogin.isChecked){
                MainApplication.prefs.setBoolean("auto_login",true)
//                val db = SoptDatabase.getInstance(applicationContext)
//                CoroutineScope(Dispatchers.IO).launch {
//                    db!!.soptDao().insert(SoptEntity(autoLogin = true))
//                }
            }
        }
    }

SettingFragment.kt๋ฅผ ๋งŒ๋“ค์–ด ํ™˜๊ฒฝ์„ค์ • ์ฐฝ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

package changhwan.experiment.sopthomework.ui.view.profile.autologin

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import changhwan.experiment.sopthomework.MainApplication
import changhwan.experiment.sopthomework.R
import changhwan.experiment.sopthomework.databinding.FragmentSettingBinding
import changhwan.experiment.sopthomework.ui.view.profile.follower.FollowerFragment


class SettingFragment : Fragment() {

    private var _binding: FragmentSettingBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentSettingBinding.inflate(layoutInflater,container,false)

        updataAutoLoginState()
        backSetting()
        actionAutoLoginStateChange()
        return binding.root
    }

    private fun updataAutoLoginState(){
        binding.cbAutoLoginState.isChecked = MainApplication.prefs.getBoolean("auto_login",false)
    }

    private fun actionAutoLoginStateChange(){
        binding.cbAutoLoginState.setOnCheckedChangeListener { buttonView, isChecked ->
            if(isChecked){
                MainApplication.prefs.setBoolean("auto_login",true)
            }else{
                MainApplication.prefs.setBoolean("auto_login",false)
            }
        }
    }

    private fun backSetting(){
        binding.btnBack.setOnClickListener{
            binding.btnBack.setOnClickListener {
                val followerFragment = FollowerFragment()
                requireParentFragment().childFragmentManager.beginTransaction()
                    .replace(R.id.fragmentFrame,followerFragment ).commit()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

๊ณผ์ œ 1-3

Mascota ์ฐธ๊ณ 

1-3 ์ „๋žต ์ถ”๊ฐ€

image-20211224144301994

์šฐ์„  ๊ฐ€์žฅ ์ƒ์œ„์— data,di,ui,utill๋กœ ๋‚˜๋ˆ„๊ณ 

data์—๋Š”

local๊ณผ remote๋กœ ๋‚˜๋ˆ ์„œ

local์€ ๊ทธ์•ผ๋ง๋กœ ๋กœ์ปฌ room๊ฐ™์€๊ฑฐ

remote์—๋Š” retrofit๊ฐ™์€๊ฑฐ ๋ชฐ์•„๋„ฃ์—ˆ๋‹ค

di์—๋Š” ์˜์กด์„ฑ ์ฃผ์ž…์— ์‚ฌ์šฉ๋˜๋Š”

์ธํ„ฐ์…‰ํ„ฐ๋ผ๋“ ์ง€ ๋ชจ๋“ˆ๋„ ๋ถ„๋ฆฌํ•ด์„œ ๋„ฃ์–ด์•ผํ•˜๋Š”๋ฐ ๋ชจ๋“ˆ์€ ๋ณต์žกํ•ด์„œ ๋ถ„๋ฆฌ๋ฅผ ๋ชปํ–ˆ๋‹ค ใ… 

ui

uiํ•˜์œ„์—๋Š” view,viewmodel์ด์žˆ๋Š”๋ฐ ๋„ค์ด๋ฐ๊ณผ๊ฐ™์ด ์ ์ ˆํ•œ๊ฒƒ ๋„ฃ์–ด์ฃผ๊ณ 

view์—๋Š”

๊ธฐ๋Šฅ ๋ณ„๋กœ ๋‚˜๋ˆด์œผ๋ฉฐ ์–ด๋Œ‘ํ„ฐ ๊ฐ™์€๊ฒƒ๋„ ๋“ค์–ด๊ฐ„๋‹ค

๋งˆ์ง๋ง‰์œผ๋กœutill์—๋Š”

diffutill๊ฐ™์€ ์ „์—ญ์—์„œ ์“ฐ์ด๋Š”utill๋“ค์„ ๋ชฐ์•„๋†จ๋‹ค

2-1

์œ„์ชฝ์—์„œ ๋„ค๋น„๊ฒŒ์ด์…˜์€ ์ ํ˜€์žˆ๊ณ 

OnBoardingThiedFragmnet.kt์—์„œ

private fun setBackButton(){
    requireActivity().onBackPressedDispatcher.addCallback(this){
        findNavController().navigate(R.id.action_pop_onBoardingThirdFragment_to_onBoardingFirstFragment)
    }
}

back๋ฒ„ํŠผ ๋ˆŒ๋ ธ์„๋•Œ ์‹คํ–‰๋˜๋Š”๋ถ€๋ถ„ ๋งŒ๋“ค์–ด๋†“์€๊ฒƒ

2-2

OnBoardingActivity.kt

package changhwan.experiment.sopthomework.ui.view.onboarding

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.navigation.Navigation.findNavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import changhwan.experiment.sopthomework.R
import changhwan.experiment.sopthomework.databinding.ActivityOnBoardingBinding
import org.koin.android.ext.android.bind

class OnBoardingActivity : AppCompatActivity() {



    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityOnBoardingBinding.inflate(layoutInflater)
        setContentView(binding.root)


        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.container_on_boarding) as NavHostFragment
        val navController = navHostFragment.navController
        val appBarConfiguration = AppBarConfiguration(
            topLevelDestinationIds = setOf(),
            fallbackOnNavigateUpListener = ::onSupportNavigateUp
        )
        findViewById<Toolbar>(R.id.tb_on_boarding)
            .setupWithNavController(navController, appBarConfiguration)
    }


}

์—ฌ๊ธฐ์— toolbar์™€ ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ ์ž‘์„ฑ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages