๐ 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์ ๊ธฐ์ด์ ์ธ ๋ด์ฉ์ ์ ์ฒด์ ์ผ๋ก ๋ง์ด ํ์๊ธฐ ๋๋ฌธ์ ์์ผ๋ก์ ํฌ์คํ ์ ๊ฐ์ธ์ ์ผ๋ก ๊ณต๋ถํ๋ ํ ๋ง/๊ธฐ์ ์ ๋ฑ๊ฐ์ ํฌ์คํ ์ผ๋ก ์ ๋ฆฌํ ์์ ์ด๋ค.
2. Joyce ์๋๋ก์ด๋ ์์ ๋ ํ ๋ฐ ํ๋ก์ ํธ ์ค์ต
1~9. ์ด์ ์ ๊ณต๋ถํ๋ ๋ด์ฉ : ์ด์ TIL์ ์ ๋ฆฌ
10. Todo List ํ๋ก์ ํธ ์ ์
- Room, RecyclerView๋ฅผ ๋ณต์ตํ๊ธฐ ์ํจ
3. ํํ๋ก์ ํธ : Youtube Data API - SearchFragment, MVVM ๋ฑ ํ์ ์๋ฃ
๋งก์ ๋ถ๋ถ ์์์์ ํ์ ๊ตฌํ์ ๋ชจ๋ ์๋ฃํ๋ค.
1. Youtube Data API๋ฅผ ์ด์ฉํ์ฌ ๊ฒ์ ๊ฒฐ๊ณผ ์ถ๋ ฅ
- ์ฐ์ , ํด๋น 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 ๊ฐ์ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฑ)์ ์ด๋ฒ ์ฃผ ๋ด๋ด ๊ณ์ ๋ณต์ตํ๋ ์์ฐ์ค๋ ์ดํด๊ฐ ๋๊ณ ์๋ค. ์ฌ๋ฐ๊ณ ์ข๋ค. ๋ค๋ฅธ ํ์๋ถ์ ๋์์ค ์ ์๋ ๋ฐ์ ๊ธฐ์จ์ ๋๋๋ค.
โ