[WIL] 2024๋…„ 5์›” ์…‹์งธ์ฃผ (์‹ฌํ™” ํŒ€ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ ๋ฐ ํ•„์ˆ˜ ๊ตฌํ˜„ ์™„๋ฃŒ : Youtube Data API, MVVM, SearchFragment)
๋ฐ˜์‘ํ˜•



 

 

๐Ÿ“… 24๋…„ 5์›” 13์ผ ~ 5์›” 19์ผ

  ์›”์š”์ผ
ํ™”์š”์ผ
์ˆ˜์š”์ผ ๋ชฉ์š”์ผ ๊ธˆ์š”์ผ ๋ฐ ์ฃผ๋ง
์‹ฌํ™” ๊ฐ•์˜ ๋‚ด์šฉ
๋ธ”๋กœ๊ทธ์— ์ •๋ฆฌ
  โ–  Retrofit
โ–  ๊ฐœ๋ฐœํ”„๋กœ์„ธ์Šค
โ–  ๋””๋ฒ„๊น…
โ–  ๋ฏธ์„ธ๋จผ์ง€์•ฑ
  โ–ก ๋ฏธ์„ธ๋จผ์ง€์•ฑ  
๊ณผ์ œ ํ•ด์„ค   โ–ก ๊ณผ์ œ ํ•ด์„ค ์ˆ˜๊ฐ•      
ํŒ€ ํ”„๋กœ์ ํŠธ โ–  S.A. ์ž‘์„ฑ
โ–  Figma
   ์™€์ด์–ดํ”„๋ ˆ์ž„
โ–  ๊ธฐ๋ณธ ํ‹€ ๊ตฌํ˜„   โ–  SearchFragment
   ํ•„์ˆ˜ ๊ธฐ๋Šฅ ๊ตฌํ˜„
โ–  ํŒ€ ํ•„์ˆ˜ ๊ตฌํ˜„ ๋ชจ๋‘ ์™„๋ฃŒ ํ›„ Merge
๋ฒ ์ด์ง๋ฐ˜ ๊ฐ•์˜
    โ–  ์ด์ „ ๊ฐ•์˜ ์ˆ˜๊ฐ•
   (1/2/3๊ฐ•)
โ–  ๋ฒ ์ด์ง๋ฐ˜ 9ํšŒ์ฐจ
   (Retrofit)
 
Joyce ์„œ์  ๋…ํ•™   โ–ก TodoList ์ œ์ž‘
   (Room)
โ–ก ๋ฏธ์„ธ๋จผ์ง€ V1.0 โ–ก ๋ฏธ์„ธ๋จผ์ง€ V2.0 โ–ก ๋ฏธ์„ธ๋จผ์ง€ V3.0
์‹ฌํ™” ๋ชฉํ‘œ
 - KIA ๊ฐœ๋… ํ›‘๊ธฐ
 - Android Developer ์ฝ๊ธฐ
 - ๊ฐœ์ธํ”„๋กœ์ ํŠธ UI ๊ตฌํ˜„
 - ์ง€๋‚œ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ ๋œฏ์–ด๋ณด๊ธฐ
 - ๋งํฌ ์ˆ˜๊ฐ•
 - ์บ ํ”„ ๊ณต์‹ ๊ต์œก์ด ๋๋‚˜๊ณ  ๋‚˜๋ฉด, ์ฑŒ๋ฆฐ์ง€/์Šคํƒ ๋‹ค๋“œ ์ฐจ๋ก€๋กœ ์ˆ˜๊ฐ•ํ•˜๊ธฐ
 - ์ •์ฐฝ๊ฒฝ ์ •๋ฆฌ

 

 

 

 


 

 

1. ์•ˆ๋“œ๋กœ์ด๋“œ ์‹ฌํ™” ๊ฐ•์˜ ๋‚ด์šฉ ์ •๋ฆฌ

 

 

 

[Android ๊ธฐ์ดˆ] 15. SharedPreferences

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ์˜ ์˜๊ตฌ(๋น„ํœ˜๋ฐœ์„ฑ) ๋ฐ์ดํ„ฐ ์ €์žฅ๋ฒ•SharedPreference (์„ค์ • ์ •๋ณด ์ €์žฅํ•  ๋•Œ ๋งŽ์ด ์‚ฌ์šฉ)๋ฐ์ดํ„ฐ๋ฒ ์ด์ŠคํŒŒ์ผ      1. Preference๋ž€? SharedPreferences๋กœ ๋‹จ์ˆœ ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ  |  Android DevelopersD

limheejin.tistory.com

 

