728x90

HomeFragment 상단 뷰 페이저

 

HomeFragment 

더보기
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayoutMediator
import com.nbc.shownect.R
import com.nbc.shownect.databinding.FragmentHomeBinding
import com.nbc.shownect.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.shownect.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.shownect.presentation.home.adapter.GenreAdapter
import com.nbc.shownect.presentation.home.adapter.KidShowAdapter
import com.nbc.shownect.presentation.home.adapter.PosterClickListener
import com.nbc.shownect.presentation.home.adapter.TopRankAdapter
import com.nbc.shownect.presentation.home.adapter.UpcomingShowAdapter
import com.nbc.shownect.presentation.main.MainViewModel
import com.nbc.shownect.presentation.main.MainViewModelFactory
import com.nbc.shownect.presentation.ticket.TicketDialogFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class HomeFragment : Fragment(), PosterClickListener {
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    private val viewModel: HomeViewModel by viewModels {
        HomeViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(fetch),
        )
    }
    private val sharedViewModel: MainViewModel by activityViewModels<MainViewModel> {
        MainViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(
                fetch
            )
        )
    }
    private val upComingShowAdapter: UpcomingShowAdapter by lazy { UpcomingShowAdapter(this) }
    private val topRankAdapter: TopRankAdapter by lazy { TopRankAdapter(this) }
    private val genreAdapter: GenreAdapter by lazy { GenreAdapter(this) }
    private val kidShowAdapter: KidShowAdapter by lazy { KidShowAdapter(this) }
    private var isPaging = false
    private var pagingJob: Job? = null
    private val onPageChangeCallback: OnPageChangeCallback = object : OnPageChangeCallback() {
        //페이지 선택 될 때마다 호출
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            binding.tvPageIndicator.text = "${position + 1} / 10"
        }
    }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        initViews()
        setUpObserve()
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        with(viewModel) {
            //공연 예정작
            fetchUpcoming()
            //TOP 10 공연
            fetchTopRank()
            //장르 스피너 선택
            binding.spinnerHomeGenre.setOnSpinnerItemSelectedListener<String> { _, _, newIndex, _ ->
                fetchGenre(newIndex)
            }
            //어린이 관람 가능 공연 목록
            fetchKidShow()
        }
    }

    //화면 초기 설정
    private fun initViews() {
        //어뎁터 초기화
        upComingShowAdapter
        topRankAdapter
        kidShowAdapter
        initRecyclerView()

        //viewPager 연결
        with(binding.viewPager) {
            adapter = upComingShowAdapter
            //viewPager PageTransformer 세팅
            offscreenPageLimit = 1
            setPageTransformer(SliderTransformer(requireContext()))
            val itemDecoration = HorizontalMarginItemDecoration(
                requireContext(),
                R.dimen.viewpager_current_item_horizontal_margin
            )
            addItemDecoration(itemDecoration)


            registerOnPageChangeCallback(onPageChangeCallback)

            //tab 연결
            TabLayoutMediator(binding.tabPosterIndicator, this) { tab, position ->
                currentItem = tab.position
            }.attach()
        }
        //장르 연극 초기화
        viewModel.fetchGenre(0)
    }

    //옵저브 세팅
    private fun setUpObserve() {
        with(viewModel) {
            showList.observe(viewLifecycleOwner) {
                upComingShowAdapter.submitList(it)
                if (!isPaging) startPaging()
            }
            topRank.observe(viewLifecycleOwner) {
                topRankAdapter.submitList(it?.take(10))
            }
            genre.observe(viewLifecycleOwner) {
                genreAdapter.submitList(it)
            }
            kidShow.observe(viewLifecycleOwner) {
                kidShowAdapter.submitList(it)
            }
            //로딩 화면 처리
            isLoadingGenre.observe(viewLifecycleOwner) {
                binding.skeletonGenreLoading.isVisible = !it
            }
            isLoadingRecommend.observe(viewLifecycleOwner) {
                binding.skeletonTopRankLoading.isVisible = !it
            }
            isLoadingKid.observe(viewLifecycleOwner) {
                binding.skeletonKidLoading.isVisible = !it
            }

        }
    }

    //리사이클러뷰 초기화
    private fun initRecyclerView() {
        with(binding) {
            //HOT 추천 리사이클러뷰
            rvHomeTopRank.apply {
                adapter = topRankAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //장르별 리사이클러뷰
            rvHomeGenre.apply {
                adapter = genreAdapter
                layoutManager =
                    LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //어린이 공연 리사이클러뷰
            rvHomeKidShow.apply {
                adapter = kidShowAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
        }
    }

    //3초 후 자동 페이징
    private fun nextPage() {
        runCatching {
            with(binding) {
                if (viewPager.currentItem == 9) {
                    lifecycleScope.launch {
                        delay(3000)
                    }
                    viewPager.currentItem = 0
                } else {
                    viewPager.currentItem++
                }
            }
        }.onFailure {
            Toast.makeText(context, "Exception nextPage()", Toast.LENGTH_SHORT).show()
        }
    }

    //페이징 스타트 함수
    private fun startPaging() {
        isPaging = true
        pagingJob = lifecycleScope.launch {
            while (true) {
                delay(3000)
                nextPage()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        isPaging = false
        pagingJob?.cancel()
    }

    //포스터 클릭 시 티켓
    override fun posterClicked(id: String) {
        val ticketDialog = TicketDialogFragment()
        sharedViewModel.sharedShowId(id) //해당 공연의 id를 MainViewModel로 보내줌
        ticketDialog.setStyle(
            DialogFragment.STYLE_NORMAL,
            R.style.RoundCornerBottomSheetDialogTheme
        )
        ticketDialog.show(childFragmentManager, ticketDialog.tag)
    }
}

offscreenPageLimit = 1 

  • 미리 로딩할 페이지 수

SliderTransformer - 페이지 변환 효과, 스크롤될 때 각 페이지에 적용되는 시각적 변환

  • transformPage 메소드는 페이지의 위치(position)에 따라 페이지의 변환을 결정
  • position 값은 현재 중앙에 위치한 페이지를 기준으로 왼쪽에 있는 페이지는 음수(-1, -0.5 등), 오른쪽에 있는 페이지는 양수(0.5, 1 등)으로 표현
  • page.translationX는 페이지의 가로 위치를 조정하여, 스크롤 시 페이지가 어떻게 이동할지 정의 (페이지 사이의 간격을 조정하는 데 사용)
  • page.scaleY는 페이지의 세로 축 스케일을 조정하여, 페이지가 어느 정도 떨어져 있을 때 크기가 줄어들게 함
  • page.alpha는 투명도 설정 (페이지가 중앙에 있을 때(position == 0)) , 최대 100% 불투명도(1.0f) 중앙에서 멀어질 수록 투명 (0.25f 는 최소 투명도)

리소스에서 다음 페이지가 보이는 부분(nextItemVisiblePx)과 현재 페이지의 수평 마진(currentItemHorizontalMarginPx)을 가져오고, 이 두 값을 합산하여 pageTranslationX를 계산 (스크롤 시 페이지의 위치 변환에 사용)

더보기
import android.content.Context
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import com.nbc.shownect.R
import kotlin.math.abs

class SliderTransformer(context: Context) :
    ViewPager2.PageTransformer {
    override fun transformPage(page: View, position: Float) {
        page.translationX = -pageTranslationX * position
        page.scaleY = 1 - (0.25f * abs(position))
        page.alpha = 0.25f + (1 - abs(position))
    }

    val nextItemVisiblePx =
        context.resources.getDimension(R.dimen.viewpager_next_item_visible)
    val currentItemHorizontalMarginPx =
        context.resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
    val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
}

 

HorizontalMarginItemDecoration - 리사이클러뷰 항목에 수평 마진을 추가하는 기능 (주로 리사이클러 뷰 내부의 아이템 간격을 조정하는 데 사용)

  • 생성자는 Context와 마진 값을 DP 단위로 받아 해당 값을 픽셀 단위로 변환
  • context.resources.getDimension() 리소스 ID를 통해 해당 마진 값의 실제 픽셀 크기를 가져옴
  • getItemOffsets() 메소드는 리사이클러뷰 내 각 항목에 대한 오프셋(여기서는 마진)을 설정, outRect.left와 outRect.right를 설정하여 각 항목의 왼쪽과 오른쪽에 마진을 적용
더보기
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.annotation.DimenRes
import androidx.recyclerview.widget.RecyclerView

class HorizontalMarginItemDecoration(context: Context, @DimenRes horizontalMarginInDp: Int) :
    RecyclerView.ItemDecoration() {
    private val horizontalMarginInPx: Int =
        context.resources.getDimension(horizontalMarginInDp).toInt()

    override fun getItemOffsets(
        outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
    ) {
        outRect.right = horizontalMarginInPx
        outRect.left = horizontalMarginInPx
    }
}

res/values/dimens/dimens.xml

더보기
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    //추가된 부분 
    <dimen name="viewpager_next_item_visible">26dp</dimen>
    <dimen name="viewpager_current_item_horizontal_margin">42dp</dimen>
</resources>

viewpager_next_item_visible

  • ViewPager2를 사용할 때, 슬라이드되고 있는 다음 항목이 얼마나 보여질지를 정의하는 값
  • SliderTransformer Class에서 사용 

viewpager_current_item_horizontal_margin

  • 현재 활성화된 ViewPager2 항목의 수평 마진 크기를 정의
  • 현재 보여지는 항목과 양 옆의 항목 사이의 간격을 결정
  • HorizontalMarginItemDecoration Class 에서 사용 

 

참고 

https://github.com/unaisulhadi/ViewPager2-Carousel

 

GitHub - unaisulhadi/ViewPager2-Carousel: A simple Carousel for ViewPager2

A simple Carousel for ViewPager2. Contribute to unaisulhadi/ViewPager2-Carousel development by creating an account on GitHub.

github.com

https://stackoverflow.com/questions/71108306/viewpager2-left-and-right-preview-item-scrolling-not-working

 

Viewpager2 left and right preview item scrolling not working

I am trying to create an image slider using ViewPager2 with left and right previews. Everything is working perfectly, but scrolling on left and right preview items is not working. There is a similar

stackoverflow.com

 

728x90

블러처리된 이미지 백그라운드

 

프로젝트에서 이미지를 블러 처리할 일 이 생겨서 방법을 찾아 보았다. 

 

처음에는 Glide에서 BlurTransformation 을 지원해주는 줄 알았지만 wasabeef 의 라이브러리를 추가해 주어야 했다.

여기서 BlurTransformation()이 사용이안되어서 오랜시간 삽질을 했다. 

 

 

이미지를 처리하기 위한 라이브러리 추가 

 

사용방법  

			Glide.with(requireContext())
                            .load(showDetail.poster)
                            .apply(RequestOptions.bitmapTransform(BlurTransformation(15, 1)))
                            .into(ivSimplePosterBlur)

 

이미지를 블러 처리하는 부분  

.apply(RequestOptions.bitmapTransform(BlurTransformation(15, 1)))

BlurTransformation 은 radius (15) , sampling (1) 를 매개변수로 받는다.

 

Radius

  • 수치가 높을 수록 더 강한 블러처리 (이 값이 클수록 이미지가 더 흐릿해짐)
  • radius가 클수록 더 많은 픽셀이 블러 처리
  • 보통 1부터 25까지 처리 가능   

sampling 

  • 이미지를 샘플링하는 데 사용
  • 블러 효과의 품질을 결정
  • 높은 샘플링 값은 더 고해상도의 이미지를 사용하여 블러를 생성하므로 더 세밀한 블러를 얻을 수 있지만, 그만큼 처리 시간이 더 오래 걸릴 수 있음 (sampling 값을 높이면 성능은 향상되지만, 이미지의 디테일이 손실될 수 있음)
  • 최소값은 1 (1을 설정하면 이미지를 축소하지 않고 원본 해상도 그대로 블러 처리)

 

 

 

 

 

 

라이브러리 깃 허브 

 

BlurTransformation 

https://github.com/wasabeef/glide-transformations

 

GitHub - wasabeef/glide-transformations: An Android transformation library providing a variety of image transformations for Glid

An Android transformation library providing a variety of image transformations for Glide. - wasabeef/glide-transformations

github.com

 

Glide 

https://github.com/bumptech/glide

 

GitHub - bumptech/glide: An image loading and caching library for Android focused on smooth scrolling

An image loading and caching library for Android focused on smooth scrolling - bumptech/glide

github.com

 

728x90

프래그먼트간 데이터를 공유하려면 여러 가지 방법이있다.

  • ViewModel 사용하기 
    • 여러 프래그먼트가 같은 액티비티에 속해 있으면, 이 프래그먼트들은 같은 ViewModel을 공유해서 데이터를 주고받을 수 있어서 프래그먼트들 사이의 결합도가 낮아지고, 앱을 더 쉽게 관리할 수 있다.
  • 액티비티를 통해 데이터 공유하기
    • 프래그먼트는 액티비티에 붙어 있어서 액티비티를 매개체로 삼아 데이터를 주고받을 수 있다.
    • 한 프래그먼트에서 액티비티의 메소드를 호출해 데이터를 전달하고, 다른 프래그먼트에서는 액티비티를 통해 그 데이터를 가져올 수 있다.
  • 인터페이스 사용하기
    • 데이터를 공유할 때 인터페이스를 정의해서 사용하는 방법.
    • 액티비티에서 인터페이스를 구현하고, 데이터를 보내고 싶은 프래그먼트에서는 인터페이스 메소드를 호출해 데이터를 전달하면 다른 프래그먼트에서 이 인터페이스를 통해 데이터를 받을 수 있다.
    • 복잡하다.
  • Bundle 사용해서 데이터 전달하기
    • 프래그먼트를 전환할 때 Bundle에 데이터를 담아서 보낼 수 있음.
    • 주로 새 프래그먼트를 만들 때 시작 데이터를 전달하는 데 사용됨.
  • EventBus 같은 외부 라이브러리 사용하기
    • EventBus 같은 라이브러리를 사용해서 앱 전체에서 이벤트를 발행하고 구독함으로써 프래그먼트 간에 데이터를 공유할 수 있다.
    • 코드를 더 간결하게 만들어 주고, 결합도를 낮출 수 있지만, 외부 라이브러리에 의존하게 됨.
  • Shared Preferences로 데이터 공유하기
    • 간단한 데이터는 Shared Preferences를 통해 저장하고 공유할 수 있음. 앱의 설정 값이나 작은 데이터 조각을 저장하는 데 주로 사용
    • 매번 sharedPreference에서 값을 읽어와야하는 번거로움.

 

이번 프로젝트에서는 ViewModel을 사용하는 방법을 적용해 보았다.  

 

Activity에 포함된 둘 이상의 프래그먼트는 ViewModel을 통해서 데이터 공유가 가능하다.

 

Fragment들은 각자의 ViewModel (by viewModels()) 를 가질 수도 있고,

Activity의 ViewModel (by activityViewModels)  을 공유해서 사용할 수도 있는 것

 

프로젝트에 적용해 보기

해당 공연의 id를 받아와서 데이터를 뿌려주는 화면

HomeFragment ( TicketDialogFragment 으로 데이터를 보낼 Fragment

더보기
package com.nbc.curtaincall.ui.home

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayoutMediator
import com.nbc.curtaincall.R
import com.nbc.curtaincall.databinding.FragmentHomeBinding
import com.nbc.curtaincall.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.curtaincall.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.curtaincall.ui.home.adapter.GenreAdapter
import com.nbc.curtaincall.ui.home.adapter.KidShowAdapter
import com.nbc.curtaincall.ui.home.adapter.PosterClickListener
import com.nbc.curtaincall.ui.home.adapter.TopRankAdapter
import com.nbc.curtaincall.ui.home.adapter.UpcomingShowAdapter
import com.nbc.curtaincall.ui.main.MainViewModel
import com.nbc.curtaincall.ui.main.MainViewModelFactory
import com.nbc.curtaincall.ui.ticket.TicketDialogFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class HomeFragment : Fragment(), PosterClickListener {
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    //해당 프레그먼트의 ViewModel
    private val viewModel: HomeViewModel by viewModels {
        HomeViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(fetch),
        )
    }
    //해당 액티비티의 ViewModel -> sharedViewModel을 통해서 데이터 공유
    private val sharedViewModel: MainViewModel by activityViewModels<MainViewModel> {
        MainViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(
                fetch
            )
        )
    }
    private val upComingShowAdapter: UpcomingShowAdapter by lazy { UpcomingShowAdapter() }
    private val topRankAdapter: TopRankAdapter by lazy { TopRankAdapter(this) }
    private val genreAdapter: GenreAdapter by lazy { GenreAdapter(this) }
    private val kidShowAdapter: KidShowAdapter by lazy { KidShowAdapter(this) }
    private var isPaging = false
    private var pagingJob: Job? = null

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        with(viewModel) {
            //공연 예정작
            fetchUpcoming()
            //TOP 10 공연
            fetchTopRank()
            //장르 스피너 선택
            binding.spinnerHomeGenre.setOnSpinnerItemSelectedListener<String> { _, _, newIndex, _ ->
                fetchGenre(newIndex)
            }
            //어린이 관람 가능 공연 목록
            fetchKidShow()
        }
    }

    //화면 초기 설정
    private fun initViews() {
        //어뎁터 초기화
        upComingShowAdapter
        topRankAdapter
        kidShowAdapter
        initRecyclerView()
        with(viewModel) {
            showList.observe(viewLifecycleOwner) {
                upComingShowAdapter.submitList(it)
                with(binding) {
                    //viewpager 연결
                    viewPager.adapter = upComingShowAdapter
                    //tab 연결
                    TabLayoutMediator(tabPosterIndicator, viewPager) { tab, position ->
                        viewPager.currentItem = tab.position
                    }.attach()
                }
                if (!isPaging) startPaging()
            }
            //장르 연극 초기화
            fetchGenre(0)
        }
    }

    //옵저브 세팅
    private fun setUpObserve() {
        with(viewModel) {
            topRank.observe(viewLifecycleOwner) {
                topRankAdapter.submitList(it.take(10))
            }
            genre.observe(viewLifecycleOwner) {
                genreAdapter.submitList(it)
            }
            kidShow.observe(viewLifecycleOwner) {
                kidShowAdapter.submitList(it)
            }
        }
    }

    //리사이클러뷰 초기화
    private fun initRecyclerView() {
        with(binding) {
            //HOT 추천 리사이클러뷰
            rvHomeTopRank.apply {
                adapter = topRankAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //장르별 리사이클러뷰
            rvHomeGenre.apply {
                adapter = genreAdapter
                layoutManager =
                    LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //어린이 공연 리사이클러뷰
            rvHomeKidShow.apply {
                adapter = kidShowAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
        }
    }

    //3초 후 자동 페이징
    private fun nextPage() {
        runCatching {
            with(binding) {
                if (viewPager.currentItem == 9) {
                    lifecycleScope.launch {
                        delay(3000)
                    }
                    viewPager.currentItem = 0
                } else {
                    viewPager.currentItem++
                }
            }
        }.onFailure {
            Toast.makeText(context, "Exception nextPage()", Toast.LENGTH_SHORT).show()
        }
    }

    //페이징 스타트 함수
    private fun startPaging() {
        isPaging = true
        pagingJob = lifecycleScope.launch {
            while (true) {
                delay(3000)
                nextPage()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        isPaging = false
        pagingJob?.cancel()
    }

    //포스터 클릭 시 티켓
    override fun posterClicked(id: String) {
        val ticketDialog = TicketDialogFragment()
        sharedViewModel.sharedShowId(id) //해당 공연의 id를 MainViewModel로 보내줌
        ticketDialog.setStyle(
            DialogFragment.STYLE_NORMAL,
            R.style.RoundCornerBottomSheetDialogTheme
        )
        ticketDialog.show(childFragmentManager, TicketDialogFragment().tag)
    }
}

 

TicketDialogFragment (HomeFragment 에서 데이터를 받아올 Fragment) 

더보기
package com.nbc.curtaincall.ui.ticket

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import coil.load
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.nbc.curtaincall.databinding.SimpleInfoBottomsheetDialogBinding
import com.nbc.curtaincall.fetch.network.retrofit.RetrofitClient.fetch
import com.nbc.curtaincall.fetch.repository.impl.FetchRepositoryImpl
import com.nbc.curtaincall.ui.main.MainViewModel
import com.nbc.curtaincall.ui.main.MainViewModelFactory

class TicketDialogFragment : BottomSheetDialogFragment() {
    private var _binding: SimpleInfoBottomsheetDialogBinding? = null
    private val binding get() = _binding!!
    //해당 액티비티의 ViewModel -> sharedViewModel을 통해서 데이터 공유
    private val sharedViewModel: MainViewModel by activityViewModels<MainViewModel> {
        MainViewModelFactory(
            fetchRemoteRepository = FetchRepositoryImpl(fetch = fetch)
        )
    }

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

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //TODO Swipe To Action
        with(sharedViewModel) {
            //HomeFragment에서 id를 보내주면 fetchShowDetail() 호출
            showId.observe(viewLifecycleOwner) { id ->
                sharedViewModel.fetchShowDetail(id)
            }
            showDetailInfo.observe(viewLifecycleOwner) { showDetailInfoList ->
                with(binding) {
                    ivDetailPosterImage.load(showDetailInfoList[0].poster)
                    tvDetailAge.text = showDetailInfoList[0].prfage
                    tvDetailRuntime.text = showDetailInfoList[0].prfruntime
                    tvDetailFacilityName.text = showDetailInfoList[0].dtguidance
                    tvDetailPrice.text= showDetailInfoList[0].pcseguidance
                    tvDetailFacilityName.text = showDetailInfoList[0].fcltynm
                    tvDetailGenre.text = showDetailInfoList[0].genrenm
                }
            }
        }
    }

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

HomeViewModel (액티비티의 ViewModel로 MainViewModel 에서 데이터 공유)

더보기
package com.nbc.curtaincall.ui.main

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.nbc.curtaincall.fetch.model.DbResponse
import com.nbc.curtaincall.fetch.repository.impl.FetchRepositoryImpl
import kotlinx.coroutines.launch

class MainViewModel(private val fetchRemoteRepository: FetchRepositoryImpl) : ViewModel() {
    //공연 상세 정보 리스트
    private val _showDetailInfo = MutableLiveData<List<DbResponse>>()
    val showDetailInfo: LiveData<List<DbResponse>> get() = _showDetailInfo
    //공연 id
    private val _showId = MutableLiveData<String>()
    val showId: LiveData<String> get() = _showId

    //id를 공유 하기 위한 함수
    fun sharedShowId(id: String) {
        _showId.value = id
    }

    //공연 상세 정보를 받아옴
    fun fetchShowDetail(id: String) {
        viewModelScope.launch {
            _showDetailInfo.value = fetchRemoteRepository.fetchShowDetail(path = id).showList
        }
    }
}

class MainViewModelFactory(private val fetchRemoteRepository: FetchRepositoryImpl):ViewModelProvider.Factory{
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(fetchRemoteRepository) as T
    }
}

 

 

HomeFragment 에서 MainViewModel 로 id를 보내주고 TicketDialogFragment 에서 저장된 id 를 꺼내쓰는 구조 

Fragment가 여러개라도 속해있는 Activity에서 파생되었기 때문에 Activity를 통해서 데이터 공유가 가능하다. 

728x90

프로젝트 개선사항 

  • MVVM 패턴 구조로 변경
  • API 통신에 대한 예외 처리
  • 다른 화면으로 전환 후 다시 카테고리 화면으로 돌아 갔을 때 선택된 칩에 대한 카테고리를 기억하는 기능 추가 
  • 트렌드 화면에서 스크롤을 끝까지 내리면 컨텐츠가 계속 나오도록 무한 스크롤 구현
  • 상단으로 이동 할 수있는 Scroll To Top 기능 추가 

무한 스크롤/Scroll To Top - 카테고리 저장/통신 실패 시 예외처리 화면

이전 코드와 변경 사항은 MVVM 패턴 을 적용해 보기 위해 Model-View-ViewModel 구조로 분리 해봤다.

Repository

더보기
package com.brandon.playvideo_app.repository

import com.brandon.playvideo_app.data.api.RetrofitInstance
import com.brandon.playvideo_app.data.model.CategoryItem
import com.brandon.playvideo_app.data.model.ChannelItem
import com.brandon.playvideo_app.data.model.Item
import com.brandon.playvideo_app.data.model.YoutubeVideoResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class PlayVideoRepository {
    //api 통신 부분
    suspend fun getTrendingVideos(): YoutubeVideoResponse =
        withContext(Dispatchers.IO) {
            RetrofitInstance.api.getTrendingVideos()
        }

    //칩 선택시 카테고리 영상
    suspend fun fetchCategoryVideos(videoCategoryId: String): List<Item> =
        withContext(Dispatchers.IO) {
            RetrofitInstance.api.getTrendingVideos(videoCategoryId = videoCategoryId).items
        }

    //채널 정보
    suspend fun getChannelInfo(channelIdList: MutableList<String>): List<ChannelItem> =
        withContext(Dispatchers.IO) {
            RetrofitInstance.api.getChannelInfo(channelId = channelIdList.joinToString(",")).items
        }
    //카테고리별 id를 받아오는 코드

    suspend fun getCategoryIds(): List<CategoryItem> =
        withContext(Dispatchers.IO) {
            RetrofitInstance.api.getCategoryIds().items
        }

    //다음 페이지의 트렌딩 비디오를 받아오 옴
    suspend fun getNextTrendingVideos(nextPageToken: String): YoutubeVideoResponse =
        withContext(Dispatchers.IO) {
            RetrofitInstance.api.getTrendingVideos(pageToken = nextPageToken)
        }
}

TrendFragment

더보기
package com.brandon.playvideo_app.ui.trend

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.brandon.playvideo_app.R
import com.brandon.playvideo_app.databinding.ToolbarCommonBinding
import com.brandon.playvideo_app.databinding.TrendFragmentBinding
import com.brandon.playvideo_app.viewmodel.TrendViewModel
import timber.log.Timber

class TrendFragment : Fragment() {
    private var _binding: TrendFragmentBinding? = null
    private val binding get() = _binding!!

    private val videoAdapter by lazy { VideoAdapter() }
    private val viewModel by viewModels<TrendViewModel>()


    companion object {
        @JvmStatic
        fun newInstance() =
            TrendFragment().apply {
                arguments = Bundle().apply {
                }
            }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //Timber.d("Create")
    }

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

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

        val toolbarBinding = ToolbarCommonBinding.bind(view.findViewById(R.id.included_tool_bar))
        toolbarBinding.toolbarCommon.inflateMenu(R.menu.common_tool_bar_menu)

        toolbarBinding.toolbarCommon.setOnMenuItemClickListener { menuItem ->
            when (menuItem.itemId) {
                R.id.search -> {
                    // 메뉴 아이템 1 클릭 시 동작할 코드 작성
                    Timber.d("Search Item Clicked!")
                    true
                }
                // 다른 메뉴 아이템에 대해서도 필요한 경우 추가할 수 있음
                else -> false
            }
        }
        //viewModel에 데이터 요청
        viewModel.trendingVideos()
        setUpClickListener()
    }

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

    //리사이클러뷰 초기화
    private fun initRecyclerView() {
        with(binding) {
            recyclerView.apply {
                adapter = videoAdapter
                layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
                addOnScrollListener(onScrollListener)
            }
        }
    }


    //viewModel 상태 관찰 //binding 으로 묶고 viewModel 상태를 observing 해도 되는지??
    private fun viewModelObserve() {
        with(viewModel) {
            trendVideos.observe(viewLifecycleOwner) {
                val videos = videoAdapter.currentList.toMutableList().apply {
                    addAll(it)
                }
                videoAdapter.submitList(videos)
            }
            isLoading.observe(viewLifecycleOwner) {
                binding.pbTrendLoading.isVisible = it
            }
            receptionImage.observe(viewLifecycleOwner) {
                binding.constraintLayoutTrendFragment.isVisible = it
            }
        }
    }

    private fun setUpClickListener() {
        with(binding) {
            fbTrendScrollToTop.setOnClickListener {
                recyclerView.smoothScrollToPosition(0)
            }
        }
    }

    private var onScrollListener: RecyclerView.OnScrollListener =
        object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                //스크롤이 끝까지 닫아서 내릴 곳이 없으면 아이템을 추가
                if (!recyclerView.canScrollVertically(1)) {
                    with(viewModel) {
                        loadingState(true)
                        getNextTrendingVideos()
                    }
                }
                //scrollToTop 버튼 visible
                with(binding) {
                    if (dy < 0 && fbTrendScrollToTop.isVisible) fbTrendScrollToTop.hide()
                    else if (dy > 0 && !fbTrendScrollToTop.isVisible) fbTrendScrollToTop.show()
                    //맨위면 hide
                    if (!recyclerView.canScrollVertically(-1)) fbTrendScrollToTop.hide()
                }
            }
        }
}

TrendViewModel

더보기
package com.brandon.playvideo_app.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.brandon.playvideo_app.data.model.Item
import com.brandon.playvideo_app.repository.PlayVideoRepository
import kotlinx.coroutines.launch

class TrendViewModel(val repository: PlayVideoRepository = PlayVideoRepository()) : ViewModel() {
    private val _trendVideos = MutableLiveData<List<Item>>()
    val trendVideos: LiveData<List<Item>> get() = _trendVideos

    private val _isLoading = MutableLiveData<Boolean>(true)
    val isLoading: LiveData<Boolean> get() = _isLoading

    private val _receptionImage = MutableLiveData<Boolean>(false)
    val receptionImage: LiveData<Boolean> get() = _receptionImage

    //다음 페이지 정보 관련 변수
    private val pageList: MutableList<String> = mutableListOf()
    private var pageIdx = 0

    //repository 데이터 요청 하고 통신 끝나면 isLoading false
    //최초 실행시 nextPage를 받아 와서 List에 저장
    fun trendingVideos() {
        viewModelScope.launch {
            runCatching {
                val trendingVideos = repository.getTrendingVideos().items
                _trendVideos.value = trendingVideos
                loadingState(false)
                val nextPageToken = repository.getTrendingVideos().nextPageToken
                pageList.add(nextPageToken)
                failedState(false)
            }.onFailure {
                loadingState(false)
                failedState(true) //통신 실패
            }
        }
    }

    fun loadingState(state: Boolean) {
        _isLoading.value = state
    }

    //다음 트렌딩 비디오를 받아 오는 함수
    fun getNextTrendingVideos() {
        viewModelScope.launch {
            val videos = repository.getNextTrendingVideos(pageList[pageIdx]).items
            _trendVideos.value = videos
            loadingState(false)
            //해당 페이지의 nextPageToken을 받아옴
            val nextPageToken = repository.getNextTrendingVideos(pageList[pageIdx]).nextPageToken
            pageList.add(nextPageToken)
            //페이지 index 이동
            pageIdx++
        }
    }

    //통신 불가 예외 처리 state true 이면 실패
    private fun failedState(state: Boolean) {
        _receptionImage.value = state
    }
}

CategoryFragment

더보기
package com.brandon.playvideo_app.ui.category

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.brandon.playvideo_app.R
import com.brandon.playvideo_app.databinding.CategoryFragmentBinding
import com.brandon.playvideo_app.databinding.ToolbarCommonBinding
import com.brandon.playvideo_app.viewmodel.CategoryViewModel
import com.google.android.material.chip.Chip

class CategoryFragment : Fragment() {
    private var _binding: CategoryFragmentBinding? = null
    private val binding get() = _binding!!
    private val viewModel by viewModels<CategoryViewModel>()
    private val channelAdapter by lazy { ChannelAdapter() }
    private lateinit var categoryVideoAdapter: CategoryVideoAdapter

    companion object {
        @JvmStatic
        fun newInstance() =
            CategoryFragment().apply {
                arguments = Bundle().apply {
                }
            }
    }

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

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

        //진입시 초기 트렌드 비디오 셋팅
        initRecyclerView()
        viewModelObserve()
        return binding.root
    }

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

        val toolbarBinding = ToolbarCommonBinding.bind(view.findViewById(R.id.included_tool_bar))
        toolbarBinding.toolbarCommon.inflateMenu(R.menu.common_tool_bar_menu)
        toolbarBinding.toolbarCommon.setOnMenuItemClickListener { menuItem ->
            when (menuItem.itemId) {
                R.id.search -> {
                    true
                }

                else -> false
            }
        }

    }

    private fun initRecyclerView() {
        categoryVideoAdapter = CategoryVideoAdapter(listOf())
        with(binding) {
            //카테고리 영상 recyclerView
            recyclerviewCategory.apply {
                adapter = categoryVideoAdapter
                layoutManager =
                    LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
            //채널 정보 recyclerView
            recyclerviewChannelsByCategory.apply {
                adapter = channelAdapter
                layoutManager =
                    LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            }
        }
    }

    //초기 화면 셋팅 칩 생성
    private fun initViews() {
        with(viewModel) {
            loadingState(false)
            getCategoryIds()
            //assignable 허용된 id만 가져와서 칩그룹에 추가하는 코드
            categoryIdList.observe(viewLifecycleOwner) { idList ->
                idList.filter { it.snippet.assignable }.forEach { id ->
                    binding.chipGroupCategory.addView(createChip(id.snippet.title, id.id))
                }
            }
        }
    }

    private fun createChip(category: String, videoCategoryId: String): Chip {
        return Chip(context).apply {
            setText(category)
            isClickable = true
            isCheckable = true

            setOnClickListener {
                with(viewModel) {
                    //로딩 ui 처리
                    loadingState(true)
                    //칩이 눌리면 카테고리별 영상과 채널 정보를 가져옴
                    fetchCategoryVideos(videoCategoryId)
                    //칩이 눌리면 카테고리명 저장
                    saveCategoryTitle(category)
                }
            }
        }
    }

    private fun viewVisible(state: Boolean) {
        with(binding) {
            tvChannelByCategory.isVisible = state
            constraintLayoutCategoryFragment.isVisible = !state
            ivCategoryLogo.isVisible = state
        }
    }

    private fun viewModelObserve() {
        with(viewModel) {
            //초기 화면 트렌딩 비디오
            trendVideos.observe(viewLifecycleOwner) {
                categoryVideoAdapter.items = it
                categoryVideoAdapter.notifyDataSetChanged()
            }
            //카테고리 칩이 눌리면 카테고리별 영상과 채널의 썸네일을 보여줌
            categoryVideos.observe(viewLifecycleOwner) {
                categoryVideoAdapter.items = it
                categoryVideoAdapter.notifyDataSetChanged()
            }
            //채널 썸네일
            channel.observe(viewLifecycleOwner) {
                channelAdapter.channelItem = it
                channelAdapter.notifyDataSetChanged()
            }
            //로딩 상태 처리
            isLoading.observe(viewLifecycleOwner) {
                binding.pbCategoryLoading.isVisible = it
            }
            //api 통신 상태에 따른 ui 처리
            receptionState.observe(viewLifecycleOwner) {
                viewVisible(it)
            }
            //최초 CategoryFragment 진입 했을 때 처리
            initState.observe(viewLifecycleOwner) { initState ->
                if (initState) {
                    viewModel.getTrendingVideos() //초기 화면 트렌드 비디오 셋팅
                    binding.tvChannelByCategory.isVisible = false
                }
            }
            //선택된 칩의 위치를 찾아서 isChecked 상태로 변경
            saveCategoryTitle.observe(viewLifecycleOwner) { categoryTitle ->
                val selectedChip =
                    binding.chipGroupCategory.children.firstOrNull { (it as Chip).text == categoryTitle } as? Chip
                selectedChip?.isChecked = true
            }
        }
    }
}

CategoryViewModel

더보기
package com.brandon.playvideo_app.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.brandon.playvideo_app.data.model.CategoryItem
import com.brandon.playvideo_app.data.model.ChannelItem
import com.brandon.playvideo_app.data.model.Item
import com.brandon.playvideo_app.repository.PlayVideoRepository
import kotlinx.coroutines.launch

class CategoryViewModel(val repository: PlayVideoRepository = PlayVideoRepository()) : ViewModel() {
    private val _trendVideos = MutableLiveData<List<Item>>()
    val trendVideos: LiveData<List<Item>> get() = _trendVideos

    private val _categoryVideos = MutableLiveData<List<Item>>()
    val categoryVideos: LiveData<List<Item>> get() = _categoryVideos

    private val _channel = MutableLiveData<List<ChannelItem>>()
    val channel: LiveData<List<ChannelItem>> get() = _channel

    //카테고리의 ID
    private val _categoryIdList = MutableLiveData<List<CategoryItem>>()
    val categoryIdList: LiveData<List<CategoryItem>> get() = _categoryIdList

    //채널 고유의 ID
    private val channelIdList = mutableListOf<String>()
    private val _isLoading = MutableLiveData<Boolean>(true)
    val isLoading: LiveData<Boolean> get() = _isLoading
    private val _receptionState = MutableLiveData<Boolean>(true)
    val receptionState: LiveData<Boolean> get() = _receptionState
    private val _initState = MutableLiveData<Boolean>(true)
    val initState: LiveData<Boolean> get() = _initState
    private val _saveCategoryTitle = MutableLiveData<String>()
    val saveCategoryTitle: LiveData<String> get() = _saveCategoryTitle

    //칩 눌렀을 때 카테고리 별 영상
    fun fetchCategoryVideos(videoCategoryId: String) {
        viewModelScope.launch {
            runCatching {
                //api 통신 videos endPoint
                val videos = repository.fetchCategoryVideos(videoCategoryId = videoCategoryId)
                _categoryVideos.value = videos

                //channelIdList가 비어 있지 않으면 초기화 후 id추가
                if (channelIdList.isNotEmpty()) channelIdList.clear()
                videos.forEach { channelIdList.add(it.snippet.channelId) }

                //통신이 끝나면 loading 끝
                _isLoading.value = false

                //api 통신 채널 정보를 가져오는 코드 channels endPoint
                val channels = repository.getChannelInfo(channelIdList)
                _channel.value = channels

                //통신 상태 처리
                receptionState(true)
            }.onFailure {

                //api 통신 예외 발생시
                receptionFailed()
                receptionState(false)
            }
        }
    }

    //트렌딩 비디오 영상 초기 화면 셋팅용
    fun getTrendingVideos() {
        viewModelScope.launch {
            runCatching {
                val videos = repository.getTrendingVideos().items
                _trendVideos.value = videos
                _initState.value = false
            }.onFailure {
                receptionFailed()
                receptionState(false)
            }
        }
    }

    //카테고리의 id 받아오는 코드
    fun getCategoryIds() {
        viewModelScope.launch {
            runCatching {
                val ids = repository.getCategoryIds()
                _categoryIdList.value = ids
            }.onFailure {
                receptionFailed()
                receptionState(false)
            }
        }
    }

    //로딩 상태 처리
    fun loadingState(state: Boolean) {
        _isLoading.value = state
    }

    //api 통신 실패 처리
    private fun receptionFailed() {
        _categoryVideos.value = listOf()
        _channel.value = listOf()
        _isLoading.value = false
    }

    //통신 상태
    fun receptionState(state: Boolean) {
        _receptionState.value = state
    }

    //선택된 칩 위치를 기억 하기 위한 코드
    fun saveCategoryTitle(category: String) {
        _saveCategoryTitle.value = category
    }
}

 

728x90

loading 화면을 보여주기 위해 progressBar를 사용하려는데

 

사용법은 이렇게 적혀있었다. 

https://github.com/emreesen27/Android-Nested-Progress

 

settings.gradle.kts(root-Gradle)

?????

 

?????

출처 - https://jitpack.io/#emreesen27/Android-Nested-Progress

 

뭐가 문제인지 빨간줄이 떠서 여러가지 시도를 해보다가 안되서 결국 검색을 해봤다. 

 

해결 방법은 

 

이렇게 바꿔주고 Sync Project를 해주면 잘 된다.

 

 

 

사용 라이브러리 github 

https://github.com/emreesen27/Android-Nested-Progress

 

GitHub - emreesen27/Android-Nested-Progress: Nested Circular Progress Library For Android

Nested Circular Progress Library For Android. Contribute to emreesen27/Android-Nested-Progress development by creating an account on GitHub.

github.com

 

728x90

in 을 사용하다 보면 평소에 생각하지 않고 외운것 처럼 자연스럽게 사용 하다가도,가끔 헷갈릴 때가 있다. in 앞에 조건이 뒤 조건에 포함 되어있는건지 in 뒤에 조건이 앞에 조건에 포함되어 있는건지 그래서 기억하기 쉽게 정리를 해본다.

결론부터 얘기 하면, in 앞의 조건이 in (Range) 안에 있으면 true 아니면 false      

fun main() {
    var numList = listOf(1, 2, 3, 4, 5)
    var numList2 = listOf(1, 3, 5, 7, 9)
    
    if (3 in numList) {
        println("3은 numList 컬렉션에 포함되어 있습니다.")
    } else {
        println("3은 numList 컬렉션에 포함되어 있지 않습니다.")
    }
    //3은 numList 컬렉션에 포함되어 있습니다.
    
    val x = 5
    
    if (x in 1..10) {
        println("$x 는 1과 10 사이에 있습니다.")
    } else {
        println("$x 는 1과 10 사이에 없습니다.")
    }
    //5 는 1과 10 사이에 있습니다.
    
    println("filter -> 0과 6사이에 있는 numList2 ${numList2.filter { it in (0..6) }}")
    //filter -> 0과 6사이에 있는 numList2 [1, 3, 5]
   
    println("filter -> 0과 6사이에 없는 numList2 ${numList2.filter { it !in (0..6) }}")
 	//filter -> 0과 6사이에 없는 numList2 [7, 9]
   
    val string = "abcdefg"
    val string2 = "abc"
   
   
    if(string2 in string){
        println("$string2 는 $string 안에 포함 됩니다.")
    }else{
        println("$string2 는 $string 안에 포함 되지 않습니다.")
    }
    //abc 는 abcdefg 안에 포함 됩니다.
}
728x90

코틀린에서 val 과 var의 차이는 불변과 가변의 차이로 알고 있음,

val name = "name"

name = "KIM" //변경불가능 

var age = 10

age = 20 //변경가능 

val 은 Java의 final 과 비슷함 한 번 초기화 후에, 값이 변경이 불가능한데  array,list,set,map를 생성할때도 마찬가지로 읽기전용 listOf() , setOf() , mapOf() 등의 불변 함수, mutableListOf() , mutableSetOf() , mutableMapOf() 등과 같이 가변함수로 나누어져 있는데,

val list = mutableListOf(1,2,3)
list.add(4) // 4요소 추가
list.remove(1) // 1요소 제거
list[2] = 5 //2번째 index 값 5로 변경

val list = mutableListOf() 이 형태는 val 인데 어떻게 값이 변경이 될까 갑작스럽게 혼란스러움 

 

그래서 val 과 var 의 의미를 다시 생각해 보니 val 과 var 이 읽기 전용, 변경가능 이라는 말이, 값을 할당할 때 재할당이 가능하냐 불가능하냐로 이해하고 있고, list를 val 로 선언을 해도 값을 변경가능한 이유는 list변수 자체는 불변성이지만 변수가 참조하는 객체가 불변성을 가지는 것은 아니기 때문에, 선언할때  mutableListOf()..등 -> mutable(가변)이냐 listOf()..등 -> immutable(불변) 으로 생성해서 사용한다. (요소를 추가하고 제거하는 것은 list 변수 자체를 변경하는게 아님)   

 

다시 생각해보니 헷갈렸던 부분이 해소가됨

val list = mutableListOf(1,2,3)
list = mutableListOf(2,3,4) //당연하게도 val 로 선언한 list는 값을 재할당 할 수 없음
var varList = mutableListOf(1,2,3)
varList = mutableListOf(4,5,6) //재할당 가능

그렇다고 varList 를 listOf() immutable로 만들수는 없음 

 

 

 

 

+ Recent posts