[Android ๊ธฐ์ดˆ] 16. Room (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ์˜ ์˜๊ตฌ(๋น„ํœ˜๋ฐœ์„ฑ) ๋ฐ์ดํ„ฐ ์ €์žฅ๋ฒ•SharedPreference๋ฐ์ดํ„ฐ๋ฒ ์ด์ŠคํŒŒ์ผ   1) Room ๊ฐœ์š” ๋ฐฉ  |  Jetpack  |  Android Developers์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ปฌ๋ ‰์…˜์„

limheejin.tistory.com

 

[Android ๊ธฐ์ดˆ] 17. ์‚ฌ์šฉ์ž ์œ„์น˜ ์–ป๊ธฐ

์œ„์น˜ ์ธ์‹ ์•ฑ ๋นŒ๋“œ  |  Sensors and location  |  Android Developers์œ„์น˜ ์ธ์‹ ์•ฑ ๋นŒ๋“œ ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ณ ์œ ํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋Š” ์œ„์น˜ ์ธ์‹ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋ชจ๋ฐ”์ผ ์‚ฌ์šฉ์ž๋Š” ์–ด๋””์—๋‚˜ ๊ธฐ๊ธฐ๋ฅผ ํœด๋Œ€ํ•˜๊ธฐ

limheejin.tistory.com

 

[Android ๊ธฐ์ดˆ] 18. ๊ตฌ๊ธ€ ์ง€๋„์•ฑ ์ œ์ž‘

1. ์ง€๋„ ์‚ฌ์šฉ ์„ค์ •ํ•˜๊ธฐimplementation 'com.google.android.gms:play-services-maps:18.1.0'implementation 'com.google.android.gms:play-services-location:21.0.1'Gradle์˜ dependencies ํ•ญ๋ชฉ ์„ค์ • Manifest ํŒŒ์ผ์— permission ์„ค์ • ๊ตฌ๊ธ€ ์ง€๋„ API

limheejin.tistory.com

 

[Android ๊ธฐ์ดˆ] 19. Retrofit (HTTP, API, REST, JSON, GSON)

Retrofit์„ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด ์•Œ์•„์•ผ ํ•˜๋Š” ๊ฐœ๋…์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธHTTPAPIRESTJSONGSON   1. ์„œ๋ฒ„? ํด๋ผ์ด์–ธํŠธ? ์„œ๋ฒ„ (Server)- ๋ฐ์ดํ„ฐ๋‚˜ ๋ฆฌ์†Œ์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ์‹œ์Šคํ…œ- ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ , ์š”์ฒญ์ด ๋“ค์–ด์˜ค

limheejin.tistory.com

 

[Android ๊ธฐ์ดˆ] 20(๋). ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ๋””๋ฒ„๊น…

๋””๋ฒ„๊น…https://developer.android.com/studio/debug?hl=ko ์•ฑ ๋””๋ฒ„๊ทธ  |  Android Studio  |  Android DevelopersAndroid ์ŠคํŠœ๋””์˜ค์˜ ๊ธฐ๋ณธ์ ์ธ ๋””๋ฒ„๊ฑฐ ์ž‘์—…์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.developer.android.com๋ชจ๋“  ์†Œํ”„ํŠธ์›จ์–ด์—์„œ ์†Œ์Šค ์ฝ”๋“œ

limheejin.tistory.com

 

  • ์ €๋ฒˆ์ฃผ์— ์ˆ˜๊ฐ• ํ›„ ์ •๋ฆฌํ•˜์ง€ ๋ชปํ–ˆ๋˜ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๊ณ , ๋ณต์Šตํ–ˆ๋‹ค.
  • ์ด์ œ ์ˆ˜๊ฐ•์ด ๋๋‚ฌ๊ณ , Android์˜ ๊ธฐ์ดˆ์ ์ธ ๋‚ด์šฉ์€ ์ „์ฒด์ ์œผ๋กœ ๋งŽ์ด ํ›‘์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ž์œผ๋กœ์˜ ํฌ์ŠคํŒ…์€ ๊ฐœ์ธ์ ์œผ๋กœ ๊ณต๋ถ€ํ•˜๋Š” ํ…Œ๋งˆ/๊ธฐ์ˆ ์„ ๋‚ฑ๊ฐœ์˜ ํฌ์ŠคํŒ…์œผ๋กœ ์ •๋ฆฌํ•  ์˜ˆ์ •์ด๋‹ค.

 

https://roadmap.sh/android

 

 


 

2. Joyce ์•ˆ๋“œ๋กœ์ด๋“œ ์„œ์  ๋…ํ•™ ๋ฐ ํ”„๋กœ์ ํŠธ ์‹ค์Šต

 

1~9. ์ด์ „์— ๊ณต๋ถ€ํ–ˆ๋˜ ๋‚ด์šฉ : ์ด์ „ TIL์— ์ •๋ฆฌ

 

[Android TIL] 240415 (Joyce ์„œ์  ๊ณต๋ถ€, ์Šคํƒ ๋‹ค๋“œ๋ฐ˜ 4์ฃผ์ฐจ ๊ณผ์ œ UI ๊ตฌํ˜„, ๋ฒ ์ด์ง ๋ฐ ์Šคํƒ ๋‹ค๋“œ๋ฐ˜ 3·4์ฃผ์ฐจ

๐ŸŒฑ Today I Learned (์ง‘์ค‘์‹œ๊ฐ„ : 9์‹œ๊ฐ„ 5๋ถ„) (09:25 ~ 11:00 / 1์‹œ๊ฐ„ 30๋ถ„) ๋ฐ์ผ๋ฆฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ’€์ด ๋ฐ ํŒ€ ์Šคํฌ๋Ÿผ (11:00 ~ 12:10 / 1์‹œ๊ฐ„ 10๋ถ„) ๋ฒ ์ด์ง๋ฐ˜ ๊ฐ•์˜ 3์ฃผ์ฐจ, 4์ฃผ์ฐจ ์ˆ˜๊ฐ• (12:10 ~ 13:00 / 50๋ถ„) ์Šคํƒ ๋‹ค๋“œ๋ฐ˜ ๊ฐ•์˜ 3

limheejin.tistory.com

 

[WIL] 2024๋…„ 5์›” ๋‘˜์งธ์ฃผ (์‹ฌํ™” ๊ฐ•์˜ ์ˆ˜๊ฐ•, ๊ฐœ์ธ ๊ณผ์ œ ๊ตฌํ˜„์ค‘, ์Šคํ†ฑ์›Œ์น˜, ๋ฎค์งํ”Œ๋ ˆ์ด์–ด, QR๋ฆฌ๋”๊ธฐ)

๐Ÿ“… 24๋…„ 5์›” 6์ผ ~ 5์›” 12์ผ ์›”์š”์ผํ™”์š”์ผ์ˆ˜์š”์ผ๋ชฉ์š”์ผ๊ธˆ์š”์ผ ๋ฐ ์ฃผ๋ง์‹ฌํ™” ๊ฐ•์˜ ์ˆ˜๊ฐ•โ–  Shared  Preferenceโ–  Roomโ–  ์‚ฌ์šฉ์ž์œ„์น˜โ–  ๊ตฌ๊ธ€์ง€๋„์•ฑโ–  Retrofitโ–  ๊ฐœ๋ฐœํ”„๋กœ์„ธ์Šคโ–  ๋””๋ฒ„๊น… โ–ก ๋ฏธ์„ธ๋จผ์ง€์•ฑโ–ก ๋ฏธ

limheejin.tistory.com

 

10. Todo List ํ”„๋กœ์ ํŠธ ์ œ์ž‘

  • Room, RecyclerView๋ฅผ ๋ณต์Šตํ•˜๊ธฐ ์œ„ํ•จ

 

 


 

3. ํŒ€ํ”„๋กœ์ ํŠธ : Youtube Data API - SearchFragment, MVVM ๋“ฑ ํ•„์ˆ˜ ์™„๋ฃŒ

 

๋งก์€ ๋ถ€๋ถ„ ์•ˆ์—์„œ์˜ ํ•„์ˆ˜ ๊ตฌํ˜„์„ ๋ชจ๋‘ ์™„๋ฃŒํ–ˆ๋‹ค.

 

1. Youtube Data API๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ถœ๋ ฅ

 

Search  |  YouTube Data API  |  Google for Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Search ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. ๊ฒ€์ƒ‰๊ฒฐ๊ณผ์—๋Š” API ์š”์ฒญ์— ์ง€์ •๋œ ๊ฒ€์ƒ‰ ๋งค๊ฐœ๋ณ€์ˆ˜์™€

developers.google.com

  • ์šฐ์„ , ํ•ด๋‹น API๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒ€์ƒ‰ ํ™”๋ฉด์„ ๋งŒ๋“ค ๊ฒƒ์ด๋‹ค.
  • ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์€ 'Search' ์—”๋“œํฌ์ธํŠธ์—์„œ ๋ฐ˜ํ™˜์„ ์š”์ฒญํ•˜๋Š” ๋น„์šฉ์ด ์ƒ๊ฐ ์ด์ƒ์œผ๋กœ ๋น„์‹ธ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
    ์ผ๋ฐ˜์ ์ธ ๋Œ“๊ธ€, ์žฌ์ƒ๋ชฉ๋ก, ์ฑ„๋„๋ช… ์ถœ๋ ฅ์€ 1์˜ ๋น„์šฉ์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ๊ฒ€์ƒ‰์€ ํ•œ ๋ฒˆ์˜ ์š”์ฒญ๋งˆ๋‹ค ๋ฌด๋ ค 100์„ ์‚ฌ์šฉํ•œ๋‹ค.
    ๋ฌด๋ฃŒ ๋ฒ„์ „์˜ ๊ฒฝ์šฐ ํ•˜๋ฃจ 10,000์˜ ํ• ๋‹น๋Ÿ‰์„ ์ฑ„์šฐ๋ฉด ๋‹ค์Œ๋‚  ์˜คํ›„ 4์‹œ๊นŒ์ง€ HTTP 403 ์˜ค๋ฅ˜๊ฐ€ ๋œฌ๋‹ค. 
  • ์•„์ง ํŒ€ํ”„๋กœ์ ํŠธ ๋ฐœํ‘œ๊ฐ€ ๋๋‚œ ๊ฒƒ์ด ์•„๋‹ˆ๋ฏ€๋กœ, ์ž์„ธํ•œ ์ฝ”๋“œ(ํŠนํžˆ ๋„คํŠธ์›Œํฌ)๋Š” ์ฐจํ›„์— ์ •๋ฆฌํ•˜๊ฒ ๋‹ค.

 

๐Ÿ’ก ๊ณ ๋ฏผ์ : ์–ด๋–ค ๋ฐฉ๋ฒ•์œผ๋กœ ๊ฒ€์ƒ‰์„ ์š”์ฒญํ•  ๊ฒƒ์ธ๊ฐ€?

    private fun setupSearch() {

                // (1) ๊ฒ€์ƒ‰์–ด๊ฐ€ ๊ฐฑ์‹ ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™ ๊ฒ€์ƒ‰
        binding.etSearch.addTextChangedListener { editable ->
            val query = editable.toString()
            if (query.isNotEmpty()) {
                viewModel.searchVideos(query)
            }
        }

        // (2) ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์—†์ด ํ‚ค๋ณด๋“œ์˜ ์—”ํ„ฐ๋กœ ์ž‘๋™ (imeOptions="actionSearch", inputType="text" ๋กœ ์„ค์ • ํ›„ ๊ตฌํ˜„)
        binding.etSearch.setOnEditorActionListener { v, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH ||
                event?.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_ENTER
            ) {
                val query = binding.etSearch.text.toString()
                if (query.isNotEmpty()) {
                    viewModel.searchVideos(query)
                } else {
                    Toast.makeText(requireContext(), "๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", Toast.LENGTH_SHORT).show()
                }
                true
            } else false
        }

        // (3) ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ(btnSearch)์„ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด ์•„๋ž˜ ์ฝ”๋“œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
        binding.btnSearch.setOnClickListener {
            val query = binding.etSearch.text.toString()
            if (query.isNotEmpty()){
                viewModel.searchVideos(query)
            } else {
                Toast.makeText(requireContext(), "๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", Toast.LENGTH_SHORT).show()
            }
        }

    }
  • ๊ธฐ๋Šฅ์„ ๋‹ค ๊ตฌํ˜„ํ•ด๋†“๊ณ , ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ์žˆ์—ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ์ข…๋ฅ˜๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.
    (1) (๊ฒ€์ƒ‰ ๋ฒ„ํŠผX) ํ…์ŠคํŠธ๊ฐ€ ์ž…๋ ฅ๋˜๊ณ  ์•ฝ๊ฐ„์˜ ํ…€์ด ์žˆ์„ ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๊ฒ€์ƒ‰
    (2) (๊ฒ€์ƒ‰ ๋ฒ„ํŠผX) ํ…์ŠคํŠธ๋ฅผ ์›ํ•˜๋Š” ๋งŒํผ ๋‹ค ์ž…๋ ฅํ•˜๊ณ , ์—”ํ„ฐ๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ๋งŒ ๊ฒ€์ƒ‰
    (3) (๊ฒ€์ƒ‰ ๋ฒ„ํŠผO) ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๊ตฌํ˜„ํ•ด๋‘๊ณ  ๊ทธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ๊ฒ€์ƒ‰
  • ์ฒ˜์Œ์—” (1)์ด ๊ดœ์ฐฎ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์œผ๋‚˜, ๊ฒ€์ƒ‰์„ ๋„ˆ๋ฌด ์ž์ฃผ ๊ฐฑ์‹ ํ•ด์„œ API ํ• ๋‹น๋Ÿ‰์„ ์ดˆ๊ณผํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.
  • ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด๋‘๋Š”๊ฒŒ ์‹ฌ๋ฏธ์ ์œผ๋กœ ๋ณ„๋กœ๋ผ์„œ 2๋ฒˆ ์ฝ”๋“œ๋งŒ ๋‚จ๊ฒจ๋‘์—ˆ๋‹ค.

 

 

  • ์œ„์™€ ๊ฐ™์ด SearchFragment.kt๊ฐ€ ์™„์„ฑ๋˜์—ˆ๋‹ค.
    ์ฃผ์–ด์ง„ ๊ฒ€์ƒ‰์–ด์— ๋”ฐ๋ผ์„œ Youtube Data API์˜ search ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ด์šฉํ•ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค.
  • MVVM์œผ๋กœ ๋ฐ”๊พธ๊ธฐ ์ด์ „๊นŒ์ง€์˜ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    model
      ใ„ด database
        ใ„ด ...
      ใ„ด SearchData.kt
    presentation
      ใ„ด activity
      ใ„ด adapter
        ใ„ด RVSearchAdapter.kt
      ใ„ด fragment
        ใ„ด SearchFragment.kt
    network
      ใ„ด ChannelsInterface.kt
      ใ„ด NetworkClient.kt
      ใ„ด SearchInterface.kt
    ์œ„์—์„œ ๋งํ–ˆ๋“ฏ ์ž์„ธํ•œ ์ฝ”๋“œ๋Š” ๋‚˜์ค‘์— ์ •๋ฆฌํ•  ๊ฒƒ์ด๋‹ค.

 

 

 

 

2. MVC -> MVVM์œผ๋กœ ๋ณ€๊ฒฝ

์ด์ œ ํ”„๋กœ์ ํŠธ ๊ธฐํš ์ดˆ๊ธฐ์— ์ƒ๊ฐํ–ˆ๋˜ MVVM ํŒจํ„ด์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์˜€๋‹ค. ํŒ€์›๋ถ„์˜ ์„ค๋ช…์œผ๋กœ ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋‹ˆ, MVC ๊ตฌ์กฐ๋ฅผ MVVM ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์€ ๋‹จ๊ณ„๋ณ„๋กœ ์ˆ˜ํ–‰ํ•˜๋ฉด ํฌ๊ฒŒ ์–ด๋ ต์ง€ ์•Š๋‹ค๊ณ  ๋Š๊ผˆ๋‹ค.
MVVM ๊ตฌ์กฐ๋Š” ๋ฐ์ดํ„ฐ์™€ UI๋ฅผ ๋ถ„๋ฆฌํ•ด ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ๋†’์ด๋ฉฐ, ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ LiveData์™€ ViewModel์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

(1) SearchViewModel : LiveData๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ Fragment์— ์ „๋‹ฌ, ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
(2) SearchFragment : UI๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ViewModel์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€์ฐฐํ•˜์—ฌ UI ์—…๋ฐ์ดํŠธ
(3) RVSearchAdapter : ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ๋กœ ๋ณด์—ฌ์คŒ

 

 

1. SearchFragment.kt ๋ณ€๊ฒฝ

  • UI์™€ ์ง์ ‘์ ์œผ๋กœ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์ฝ”๋“œ๋Š” SearchFragment์— ๋‘๊ณ ,
    ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๋ทฐ๋ชจ๋ธ(SearchViewModel.kt)๋กœ ์˜ฎ๊ธด๋‹ค.
  • LiveData๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค.

 

class SearchFragment : Fragment() {
	..
    // ๋ทฐ๋ชจ๋ธ ์ƒ์„ฑ
    private val viewModel by viewModels<SearchViewModel> {
        SearchVideoViewModelFactory()
    }
    ..
  • SearchFragment ์ƒ๋‹จ์— ์œ„์™€ ๊ฐ™์ด ๋ทฐ๋ชจ๋ธ์„ ์…‹ํŒ…ํ•ด๋‘”๋‹ค.

 

    private fun searchVideos(query: String) {
        lifecycleScope.launch {
            try {
                val searchItems = getSearchResults(query)
                searchAdapter.setItems(searchItems)
            } catch (e: Exception) {
                handleException(e) // ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋งŒ๋“ฆ
            }
        }
    }

    private suspend fun getSearchResults(query: String): List<SearchItems> {
        return withContext(Dispatchers.IO) {
            try {
                val response = NetworkClient.youtubeApiSearch.getSearchList(
                    key = NetworkClient.AUTH_KEY,
                    part = "snippet",
                    safeSearch = "strict",
                    type = "video",
                    maxResults = 1, // ๋ฐ์ดํ„ฐ ์•„๋ผ๊ธฐ ์œ„ํ•ด ์ผ๋‹จ 3๊ฐœ
                    query = query,
                    videoCategoryId = "15" // Pets & Animals
                )
                response.items
            } catch (e: Exception) {
                e.printStackTrace()
                emptyList()
            }
        }
    }
    
     private fun handleException(exception: Exception) {
        val errorMessage = when (exception) {
            is IOException -> "IO Exception ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
            is HttpException -> {
                when (exception.code()) {
                    400 -> "Bad Request ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    401 -> "Unauthorized ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    404 -> "not found ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
                }
            }

            else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
        }
        Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
    }
  • ๊ธฐ์กด์— SearchFragment์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ ํ•จ์ˆ˜ ์ค‘ ์œ„์˜ ๋ถ€๋ถ„๋“ค์€ ๋ชจ๋‘ ์‚ญ์ œํ•œ๋‹ค.
  • searchVideos : ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ViewModel๋กœ ์ด๋™ํ•˜๋ฉด, ViewModel์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๊ณ  LiveData ๋ฅผ ํ†ตํ•ด UI์— ์ „๋‹ฌ
  • getSearchResults : ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ViewModel๋กœ ์ด๋™ํ•˜๋ฉด, ViewModel์—์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • handleException : ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ViewModel๋กœ ์ด๋™

 

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

		...
        setupObservers()
		...
    }
    
        private fun setupObservers() {
        viewModel.getSearchData.observe(viewLifecycleOwner) { searchItems ->
            searchAdapter.setItems(searchItems)
        }
        viewModel.errorMessage.observe(viewLifecycleOwner) { message ->
            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
        }
    }
  • ์˜ต์ €๋น™์„ ์œ„ํ•œ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค. ์œ„์˜ searchVideos์™€ ๊ฐ™์€ ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ด์œ ๋‹ค.

 

  • searchVideos ๋กœ์ง์ด ํ”„๋ž˜๊ทธ๋จผํŠธ์—์„œ ์‚ญ์ œ๋˜๊ณ  ๋ทฐ๋ชจ๋ธ๋กœ ์˜ฎ๊ฒจ๊ฐ”์œผ๋ฏ€๋กœ, ์œ„์™€ ๊ฐ™์ด ์ฝ”๋“œ๋“ค์„ ์ ์ ˆํžˆ ์ˆ˜์ •ํ•ด์ค€๋‹ค.

 

 

 

2. SearchViewModel.kt ๋ณ€๊ฒฝ

  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ViewModel์— ๋„ฃ์–ด ๊ด€๋ฆฌํ•œ๋‹ค.
  • ์—ญ์‹œ LiveData๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ์™€ UI๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

 

package com.limheejin.kidstopia.viewmodel

import ...

class SearchViewModel(private val repository: Repository): ViewModel() {
    private val _getSearchData: MutableLiveData<MutableList<SearchItems>> = MutableLiveData()
    val getSearchData: LiveData<MutableList<SearchItems>> get() =_getSearchData
    fun getSearchData(query: String) = viewModelScope.launch {
        _getSearchData.value = repository.getSearchVideoList(query).items
    }
}


class SearchVideoViewModelFactory : ViewModelProvider.Factory {
    private val repository = RepositoryImpl(NetworkClient.youtubeApiSearch)
    override fun <T : ViewModel> create(
        modelClass: Class<T>,
        extras: CreationExtras
    ): T {

        return SearchViewModel(
            repository
        ) as T
    }
}
  • ํŒ€์›๋ถ„์ด ๊ณต์œ ํ•ด์ฃผ์…จ๋˜ ๋ทฐ๋ชจ๋ธ์˜ ๊ธฐ๋ณธ ํ‹€์€ ์œ„์™€ ๊ฐ™๋‹ค. 
    ๋‚ด๊ฐ€ ํ•„์š”ํ•œ SearchItems๋ฅผ LiveData๋กœ ์„ค์ •ํ•˜๊ณ  ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ทฐ๋ชจ๋ธ ํŒฉํ† ๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์™œ ๋ทฐ๋ชจ๋ธ ํŒฉํ† ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ์ง€๋Š” ๋งํฌ(ํด๋ฆญ)์— ๋‚˜์™€์žˆ๋‹ค.
    ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ง„ ํŒฉํ† ๋ฆฌ๋Š” ๋‚ด๊ฐ€ ๋ถˆ๋Ÿฌ์˜ค๊ณ ์ž ํ•˜๋Š” Activity/Fragment์—์„œ ์ดˆ๊ธฐ ์…‹ํŒ…์— ์ด์šฉ๋œ๋‹ค.

 

class SearchViewModel(private val repository: Repository) : ViewModel() {
    private val _getSearchData: MutableLiveData<MutableList<SearchItems>> = MutableLiveData()
    val getSearchData: LiveData<MutableList<SearchItems>> get() = _getSearchData

    private val _errorMessage: MutableLiveData<String> = MutableLiveData()
    val errorMessage: LiveData<String> get() = _errorMessage

    fun searchVideos(query: String) {
        viewModelScope.launch {
            try {
                val searchItems = withContext(Dispatchers.IO){
                    repository.getSearchVideoList(query).items
                }
                _getSearchData.value = searchItems
            } catch (e: Exception) {
                handleException(e)
            }
        }
    }
    
    private fun handleException(exception: Exception) {
        val errorMessage = when (exception) {
            is IOException -> "IO Exception ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
            is HttpException -> {
                when (exception.code()) {
                    400 -> "Bad Request ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    401 -> "Unauthorized ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    404 -> "not found ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
                }
            }

            else -> "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค."
        }
        _errorMessage.value = errorMessage
    }
    
    class SearchVideoViewModelFactory : ViewModelProvider.Factory {
    private val repository = RepositoryImpl(NetworkClient.youtubeApiSearch)
    override fun <T : ViewModel> create(
        modelClass: Class<T>,
        extras: CreationExtras
    ): T {

        return SearchViewModel(
            repository
        ) as T
    }
}

 

  • ์ด์ œ ๊ธฐ๋ณธ ํ‹€์—์„œ ์ฝ”๋“œ๋ฅผ ๋‚ด ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์ ˆํžˆ ์ˆ˜์ •ํ•œ๋‹ค.
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด ์œ„์˜ _getSearchData ๊ตฌ์กฐ์™€ ๋งž๊ฒŒ _errorMessage๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๋‹ค.
  • ์•„๊นŒ Fragment์— ๊ตฌํ˜„ํ•ด๋†จ์—ˆ๋˜ searchVideos ์™€ handleException์„ ๋ทฐ๋ชจ๋ธ์— ์˜ฎ๊ฒจ์ค€๋‹ค.
  • ๋ทฐ๋ชจ๋ธ ํŒฉํ† ๋ฆฌ์—์„œ๋Š” ๊ธฐ์กด ํ‹€์—์„œ ํฌ๊ฒŒ ์ˆ˜์ •ํ•  ๊ฒƒ์ด ์—†์—ˆ๋‹ค.

 

3. RVSearchAdapter ๋ณ€๊ฒฝ

package com.limheejin.kidstopia.presentation.adapter

import ...

class RVSearchAdapter(
    private val onItemClick: (SearchItems) -> Unit,
    private val onLongClick: (Int) -> Boolean
) : RecyclerView.Adapter<RVSearchAdapter.MyViewHolder>(){

    private var items: List<SearchItems> = listOf()

    fun setItems(items: List<SearchItems>) {
        this.items = items
        notifyDataSetChanged()
    }

... ์ƒ๋žต ...

        fun bind(data: SearchItems) {
            with(binding) {
                searchitemTitle.text = data.snippet.title
                searchitemContext.text = data.snippet.description
                Glide.with(itemView.context)
                    .load(data.snippet.thumbnails.medium.url)
                    .into(searchitemThumbnail)
            }
        }
    }
}
  • ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ์–ด๋Œ‘ํ„ฐ์—์„œ๋Š” ํฌ๊ฒŒ ๋ณ€๊ฒฝํ•  ์ ์ด ์—†์ง€๋งŒ, ์‚ฌ์šฉํ•˜๋Š” ์•„์ดํ…œ์ด SearchItems์ธ ์ ์— ์œ ์˜ํ•œ๋‹ค.
  • ๋ฐ‘์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋“œ ํ•˜๋Š” ๋ถ€๋ถ„์ด ํ”„๋ž˜๊ทธ๋จผํŠธ๋‚˜ ๋ทฐ๋ชจ๋ธ์— ์žˆ์–ด์•ผ ํ•˜๋Š” ๊ฒŒ ์•„๋‹Œ๊ฐ€ ์ƒ๊ฐ์ด ๋“ค์—ˆ๋Š”๋ฐ, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ผ๋ฐ˜์ ์œผ๋กœ ์ €๋ ‡๊ฒŒ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ์–ด๋Œ‘ํ„ฐ์—์„œ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋งž๋‹ค๊ณ  ํ•œ๋‹ค.
    (1) ์ฑ…์ž„ ๋ถ„๋ฆฌ : ์–ด๋Œ‘ํ„ฐ๋Š” ๋ฐ์ดํ„ฐ์™€ UI ์š”์†Œ์˜ ๋ฐ”์ธ๋”ฉ์„ ์ฑ…์ž„์ง€๋ฉฐ, ๋ทฐ๋ชจ๋ธ์€ ๋ฐ์ดํ„ฐ ๋กœ์ง๊ณผ UI ๋กœ์ง์„ ๋ถ„๋ฆฌ
    (2) ์œ ์ง€๋ณด์ˆ˜์„ฑ : ๋ทฐํ™€๋” ํด๋ž˜์Šค ๋‚ด์—์„œ ๋ทฐ ๋ฐ”์ธ๋”ฉ์„ ์ฒ˜๋ฆฌํ•˜๋ฉด, UI ๊ด€๋ จ ์ฝ”๋“œ๊ฐ€ ์ง‘์ค‘๋˜์–ด ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜ ์‰ฌ์›€
    (3) ์„ฑ๋Šฅ ์ตœ์ ํ™” : ๋ทฐํ™€๋” ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • Fragment๋‚˜ ViewModel์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๊ณ  ์–ด๋Œ‘ํ„ฐ์— ์ „๋‹ฌ, ์–ด๋Œ‘ํ„ฐ๋Š” ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ UI์— ๋ฐ”์ธ๋”ฉ

 

 

 

 

 

3. MainActivity์— ์„ค์ •๋œ Navigation ๊ฐ ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ Fragment ์‚ด๋ฆฌ๊ธฐ

// ๊ธฐ์กด ์ฝ”๋“œ
when (item.itemId){
	R.id.mnu_home -> {
		supportFragmentManager.beginTransaction().replace(R.id.fl, HomeFragment()).commitAllowingStateLoss()
		return true
}
// ์ˆ˜์ • ์ฝ”๋“œ

override fun onNavigationItemSelected(item: MenuItem): Boolean {
        val fragmentTransaction = supportFragmentManager.beginTransaction()

        supportFragmentManager.fragments.forEach { fragment ->
            if (fragment.isVisible) {
                fragmentTransaction.hide(fragment)
            }
        }

        when (item.itemId) {
            R.id.mnu_home -> {
                val homeFragment =
                    supportFragmentManager.findFragmentByTag("HOME") ?: HomeFragment().apply {
                        fragmentTransaction.add(R.id.fl, this, "HOME")
                    }
                fragmentTransaction.show(homeFragment)
            }

            R.id.mnu_search -> {
                val searchFragment =
                    supportFragmentManager.findFragmentByTag("SEARCH") ?: SearchFragment().apply {
                        fragmentTransaction.add(R.id.fl, this, "SEARCH")
                    }
                fragmentTransaction.show(searchFragment)
            }

            R.id.mnu_user -> {
                val myVideoFragment = supportFragmentManager.findFragmentByTag("MY_VIDEO")
                    ?: MyVideoFragment().apply {
                        fragmentTransaction.add(R.id.fl, this, "MY_VIDEO")
                    }
                fragmentTransaction.show(myVideoFragment)
            }
        }
        fragmentTransaction.commitAllowingStateLoss()
        return true
    }
  • ๊ธฐ์กด์— ๋‹จ์ˆœํžˆ replace๋กœ Fragment๋ฅผ ์ฃฝ์ด๋ฉฐ ์˜ฎ๊ฒผ๋˜ ๋ฐฉ์‹์„, ๋ชจ๋‘ show(), hide()์˜ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.
  • ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ์˜ฎ๊ฒจ๋‹ค๋…€๋„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์™€ ์œ„์น˜๊ฐ€ ์‚ด์•„์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

 

 

 

 

 


 

 

๐Ÿ’ญ Retrospect

์ด์ œ๊ป ์ง„ํ–‰๋œ ํŒ€ํ”„๋กœ์ ํŠธ ์ค‘์—์„œ ๋‹ค๋ฅธ ์š”์†Œ๋Š” ๋‹ค ์ฐจ์น˜ํ•˜๊ณ , ๊ธฐ๋Šฅ ๊ตฌํ˜„๊ณผ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ์˜์—ญ์—์„  ์ œ์ผ ์—ด์‹ฌํžˆ ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค. ์ด๋ฒˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ œ์ผ ๋งŽ์ด ๋ฐฐ์šฐ๊ณ  ์žˆ๊ณ , ํ—ท๊ฐˆ๋ ธ๋˜ ์ด๋ก ๊ณผ ๊ตฌ์กฐ๋“ค(MVVM, ๋„คํŠธ์›Œํฌ, API, Fragment ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋“ฑ)์„ ์ด๋ฒˆ ์ฃผ ๋‚ด๋‚ด ๊ณ„์† ๋ณต์Šตํ•˜๋‹ˆ ์ž์—ฐ์Šค๋ ˆ ์ดํ•ด๊ฐ€ ๋˜๊ณ  ์žˆ๋‹ค. ์žฌ๋ฐŒ๊ณ  ์ข‹๋‹ค. ๋‹ค๋ฅธ ํŒ€์›๋ถ„์„ ๋„์™€์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐ์„œ ๊ธฐ์จ์„ ๋Š๋‚€๋‹ค.

โ€‹

 

 

 

 

๋ฐ˜์‘ํ˜•
 ๐Ÿ’ฌ C O M M E N T