<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>KEZ의 안드로이드</title>
        <link>https://velog.io/</link>
        <description>Android Developer</description>
        <lastBuildDate>Wed, 29 Jan 2025 14:22:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. KEZ의 안드로이드. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kej_ad" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android/Kotlin] ViewPager2로 Did not find frame 오류 때려 잡기(UI 버벅임 해결)]]></title>
            <link>https://velog.io/@kej_ad/AndroidKotlin-ViewPager2%EB%A1%9C-Did-not-find-frame-%EC%98%A4%EB%A5%98%EB%A5%BC-%EC%9E%A1%EA%B8%B0UI-%EB%B2%84%EB%B2%85%EC%9E%84-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@kej_ad/AndroidKotlin-ViewPager2%EB%A1%9C-Did-not-find-frame-%EC%98%A4%EB%A5%98%EB%A5%BC-%EC%9E%A1%EA%B8%B0UI-%EB%B2%84%EB%B2%85%EC%9E%84-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 29 Jan 2025 14:22:07 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 제가 최근에 마주쳤던 아주 난감한 오류 로그와, 그 문제를 해결하기 위해 겪었던 시련과 깨달음(?)을 공유해보려 합니다. </p>
<p>오늘 다룰 내용은 바로 <strong><code>updateAcquireFence: Did not find frame</code></strong> 라는 빨간색의 무시무시한(?) 로그입니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/b3cd6c24-f999-4f90-818b-0218e2f1d309/image.png" alt=""></p>
<p>개발하다 보면, 항상 새로운 도전이 나타나곤 하죠. 이번 도전은 평소엔 눈치 못챘었지만 오랜만에 앱을 키니까 갑자기 버벅거리는게 느껴지고 급히 로그캣을 확인하던 도중에 발견한 문제였죠</p>
<blockquote>
<p><strong>“updateAcquireFence: Did not find frame”</strong><br><strong>“FrameEvents: Did not find frame.”</strong><br><strong>“mReversing is false. Don’t call initChildren.”</strong><br><strong>“Skipped X frames! The application may be doing too much work on its main thread.”</strong></p>
</blockquote>
<p>처음엔 “아, 또 스레드 문제인가?” 싶은 마음에 UI 스레드나 무거운 로직을 의심했는데, 정작 로그를 자세히 뜯어보니 뭔가 <strong>렌더링 쪽에서 프레임을 놓쳐버렸다</strong>는 느낌이었습니다. 그리고 실제로도 탭 전환 애니메이션이 끊기고, 사용자 입장에서도 화면이 버벅이는 현상이 보였습니다. 앱 품질에도 영향을 주는 중요한 문제였죠.</p>
<hr>
<h2 id="문제-상황-bottomnavigationview와-fragment-전환">문제 상황: BottomNavigationView와 Fragment 전환</h2>
<p>앱의 주요 구조는 <code>BottomNavigationView</code>로 탭을 구성하고, 각 탭을 <code>FragmentTransaction</code>(주로 <code>replace()</code>)으로 전환하는 방식이었습니다.</p>
<ul>
<li><strong>로그 경고</strong>: <code>updateAcquireFence: Did not find frame</code> 같은 문구가 계속 뜸  </li>
<li><strong>프레임 드롭</strong>: 화면 전환 시 애니메이션이 자연스럽게 이어지지 못하고 끊김  </li>
<li><strong>사용자 경험 저하</strong>: “앱이 왠지 무겁고 버벅이는 것 같은데?”라는 느낌을 주게 됨  </li>
</ul>
<hr>
<h2 id="초반-의심-대상은-무거운-애니메이션-요소들---">초반 의심 대상은? 무거운 애니메이션 요소들 . . .</h2>
<p>“어쩌면 Home화면에 있는 거대한 Lottie View가 문제일 수도 있지 않을까?” 혹은 “ProgressBar 애니메이션이 뭔가 끼어들었나?” 등등, 흔히 생각할 수 있는 애니메이션 요소들을 하나씩 배제해보았습니다.</p>
<ul>
<li><p><strong>Lottie 제거</strong>  </p>
<ul>
<li>탭 전환 시 쓰던 Lottie를 다 빼고 테스트해봤지만, 여전히 로그가 뜨고 버벅임도 사라지지 않음.  </li>
</ul>
</li>
<li><p><strong>ProgressBar 애니메이션 제거</strong>  </p>
<ul>
<li>로딩 시 애니메이션 효과를 없앴는데도 변함없음.  </li>
</ul>
</li>
<li><p><strong>RecyclerView Adapter 정리</strong>  </p>
<ul>
<li>HomeFragment는 RecyclerView 구조로 되어있었기 때문에 <code>onDestroyView()</code>에서 제대로 정리되지 않은 Adapter가 문제일까 싶어 <code>null</code> 처리를 철저히 해봤지만 여전히 똑같은 로그가…</li>
</ul>
</li>
</ul>
<p>그야말로 ‘눈물의 배제법’이었습니다. 그런데도 문제가 전혀 나아지지 않아 답답하더군요.</p>
<hr>
<h2 id="🔎-profiler로-근본적인-프레임-버벅임의-원인을-찾아보자">🔎 Profiler로 근본적인 프레임 버벅임의 원인을 찾아보자</h2>
<p>여기서 저는 근본적인 원인을 분석해야한다 생각했고 이러한 분석이 가능한 도구가 바로 <strong>Android Studio의 Profiler</strong>이죠!!</p>
<p>실제로 렌더링 시간을 체크해보니, 특정 프레임이 <strong>예상(8.33ms)을 훌쩍 넘겨 100~200ms 이상</strong> 소요되고 있었습니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/f5391dcf-ef0c-436d-a0a0-79dbc6769e33/image.png" alt=""></p>
<p>아주 세세한 타임라인 이벤트를 보면 한 프레임에서 UI 스레드와 Render 스레드 모두 여러 작업(뷰 인플레이션, 이미지 디코딩, 텍스처 업로드 등)을 ‘몰아서’ 처리하고 있음을 알 수 있는데요</p>
<p>이러한 타임라인 이벤트를 스레드 별 로 요약하자면 다음과 같은 단계를 거치고 있는 상황입니다.</p>
<p><strong>1. 메인 스레드(Main)</strong></p>
<ul>
<li><code>RV CreateView / RV OnBindView</code>: RecyclerView에 대한 itemView 할당</li>
<li><code>inflate()</code> → ConstraintLayout 등 복잡한 레이아웃 인플레이션</li>
<li><code>AssetManager::OpenNonAsset, ImageDecoder#decodeDrawable</code>: 큰 이미지를 디코딩하여 화면에 쓸 수 있는 형태로 변환하는 작업</li>
<li><code>measure(), layout()</code>: 뷰의 크기를 계산하고, 계산된 크기대로 화면에서의 위치를 배치하는 과정</li>
</ul>
<p><strong>2. 렌더 스레드(Render)</strong></p>
<ul>
<li>대형 이미지를 Texture upload(1080×1290 등)로 GPU에 업로드</li>
<li>추가로 VectorDrawable, 더 작은 PNG들까지 연달아 업로드</li>
<li>업로드가 끝난 뒤에야 flush commands → 실제 그리기 마무리</li>
</ul>
<p><strong>이 모든 과정이 한 번의 프레임 안에 몰리면서 총 소요 시간이 Expected 8.33ms가 아니라 224.33ms까지 늘어나게 된 것입니다!!!</strong></p>
<h3 id="더-간단하게-얘기하자면">더 간단하게 얘기하자면</h3>
<ul>
<li><code>replace()</code> 함수로 <strong>새로운 Fragment</strong>를 생성하고 이전 Fragment를 삭제하는 과정에서 화면 전체가 다시 그려지는 수준의 비용이 발생</li>
<li>탭마다 <code>RecyclerView</code>나 무거운 뷰들이 잔뜩 있는 상황이라, 매번 전환 후 View 재생성 마다 큰 부담  </li>
</ul>
<p>즉, <strong>“매번 뷰를 새로 갈아끼우는 구조 자체가 문제”</strong>였던 겁니다.</p>
<h3 id="이로-인한-deadline-missed-발생">이로 인한 ‘Deadline missed’ 발생</h3>
<p>안드로이드에서는 <a href="https://www.sportsadda.com/esports/news-esports/bgmi-3-5-update-120-fps-support-android-ios-devices/">120fps 기기 기준</a>으로 <a href="https://www.xda-developers.com/smartphone-display-refresh-rates-explained/">8.33ms안에 한 프레임이 마무리돼야 매끄럽게 보이는데요,</a></p>
<p>현재 로그에는 224.33ms(약 0.22초)가 걸려 버렸으니 약 26배 이상 지연된 셈이며,
그로인해 “Skipped X frames!”라는 메시지가 뜨며 사용자가 버벅임을 느끼게 되는 것이죠</p>
<hr>
<h2 id="해결-방법-고르기-fragment-showhide-vs-viewpager2">해결 방법 고르기: <code>Fragment show/hide</code> vs <strong>ViewPager2</strong></h2>
<p>Profiler에서 원인을 찾은 뒤, 크게 두 가지 방법을 고민했습니다.</p>
<p>1) <strong><code>show/hide</code> 접근</strong>  </p>
<ul>
<li>이미 생성된 Fragment를 <code>show()</code> / <code>hide()</code>만 하여 재구성 비용 줄이기  </li>
<li>Fragment 하나당 인스턴스를 계속 유지하므로 탭에 따라 <strong>메모리 사용</strong>이 늘어날 수도 있음  </li>
<li><code>replace()</code> 대신 <code>add()</code>, <code>show()</code>, <code>hide()</code> 구조로 변경 시, 기존 코드가 많이 달라질 수 있음  </li>
</ul>
<p>2) <strong><code>ViewPager2 + FragmentStateAdapter</code></strong>  </p>
<ul>
<li>각 탭(Fragment)을 <strong>미리 로드</strong>하고, 필요한 시점에 <strong>화면만 전환</strong>  </li>
<li><strong>장점</strong>  <ul>
<li><code>FragmentStateAdapter</code>: 정리 가능한 Fragment의 뷰를 ‘메모리에서 날려’ 필요 시 다시 그려주지만, <strong>상태는 유지</strong>하므로 전환이 가벼움</li>
<li><strong>Lifecycle</strong>을 적절히 관리하며, 코드 구조가 깔끔해짐  </li>
<li><code>BottomNavigationView</code>와 쉽게 연동 가능 </li>
</ul>
</li>
<li><strong>단점</strong>  <ul>
<li>탭이 아주 많다면, 미리 만들어둔 Fragment 개수만큼 메모리에 부담이 될 수 있음  </li>
</ul>
</li>
</ul>
<h3 id="왜-viewpager2를-선택했을까">왜 <code>ViewPager2</code>를 선택했을까?</h3>
<ul>
<li><strong>유지보수성</strong>: <code>FragmentStateAdapter</code>를 사용하는 방식이 직관적이고, 화면 전환 로직도 단순해짐  </li>
<li><strong>상태 및 Lifecycle 관리</strong>: Fragment를 매번 새로 안 만들어도 되고, 필요 시 뷰만 다시 그려주는 구조라서 효율적인 Fragment 관리 체계를 위임할 수 있음(더 자세한 내용은 FragmentStateAdapter의 내부를 (예: MaxLifecycleEnforcer, store 매커니즘) 공부해보면 좋습니다.)</li>
<li><strong>하위 호환성</strong>: Jetpack 라이브러리라 여러 안드로이드 버전에서 안정적으로 동작  </li>
</ul>
<p>기존 <code>show/hide</code> 방식을 쓰면 각 Fragment를 계속 메모리에 띄워두는 형태가 되지만, <code>ViewPager2 + FragmentStateAdapter</code>는 <strong>OffscreenPageLimit에 따라 Fragment를 유지하고 적절히 뷰만 파기하고 다시 복원</strong>하기 때문에, 화면 수가 많아도 생각보다 부담이 적습니다.</p>
<hr>
<h2 id="viewpager2-적용-코드-예시">ViewPager2 적용 코드 예시</h2>
<p>아래는 실제로 적용한 예시 코드입니다.</p>
<pre><code class="language-kotlin"># MainActivity.kt
. . .
    private fun initViewPager() {
        binding.vpMain.apply {
            isUserInputEnabled = false
            adapter = MainAdapter(this@MainActivity)
            setCurrentItem(MainScreen.HOME.ordinal, false)

        }
    }

    private fun initBottomNavigation() {
        binding.bnvMain.selectedItemId = R.id.home_dest
        binding.bnvMain.setOnItemSelectedListener { menuItem -&gt;
            when (menuItem.itemId) {
                R.id.challenge_dest -&gt; binding.vpMain.setCurrentItem(
                    MainScreen.CHALLENGE.ordinal,
                    false
                )

                R.id.home_dest -&gt; binding.vpMain.setCurrentItem(
                    MainScreen.HOME.ordinal,
                    false
                )

                R.id.my_page_dest -&gt; binding.vpMain.setCurrentItem(
                    MainScreen.MY_PAGE.ordinal,
                    false
                )
            }
            true
        }
    }
. . .


# MainAdapter.kt

class MainAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
    override fun getItemCount(): Int = MainScreen.entries.size

    override fun createFragment(position: Int): Fragment {
        return when (MainScreen.fromPosition(position)) {
            MainScreen.CHALLENGE -&gt; ChallengeFragment()
            MainScreen.HOME -&gt; HomeFragment()
            MainScreen.MY_PAGE -&gt; MyPageFragment()
        }
    }
}</code></pre>
<p><a href="https://github.com/Team-HMH/HMH-Android/pull/288/files#diff-3bab0309c6e02ccec74ae94de2dc5907b411067c5b1e4f0369b1a7496d1f0708">Github PullRequset 링크</a></p>
<p>아주 간단히, <strong>ViewPager2</strong> 어댑터를 등록하고, <code>BottomNavigationView</code>와 연동했습니다. 이제 탭 전환 시 <code>replace()</code>를 매번 호출하지 않으니, 재인플레이션 때문에 발생하던 <strong>렌더링 과부하</strong>가 드라마틱하게 줄어들었습니다.</p>
<hr>
<h2 id="결론-결국-문제는-재생성-비용이었고-해결책은-fragment의-재생성-과정-생략과-상태-유지">결론: 결국 문제는 ‘재생성 비용’이었고, 해결책은 Fragment의 재생성 과정 생략과 상태 유지</h2>
<ul>
<li><strong>근본 원인</strong>: 매번 <code>replace()</code>로 Fragment를 새로 붙이는 구조가 탭 전환 시 큰 렌더링 비용을 야기  </li>
<li><strong>해결 포인트</strong>: Profiler로 화면 전환 시 렌더링이 얼마나 오래 걸리는지 ‘수치화’해보고, <strong>재생성 비용</strong>이 크다는 점을 확인  </li>
<li><strong>결과</strong>: Off-screen 범위 내에서 <code>ViewPager2</code>를 이용해 Fragment 상태를 유지하니까 탭 전환시마다 반복되던 아래 작업이 생략됨<ol>
<li>Fragment의 Add(), Remove() 과정의 반복 생략</li>
<li>inflate() 반복 과정의 생략</li>
</ol>
</li>
</ul>
<p><strong>따라서 로그 경고와 프레임 드롭이 없어짐</strong></p>
<p>이 과정을 통해, <strong>“문제를 회피하기보단 정면으로 들여다보자!”</strong>라는 개발의 기본 철학을 다시금 되새기게 되었습니다. 특히 Android Studio Profiler를 제대로 써본 덕분에 막연히 “뭐가 문제인지 모르겠다” 하고 있던 상태에서 벗어날 수 있었던 것 같습니다 ㅎ.ㅎ</p>
<blockquote>
<p>*<em>혹시 비슷한 로그나 프레임 드롭 문제에 부딪혔다면,  *</em></p>
<p>1) 전환 방식(특히 <code>replace()</code> 연쇄 호출)을 의심해볼 것<br>2) Profiler로 실제 ‘렌더링 타임’을 체크해볼 것<br>3) 필요한 경우 <strong>ViewPager2</strong>나 <code>show/hide</code> 방식을 고려해볼 것  </p>
</blockquote>
<hr>
<h3 id="한마디로-정리">한마디로 정리</h3>
<blockquote>
<p><strong>“🥵 ViewPager2로 갈아타고 나니, 그 지긋지긋한 ‘Did not find frame’ 로그가 감쪽같이 사라지더라!”</strong></p>
</blockquote>
<p>물론, 모든 상황에 <strong>100%</strong> 통하는 만능 해결책은 아니지만, 탭 간 전환 시 무거운 UI를 자주 쓰는 구조라면 큰 도움이 될 것입니다. </p>
<p>이번 글에서는 시행착오를 거쳐서라도 근본 원인을 찾아내었고 이를 통해서 근본적인 해결책을 발견하여 해결했던 경험으로 꽤나 어렵고 복잡하고 시간 걸리는 일이였던 것 같습니다 ㅠ.ㅠ</p>
<p>개발은 늘 새로운 문제와의 싸움이자, 동시에 새로운 배움의 기회라고 생각합니다 하핳 부디 여러분도 비슷한 문제로 고생 중이라면 이 글이 많은 힌트가 되시길 바라면서 저는 이만!</p>
<p><strong>긴 글 읽어주셔서 감사합니다.</strong></p>
<hr>
<h2 id="글-마무리에-추가-팁">글 마무리에 추가 팁</h2>
<ul>
<li><p><strong>구현 시 주의점</strong>:  </p>
<ul>
<li>ViewPager2로 전환할 때, 기존 Fragment Transaction 코드(<code>replace()</code>, <code>add()</code>, <code>remove()</code>)를 <strong>함께</strong> 쓰지 않도록 구조를 정리해야 합니다.  </li>
<li>중복으로 Fragment가 붙거나, 예기치 않은 버그가 생길 수 있습니다. (바보짓으로 30분 날린 사람 여깄슴다)</li>
</ul>
</li>
<li><p><strong>Profiler 사용 습관</strong>:  </p>
<ul>
<li>“Profiler는 느려질 때만 쓴다” 라기 보다는, 무거운 작업이 배포되는 경우 일정 주기로 성능 체크를 해두면 나중에 문제 터질 때 쉽게 비교할 수 있다고는 생각합니다.</li>
<li>다만 너무 잦은 분석에 빠진다면 정작 개발할 시간을 뺏길수도? 적적한 주기를 팀에서 정하는 것이 좋을 것 같다고 생각됩니다. <strong>(정답은 없음)</strong></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ KMP / CMP ] Compose Multiplatform(Wasm)에서 한글 깨짐 현상 방지하기 - 커스텀 폰트 적용]]></title>
            <link>https://velog.io/@kej_ad/Compose-MultiplatformWasm%EC%97%90%EC%84%9C-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%98%84%EC%83%81-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%8F%B0%ED%8A%B8-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@kej_ad/Compose-MultiplatformWasm%EC%97%90%EC%84%9C-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%98%84%EC%83%81-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%8F%B0%ED%8A%B8-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Fri, 29 Nov 2024 11:11:14 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 오늘은 Compose Multiplatform으로 간단한 샘플앱을 만들던 도중 한글이 깨지는 상황이 발생하여 이를 어떻게 해결했는지를 간단하게 남겨보고자 포스팅을 시작합니다.
관련 Repository: <a href="https://github.com/kez-lab/KotlinRPCSample">https://github.com/kez-lab/KotlinRPCSample</a></p>
<h2 id="서론">서론</h2>
<p>저는 간단하게 퀴즈 앱을 만들어보고자 아래와 같이 Title, Button 형태의 Screen을 제작하게 되었습니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/f506ffd5-83e8-45e9-9aa1-6ebf0b567f14/image.png" width="50%" height="50%"></p>
<p>이 과정에서 한글이 위 사진처럼 X박스의 형태로 깨진다는 것을 확인하였고, 아직 알파버전인 Wasm이기에 Wasm자체에 문제라는 것을 직감하고 해결방법을 찾아나섰습니다.</p>
<p>근본적인 이슈의 이유는 잘 모르겠지만, <strong>한글을 지원하는 폰트가 아닌 경우에 한글이 깨진다는 것을 알게되었으며</strong> 이를 통해서 커스텀 폰트를 적용한다면 한글 깨짐 에러를 잡을 수 있다는 것을 확인했습니다.</p>
<h3 id="원인-파악-시-참고한-자료">원인 파악 시 참고한 자료</h3>
<blockquote>
<p>참고 자료는 유광무님과 김만두님의 블로그를 참고하였습니다.
<a href="https://holykisa.tistory.com/117">https://holykisa.tistory.com/117</a>
<a href="https://kimmandooo.tistory.com/172">https://kimmandooo.tistory.com/172</a></p>
</blockquote>
<h2 id="해결-방법">해결 방법</h2>
<p>해결 방법은 Compose Multiplatform 1.7.0이 나오게 되면서 굉장히 간단해졌습니다.</p>
<h3 id="1-기본-준비">1. 기본 준비</h3>
<p>Compose Multiplatform 프로젝트에서 커스텀 폰트를 사용하려면 compose.components.resources 라이브러리가 필요합니다.</p>
<p>composeApp 모듈의 build.gradle.kts 파일에 아래 종속성을 추가합니다.</p>
<pre><code class="language-kotlin">commonMain.dependencies {
    implementation(compose.components.resources)
}
</code></pre>
<h3 id="2-otf-ttf-파일-넣기">2. *.otf, ttf 파일 넣기</h3>
<p>아래 사진처럼 composeApp의 commonMain 모듈 내에서 composeResources/font라는 디렉터리 내에 폰트 파일을 넣습니다.
저는 한글을 지원하는 폰트인 IBMPlexSansKR 를 사용했습니다! (Google Font에서 다운받았습니다.)
<img src="https://velog.velcdn.com/images/kej_ad/post/f7dbaeba-ecca-438b-88d2-10e240c99734/image.png" alt=""></p>
<h3 id="3-build-후-res-class-참조">3. build 후 Res Class 참조</h3>
<p>build 를 진행하면 composeApp 모듈 내에 generated/. . . /commonMainResourceAccessors 내에 Font0... 라는 클래스 파일이 생성 된 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/bdd8adf7-c084-437a-898a-81f9ac2f567f/image.png" width="70%" height="70%"></p>
<p>해당 클래스 내부를 확인해보면 <em><strong>IBMPlexSansKR_Medium</strong></em> 라는 변수가 생성된 것을 볼 수 있는데요. 해당 변수를 참조하여 Font객체를 생성할 수 있습니다.</p>
<pre><code class="language-kotlin">private object CommonMainFont0 {
  public val IBMPlexSansKR_Medium: FontResource by 
      lazy { init_IBMPlexSansKR_Medium() }
}

@InternalResourceApi
internal fun _collectCommonMainFont0Resources(map: MutableMap&lt;String, FontResource&gt;) {
  map.put(&quot;IBMPlexSansKR_Medium&quot;, CommonMainFont0.IBMPlexSansKR_Medium)
}

internal val Res.font.IBMPlexSansKR_Medium: FontResource
  get() = CommonMainFont0.IBMPlexSansKR_Medium

private fun init_IBMPlexSansKR_Medium(): FontResource =
    org.jetbrains.compose.resources.FontResource(
  &quot;font:IBMPlexSansKR_Medium&quot;,
    setOf(
      org.jetbrains.compose.resources.ResourceItem(setOf(),
    &quot;composeResources/kotlinrpcsample.composeapp.generated.resources/font/IBMPlexSansKR-Medium.ttf&quot;, -1, -1),
    )
)
</code></pre>
<h3 id="4-fontfamily-정의-및-theme에-적용">4. FontFamily 정의 및 Theme에 적용</h3>
<p>Compose Multiplatform에서는 Res.font를 통해 폰트를 로드할 수 있으며, 이를 FontFamily로 변환하여 사용할 수 있습니다.</p>
<p>여기서 주의사항은 꼭 Font객체에 대한 import를 <strong>org.jetbrains.compose.resources.Font</strong> 로 참조해야합니다.</p>
<p>만약 androidx.compose.ui.text.font.Font 로 참조하게 된다면 FontResource객체를 인자 값으로 활용하지 못하기 때문에 Res.font를 활용할 수 없습니다.</p>
<pre><code class="language-kotlin">
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontFamily
import kotlinrpcsample.composeapp.generated.resources.IBMPlexSansKR_Medium
import kotlinrpcsample.composeapp.generated.resources.Res
import org.jetbrains.compose.resources.Font

@Composable
fun QuizTheme(content: @Composable () -&gt; Unit) {
    val font = Font(Res.font.IBMPlexSansKR_Medium)
    MaterialTheme(
        typography = Typography(
            FontFamily(font)
        ),
        content = content
    )
}</code></pre>
<p>저는 QuizTheme라는 새로운 테마 Composable을 적용하여 Typography의 FontFamily로 IBMPlexSansKR_Medium을 적용한 것을 알 수 있습니다.</p>
<h2 id="결과">결과</h2>
<p>결과물을 보시면 아시다시피 한글이 굉장히 이쁘게 나오는 것을 알 수 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/060e8890-6cce-4d14-8917-1b8fed6f3a6f/image.png" alt=""></p>
<p>오늘도 역시 CMP 덕분에 다사다난한 하루를 보내게 되었지만 매번 발전해나가는 Jetbrains 덕분에 다행히 발뻗고 잘 수 있을 것 같네요 하핳 감사합니다!</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-resources-usage.html#fonts">https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-resources-usage.html#fonts</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android ViewModel에서 SaveStateHandle을 활용하기]]></title>
            <link>https://velog.io/@kej_ad/3%EB%85%84%EC%B0%A8-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B0%98%EC%84%B1%EA%B8%B0Android-ViewModel%EC%97%90%EC%84%9C-SaveStateHandle%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/3%EB%85%84%EC%B0%A8-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B0%98%EC%84%B1%EA%B8%B0Android-ViewModel%EC%97%90%EC%84%9C-SaveStateHandle%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 17 Oct 2024 03:33:42 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 오늘 도입은 조금 제 부끄러운 개발 얘기를 써내려볼까 합니다...</p>
<p>근래에 새로운 앱의 Android 개발을 하면서 Paging3 Codelab을 참고하여 개발을 진행했었습니다. 저는 Paging3 기술에 대해 잘 알지 못했기에 <a href="https://developer.android.com/codelabs/android-paging?hl=ko#0">Android Codelab</a>을 참고해서 개발하게 되었습니다.</p>
<p>그런 과정에서 저는 아래 코드를 그대로 들고 오게되었습니다.</p>
<h3 id="paging-sample-code">Paging Sample Code</h3>
<pre><code class="language-kotlin">class SearchRepositoriesViewModel(
    private val repository: GithubRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel()

    init {
        val queryLiveData =
            MutableLiveData(savedStateHandle.get(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY)</code></pre>
<p>여기서 저는 savedStateHandle 을 통해 시스템에 의한 프로세스 종료시 데이터를 복구할 수 있다는 개념적인 의미는 알고있었지만 이를 실제로 테스트해보지는 않고 방어로직 차원에서만 추가한다는 생각으로 제 실제 프로젝트에 위 코드를 그대로 넣어두었습니다.</p>
<h3 id="😐-무언가-잘못된-걸-깨달았다">😐 무언가 잘못된 걸 깨달았다.</h3>
<p>그리고 프로젝트 완성 후 제 코드를 보신분이 질문을 하시더라고요. </p>
<blockquote>
<p>Q: 왜 savedStateHandle 을 넣으셨나요? 
A: 음 시스템에 의한 프로세스 종료 시 데이터를 보존하기 위해서 입니다.</p>
</blockquote>
<blockquote>
<p>Q: 그러면 시스템에 의한 프로세스 종료에 대한 테스트는 어떻게 진행해보셨나요?
A: 엇.. 그 부분은 잘 모르겠습니다...</p>
</blockquote>
<p>여기서 저는 <strong>앗.. 뭔가 잘못되었다</strong> 라는 걸 느꼈습니다. 저는 테스트도 해보지 않고 이럴 것 같은데? 라는 짐작만으로 코드를 여기에 넣게 된 것입니다.</p>
<p>그냥 그럴 것 같은데? 라는 짐작만으로 어떤 코드를 넣는다는 사실이 굉장히 창피하고 반성해야겠다는 생각을 하며... 오늘 이 글을 쓰게 되었습니다.</p>
<p>이를 통해 앞으로는 똑같은 실수를 반복하지 않아야겠죠? 자 그러면 이 실수를 기회로 삼아 ViewModel에서 SaveStateHandle이란 무엇인지, 테스트는 어떻게 해야하는건지 확실하게 알아보는 시간을 가져보겠습니다.</p>
<h2 id="viewmodel에서-savestatehandle">ViewModel에서 SaveStateHandle</h2>
<p>개발자는 사용자의 원활한 경험을 보장하기 위해서는 애플리케이션이 비정상적으로 종료되거나 구성 변경이 발생하더라도 UI 상태를 적절히 저장하고 복원하는 것이 중요합니다. 특히 현대의 Android 애플리케이션은 복잡한 UI 상태와 데이터를 관리해야 하는 일이 자주 발생하죠 😥</p>
<p>그렇기에 대부분 <strong>ViewModel을</strong> 활용하여 데이터의 상태를 보존하고, 액티비티나 프래그먼트의 생명주기 변화(예: 화면 회전)에도 데이터를 쉽게 유지합니다.</p>
<p>하지만 시스템이 앱 프로세스를 종료한 후에도 상태를 유지하고 복원하려면 조금 더 복잡한 <strong>SavedStateHandle API</strong>를 사용해야 하는데요, 이 글에서는 Android ViewModel에서 SavedStateHandle을 사용하는 방법과 그 이점을 알아보겠습니다.</p>
<h3 id="ui-상태-저장-및-복원의-필요성">UI 상태 저장 및 복원의 필요성</h3>
<h4 id="사용자-경험-유지">사용자 경험 유지</h4>
<ul>
<li><strong>중단 없는 경험 제공</strong>: 사용자가 앱 사용 중 전화 수신, 앱 전환 등의 이유로 앱이 백그라운드로 전환될 수 있습니다. 이때 앱이 다시 활성화되었을 때 이전 상태를 그대로 복원하면 사용자 경험이 향상됩니다.</li>
<li><strong>데이터 손실 방지</strong>: 입력 중이던 데이터나 진행 중이던 작업이 초기화되지 않도록 상태를 저장하여 데이터 손실을 방지할 수 있습니다.<h4 id="다양한-종료-시나리오-대응">다양한 종료 시나리오 대응</h4>
</li>
<li><strong>시스템에 의한 프로세스 종료</strong>: 메모리 부족 등으로 인해 시스템이 앱 프로세스를 종료할 수 있습니다.</li>
<li><strong>구성 변경(Configuration Changes)</strong>: 화면 회전, 다크 모드 전환, 언어 변경 등으로 인해 Activity나 Fragment가 재생성됩니다.</li>
</ul>
<h3 id="activity의-종료-그리고-상태-저장-및-복원-시기">Activity의 종료 그리고 상태 저장 및 복원 시기</h3>
<p>Android 시스템은 아래 공식문서에서 안내하는 것과 같이 메모리 부족과 같은 이유로 앱의 프로세스를 종료할 수 있습니다. </p>
<blockquote>
<p>다만 시스템은 메모리 공간을 확보하기 위해 절대 Activity를 직접 종료하지 않습니다. 대신 Activity가 실행되는 프로세스를 종료하여 프로세스에서 실행되는 컴포넌트들을 간접적으로 종료시킵니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/5b85b4a7-3132-4ac3-9cba-544961b47b63/image.png" alt=""></p>
<p><a href="https://developer.android.com/guide/components/activities/activity-lifecycle.html?hl=ko#asem">https://developer.android.com/guide/components/activities/activity-lifecycle.html?hl=ko#asem</a></p>
<p>이렇게 시스템에 의해 Activity가 종료된 경우 사용자가 이전에 보던 화면이나 입력했던 데이터를 그대로 복원하지 않는다면 사용자는 자신이 입력했던 정보를 처음 부터 쓰는 <strong>극악의 사용자 경험</strong>을 선사해주게 되겠죠? </p>
<h3 id="savedstatehandle-이란">SavedStateHandle 이란?</h3>
<p>SavedStateHandle은 키-값 형태의 맵으로 데이터를 저장하며, Activity 또는 Fragment가 &quot;onSaveInstanceState()&quot; 메서드를 호출할 때 상태를 저장하며, 이후 프로세스가 다시 시작될 때 해당 상태를 복원합니다. </p>
<p>이를 통해 시스템이 프로세스를 종료하더라도, SavedStateHandle에 저장된 데이터는 일부 상태에서 살아남아 재사용될 수 있습니다.</p>
<blockquote>
<p>다만 사용자가 <strong>앱을 강제로 종료하거나 Task에서 스택이 제거되는 경우에는 상태가 저장되지 않으므로</strong>, 이 점을 고려하여 중요한 데이터는 로컬 저장소를 통해 관리하는 것이 필요합니다.</p>
</blockquote>
<h3 id="savedstatehandle을-이용한-상태-저장복원-구현하기">SavedStateHandle을 이용한 상태 저장/복원 구현하기</h3>
<p>SavedStateHandle은 ViewModel에서 상태를 저장하고 복원하는 데 사용되는 키-값 맵 형태의 데이터 구조입니다. 이를 통해 구성 변경뿐만 아니라 시스템에 의해 프로세스가 종료된 후에도 상태를 안전하게 복원할 수 있습니다. </p>
<h4 id="기본-설정">기본 설정</h4>
<p>Fragment 1.2.0 또는 Activity 1.1.0부터 ViewModel의 생성자에서 SavedStateHandle을 직접 사용할 수 있습니다. SavedStateHandle을 사용하는 기본 예제는 다음과 같습니다.</p>
<pre><code class="language-kotlin">class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    // 상태 저장 및 복원 로직 구현
}

class MainFragment : Fragment() {
    val viewModel: SavedStateViewModel by viewModels()
}</code></pre>
<p>위와 같이 ViewModel의 생성자에 SavedStateHandle을 추가하면, ViewModelProvider는 자동으로 적절한 SavedStateHandle을 제공합니다. </p>
<p>만약 위 명시된 라이브러리의 이전 버전을 사용하신다면 <a href="https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate#setup">공식문서</a>를 참고해주세요</p>
<blockquote>
<p><strong>참 쉽죠?</strong> 하핳 쉽다고 그냥 코드를 넣으신다면 안됩니다... 이유를 알고 테스트를 해본 이후에 필요성을 느끼고 도입해야합니다. - 자기반성중 - </p>
</blockquote>
<h3 id="상태-저장-및-복원-예제">상태 저장 및 복원 예제</h3>
<p>다음은 StateFlow와 함께 SavedStateHandle을 사용하는 예제입니다.</p>
<pre><code class="language-kotlin">class SavedStateViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _sampleText = MutableStateFlow(savedStateHandle.get&lt;String&gt;(&quot;sample_text&quot;) ?: &quot;&quot;)
    val sampleText: StateFlow&lt;String&gt; = _sampleText

    // sample text를 입력 시 savedStateHandle에 저장
    fun setSampleText(text: String) {
        _sampleText.value = text
        savedStateHandle[&quot;sample_text&quot;] = text
    }
}</code></pre>
<p>위 코드에서는 사용자가 입력한 검색어를 SavedStateHandle에 저장하고, 이를 이용해 필터링된 데이터를 가져옵니다. 이렇게 하면 시스템이 종료되었다가 다시 시작되더라도 동일한 검색 결과를 사용자에게 제공할 수 있습니다.</p>
<p>또한 SavedStateHandle은 LiveData나 Compose의 State API와 같은 관찰 Observable한 데이터 홀더와 함께 사용할 수 있습니다. 이를 통해 UI와의 상호작용 시 상태를 더욱 편리하게 관리할 수 있다고 하네요!!</p>
<h3 id="savedstatehandle-사용-시-유의사항">SavedStateHandle 사용 시 유의사항</h3>
<ul>
<li><p>SavedStateHandle에 저장되는 <strong>데이터는 단순하고 가벼워야 하며,</strong> 복잡하거나 큰 데이터는 로컬 저장소(예: Room 데이터베이스)를 통해 관리하는 것이 좋습니다. </p>
</li>
<li><p>SavedStateHandle은 <strong>Activity가 stop될 때 데이터를 저장하므로, stop 상태 이후 추가적으로 데이터를 변경하더라도 저장되지 않을 수 있습니다.</strong> 따라서 추가적인 데이터 변경이 필요한 경우, Activity가 start 상태가 될 때까지 기다린 후 저장하는 것이 필요합니다.</p>
</li>
</ul>
<h3 id="테스트-방법">테스트 방법</h3>
<p>시스템에 의한 Activity가 종료되는 케이스를 테스트하는 방법이 바로 <strong>개발자 모드 &gt; (액티비티)활동 유지 안함 옵션을 활성화</strong> 하면 된다는 것을 알았습니다!</p>
<img src="https://velog.velcdn.com/images/kej_ad/post/a9e7f655-8628-4bf7-985d-2f4af763030f/image.jpeg" width="50%" height="50%">

<p>이 옵션을 켜보고 테스트 해보니 꽤 대다수의 앱들이 이미 시스템에 의한 프로세스 종료에 대응 중인 것을 확인하고 <strong>아직도 저는 너무 많이 부족하다는 것을 깨닫게 되었습니다.</strong></p>
<blockquote>
<p>참고로 신기한 부분이 카카오톡에서 위 옵션을 키고 사진 첨부시에는 액티비티 유지안함 옵션을 꺼달라는 공지 팝업을 띄우고 있네요.</p>
</blockquote>
<h3 id="결론">결론</h3>
<p>결론적으로 ViewModel과 SavedStateHandle을 함께 사용하면 Configuration Change와 프로세스 종료에도 사용자에게 일관된 상태를 제공하여 더 나은 사용자 경험을 제공한다는 것을 깨달았습니다.</p>
<p>아! 그리고 제가 작성한 코드는 실제로 시스템에 의한 프로세스 종료시 복구가 안되더라고요...</p>
<p>맨날 말로만 사용자 경험<del>~ 어쩌구</del> 하다가 이런 예외를 만나게 되니 굉장히 부끄러워 지고 많이 배우게 된 하루였습니다.</p>
<p>아무튼 내일도 다들 즐거운 개발 화이팅하시길 바라겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Library] 안드로이드 라이브러리(SDK) Maven Central에 배포하기 (2024-08 최신)]]></title>
            <link>https://velog.io/@kej_ad/AndroidLibrary%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%ACSDK-Maven-Central%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-2024-08-%EC%B5%9C%EC%8B%A0</link>
            <guid>https://velog.io/@kej_ad/AndroidLibrary%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%ACSDK-Maven-Central%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-2024-08-%EC%B5%9C%EC%8B%A0</guid>
            <pubDate>Thu, 22 Aug 2024 13:10:36 GMT</pubDate>
            <description><![CDATA[<h1 id="안드로이드-라이브러리-maven-central에-배포하기">안드로이드 라이브러리 Maven Central에 배포하기</h1>
<p>안드로이드 개발자로서, 오픈 소스 라이브러리를 Maven Central에 배포하는 것은 다른 개발자들과 코드를 공유하고 사용성을 넓히는 중요한 과정입니다. 이 글에서는 GitHub 계정을 활용해 Android 라이브러리를 Maven Central에 배포하는 방법을 단계별로 자세히 설명합니다.</p>
<p>예시 Git Repository 주소: <a href="https://github.com/kez-lab/Compose-DateTimePicker">https://github.com/kez-lab/Compose-DateTimePicker</a></p>
<h2 id="사전-준비">사전 준비</h2>
<p>먼저 Maven Central에 안드로이드 라이브러리를 배포하려면 몇 가지 사전 준비가 필요합니다. 특히 2024년 상반기에 issues.sonatype.org가 폐기되면서 새로운 사용자는 Maven Central Portal을 통해 라이브러리를 배포해야 합니다.
<a href="https://central.sonatype.org/faq/what-happened-to-issues-sonatype-org/">https://central.sonatype.org/faq/what-happened-to-issues-sonatype-org/</a></p>
<ul>
<li><p><strong>vanniktech Maven Publish Plugin</strong>: Maven에는 현재 현재 Central Publishing Portal을 통해 Maven Central에 게시하기 위한 공식 Gradle 플러그인은 없습니다. <a href="https://central.sonatype.org/publish/publish-portal-gradle/#uploading-a-deployment-bundle">관련 링크</a></p>
</li>
<li><p>또한 저는 <a href="https://cocoslime.github.io/blog/Android-Library-Maven-Central/">김동민님의 안드로이드 라이브러리 Maven Central 에 배포하기</a>를 참고해서 라이브러리 배포를 진행했으며, 동민님께서 사용하신 것 처럼 <code>vanniktech</code> 플러그인을 사용하여 Maven Central에 배포할 예정입니다!</p>
</li>
</ul>
<h2 id="maven-central-계정-생성">Maven Central 계정 생성</h2>
<p>먼저 <a href="https://central.sonatype.com/">Maven Central Portal</a>에서 계정을 생성해야합니다. 이 포털은 Maven Central에 라이브러리를 배포하기 위한 계정 관리와 네임스페이스 검증, 그리고 토큰 생성 등을 지원합니다.</p>
<h2 id="namespace-생성-및-검증">Namespace 생성 및 검증</h2>
<ol>
<li><strong>Add Namespace</strong> 버튼을 클릭하여 네임스페이스를 생성합니다. 네임스페이스는 보통 GitHub 계정에 기반하며, 예를 들어 GitHub 계정이 <code>example</code>이라면 <code>io.github.example</code>로 생성합니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/06e2db67-e426-4cee-8bd5-e41725bb0a4f/image.png" alt=""></li>
</ol>
<blockquote>
<p>(Maven Central Portal 회원가입을 Github 소셜로그인으로 이미 진행했다면 github의 namespcae가 자동으로 검증됩니다.)</p>
</blockquote>
<ol start="2">
<li>생성된 <code>Unverified Namespace</code>를 검증하기 위해, 포털에서 제공하는 Verification Key를 GitHub에 동일한 이름의 공개 레포지토리로 생성해야합니다.</li>
</ol>
<blockquote>
<p>즉, Verification Key가 kezlab01이라면 kezlab01 이름의 레포지토리를 만들어야 합니다. 그러면 github.com/sample/kezlab01이라는 URL이 생성됩니다.</p>
</blockquote>
<ol start="3">
<li>GitHub 레포지토리의 URL이 유효해지면, Maven Central 포털에서 <code>Verify Namespace</code> 버튼을 클릭해 검증을 완료합니다. 검증 후에는 레포지토리를 삭제해도 됩니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/79e96b9f-16ae-4be5-b4a1-b32a673d1e59/image.png" alt="namespcae"></li>
</ol>
<blockquote>
<p>사진을 보시면 아시겠지만 저는 github NameSpace뿐만 아니라 개인용 도메인도 연결해두었는데요, 개인용 도메인 연결 방법 및 private 라이브러리 배포 방법은 추후 포스팅하겠습니다.</p>
</blockquote>
<h2 id="gpg-키-생성-및-관리">GPG 키 생성 및 관리</h2>
<p>Maven Central에 라이브러리를 배포하기 위해서는 GPG 키가 필요합니다. 이 키는 라이브러리가 정상적으로 서명되었음을 보증하며, 사용자들이 서명을 검증할 수 있도록 공개 키 서버에 업로드됩니다.</p>
<h3 id="1-gpg-키-생성">1. GPG 키 생성</h3>
<p>먼저, GPG 키를 생성합니다. 다음 명령어를 사용하여 키 생성 프로세스를 시작합니다.</p>
<pre><code class="language-bash">$ gpg --full-generate-key</code></pre>
<p>이 명령어를 실행하면, 이름과 이메일을 입력하라는 메시지가 나타납니다. 또한, 키의 유효 기간을 설정할 수 있습니다.</p>
<p><strong>예시 출력:</strong></p>
<pre><code>gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

GnuPG needs to construct a user ID to identify your key.

Real name: Your Name
Email address: youremail@example.com
You selected this USER-ID:
    &quot;Your Name &lt;youremail@example.com&gt;&quot;

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O</code></pre><p>키 생성이 완료되면, 아래와 같은 메시지가 표시됩니다:</p>
<pre><code>public and secret key created and signed.

pub   rsa3072 2024-06-23 [SC] [expires: 2026-06-23]
      ABCD1234EF5678901234567890ABCDEF12345678
uid                      Your Name &lt;youremail@example.com&gt;
sub   rsa3072 2024-06-23 [E] [expires: 2026-06-23]</code></pre><h3 id="2-gpg-키-확인">2. GPG 키 확인</h3>
<p>생성된 GPG 키를 확인하기 위해 다음 명령어를 사용합니다:</p>
<pre><code class="language-bash">$ gpg --list-keys</code></pre>
<p><strong>예시 출력:</strong></p>
<pre><code class="language-bash">/home/youruser/.gnupg/pubring.kbx
---------------------------------
pub   rsa3072 2024-06-23 [SC] [expires: 2026-06-23]
      ABCD1234EF5678901234567890ABCDEF12345678
uid           [ultimate] Your Name &lt;youremail@example.com&gt;
sub   rsa3072 2024-06-23 [E] [expires: 2026-06-23]</code></pre>
<p>여기서 pub의 마지막 8자인 12345678이 publick key입니다.</p>
<h3 id="3-gpg-키-서버에-업로드">3. GPG 키 서버에 업로드</h3>
<p>다른 사람들이 위 public key를 사용할 수 있도록 GPG public key를 key server에 업로드합니다.</p>
<pre><code class="language-bash">$ gpg --keyserver keyserver.ubuntu.com --send-keys 12345678</code></pre>
<p>참고로 GPG key를 서버에 업로드하지 않는다면 다음과 같이 배포시에 에러가 발생하니 참고해주세요.</p>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/aaee9a67-6890-4041-a62d-c67d029e2b86/image.png" alt=""></p>
<blockquote>
<p>간혹 key server의 host가 없다는(gpg: keyserver send failed: No route to host) 등의 어이없는 오류가 발생하기도 하는데요, 그럴 경우 아래의 명령어를 통해 연결 가능한 key server를 찾아 해당 도메인으로 public key 를 전송하시면 됩니다.</p>
</blockquote>
<pre><code class="language-bash">$ gpg -connect-agent --dirmngr &#39;keyserver --hosttable&#39;</code></pre>
<h3 id="4-gpg-키-서명-파일-생성">4. GPG 키 서명 파일 생성</h3>
<p>vanniktech 플러그인에서는 secretKeyRingFile을 통해서 GPG를 검증하여 서명에 사용하기 때문에 다음처럼 서명 파일을 생성해주셔야합니다.</p>
<pre><code class="language-bash">gpg --keyring secring.gpg --export-secret-keys &gt; ~/.gnupg/secring.gpg</code></pre>
<h2 id="빌드-설정-buildgradlekts">빌드 설정 (build.gradle.kts)</h2>
<p>Maven Central에 라이브러리를 배포하려면 <code>build.gradle.kts</code> 파일에서 적절한 설정을 추가해야 합니다. 이 과정은 플러그인 적용, Maven Central에 대한 설정, 그리고 POM 구성으로 나눌 수 있습니다.</p>
<h3 id="1-플러그인-적용">1. 플러그인 적용</h3>
<p>Maven Central에 라이브러리를 배포하기 위해 <code>vanniktech Maven Publish</code> 플러그인을 적용합니다. 이 플러그인은 Gradle 프로젝트의 아티팩트를 쉽게 Maven Central에 배포할 수 있도록 돕습니다.</p>
<pre><code class="language-kotlin">plugins {
  id(&quot;com.vanniktech.maven.publish&quot;) version &quot;0.29.0&quot;
}</code></pre>
<h3 id="2-maven-central-설정">2. Maven Central 설정</h3>
<p>플러그인을 적용한 후, Maven Central로 배포를 활성화하려면 대상 저장소를 지정하고 GPG 서명을 활성화해야 합니다. 이는 Maven Central에서 요구하는 필수 사항입니다. 이 설정은 DSL을 통해 추가하거나 Gradle 속성을 설정하여 추가할 수 있습니다.</p>
<pre><code class="language-kotlin">import com.vanniktech.maven.publish.SonatypeHost

mavenPublishing {
  // 기본 Maven Central로 배포
  publishToMavenCentral(SonatypeHost.DEFAULT)

  // 또는 https://s01.oss.sonatype.org로 배포
  publishToMavenCentral(SonatypeHost.S01)

  // 또는 https://central.sonatype.com/를 사용하는 Central Portal로 배포
  publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

  // 모든 배포에 대해 GPG 서명 활성화
  signAllPublications()
}</code></pre>
<p>위의 설정에서 SonatypeHost.DEFAULT, SonatypeHost.S01, SonatypeHost.CENTRAL_PORTAL 중 적절한 옵션을 선택하여 배포할 호스트를 지정할 수 있습니다. signAllPublications() 메서드는 모든 배포 아티팩트에 대해 GPG 서명을 적용합니다.</p>
<h3 id="3-pom-구성">3. POM 구성</h3>
<p>POM(프로젝트 객체 모델) 파일은 프로젝트의 메타데이터를 포함하며, 배포 시 함께 게시됩니다. 이 메타데이터는 라이브러리를 소비할 때 사용되는 프로젝트의 좌표(그룹 ID, 아티팩트 ID, 버전)와 프로젝트의 기본 정보(프로젝트 이름, 설명, URL, 라이선스 등)를 나타냅니다.</p>
<pre><code class="language-kotlin">mavenPublishing {
    // 프로젝트의 그룹 ID, 아티팩트 ID, 버전 설정
    coordinates(&quot;io.github.kez-lab&quot;, &quot;compose-datepicker&quot;, &quot;0.0.1&quot;)

    // POM 정보 설정
    pom {
        name.set(&quot;Compose-DatePicker&quot;)  // 라이브러리 이름
        description.set(&quot;Compose DatePicker&quot;)  // 라이브러리 설명
        url.set(&quot;https://github.com/KwakEuiJin/Compose-DatePicker&quot;)  // 프로젝트 URL
        inceptionYear.set(&quot;2024&quot;)  // 프로젝트 시작 연도

        // 라이선스 정보 설정
        licenses {
            license {
                name.set(&quot;The Apache License, Version 2.0&quot;)
                url.set(&quot;http://www.apache.org/licenses/LICENSE-2.0.txt&quot;)
                distribution.set(&quot;http://www.apache.org/licenses/LICENSE-2.0.txt&quot;)
            }
        }

        // 개발자 정보 설정
        developers {
            developer {
                id.set(&quot;KwakEuiJin&quot;)  // 개발자 ID
                name.set(&quot;KEZ&quot;)  // 개발자 이름
                url.set(&quot;https://github.com/KwakEuiJin&quot;)  // 개발자 URL
            }
        }

        // 소스 코드 관리(SCM) 정보 설정
        scm {
            url.set(&quot;https://github.com/KwakEuiJin/Compose-DatePicker&quot;)
            connection.set(&quot;scm:git:git://github.com/KwakEuiJin/Compose-DatePicker.git&quot;)
            developerConnection.set(&quot;scm:git:ssh://git@github.com:KwakEuiJin/Compose-DatePicker.git&quot;)
        }
    }
}</code></pre>
<h3 id="4-gradleproperties">4. gradle.properties</h3>
<p>프로젝트 루트의 gradle.properties 혹은 ~/.gradle/gradle.properties 에 다음 내용을 추가합니다. 
해당 정보들은 보안상의 이유로 공개레포지토리에 올라가면 안되기 때문에 전역 gradle.properties 파일에 추가하거나 CI 서버에서 환경 변수를 통해 제공할 수 있습니다.</p>
<pre><code class="language-kotlin">mavenCentralUsername= Maven Central token 의 username 
mavenCentralPassword= Maven Central token 의 password 
signing.keyId=12345678 # key 의 id
signing.password=paswword # key 의 패스워드 
signing.secretKeyRingFile=/Users/guest/.gnupg/secring.gpg # secring.gpg 의 경로</code></pre>
<h3 id="5-전체-buildgradle-파일">5. 전체 build.gradle 파일</h3>
<pre><code class="language-kotlin">import com.vanniktech.maven.publish.SonatypeHost

plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.jetbrains.kotlin.android)
    alias(libs.plugins.compose.compiler)
    alias(libs.plugins.vanniktech.maven)
}

group = &quot;io.github.KwaEuiJin&quot;
version = &quot;0.0.1&quot;

android {
    . . .
}

dependencies {
. . .
}

mavenPublishing {
    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

    signAllPublications()

    coordinates(&quot;io.github.kez-lab&quot;, &quot;compose-datepicker&quot;, &quot;0.0.1&quot;)

    pom {
        name = &quot;Compose-DatePicker&quot;
        description = &quot;Compose DatePicker&quot;
        url = &quot;https://github.com/KwakEuiJin/Compose-DatePicker&quot;
        inceptionYear = &quot;2024&quot;

        licenses {
            license {
                name = &quot;The Apache License, Version 2.0&quot;
                url = &quot;http://www.apache.org/licenses/LICENSE-2.0.txt&quot;
            }
        }
        developers {
            developer {
                id = &quot;KwakEuiJin&quot;
                name = &quot;KEZ&quot;
                url = &quot;https://github.com/KwakEuiJin&quot;
            }
        }

        scm {
            url.set(&quot;https://github.com/KwakEuiJin/Compose-DatePicker&quot;)
            connection.set(&quot;scm:git:git://github.com/KwakEuiJin/Compose-DatePicker.git&quot;)
            developerConnection.set(&quot;scm:git:ssh://git@github.com/KwakEuiJin/Compose-DatePicker.git&quot;)
        }
    }
}</code></pre>
<h2 id="publish">publish</h2>
<p>이제 라이브러리를 Maven Central에 배포할 준비가 되었습니다. 배포는 Gradle 명령어 하나로 쉽게 수행할 수 있습니다. 다음 단계를 따라 진행하세요</p>
<pre><code class="language-bash">./gradlew publishAndReleaseToMavenCentral --no-configuration-cache</code></pre>
<p>해당 명령어를 터미널에 입력 후 Maven Central Portal로 가보면 여러분을 기다리는 빨간 에러가 있을겁니다 ㅋ.ㅋ (없으시다면 라이브러리에서 컴포즈를 안쓰시는 분일겁니다.)</p>
<h3 id="예외-처리">예외 처리</h3>
<h4 id="dependency-version-information-is-missing-에러">&quot;Dependency version information is missing&quot; 에러</h4>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/536a506a-35d6-49e3-9f3d-4fb90d549894/image.png" alt=""></p>
<p>Maven Central로 라이브러리를 배포할 때, publish 태스크가 성공적으로 완료되었음에도 불구하고 &quot;Dependency version information is missing&quot; 오류가 발생할 수 있습니다. 이 문제는 주로 build.gradle.kts 파일의 dependencies 블록에 버전 정보가 누락되어 있을 때 발생합니다.</p>
<p>특히, Compose BOM(Bill of Materials)을 사용하여 의존성을 관리하는 경우, 이러한 오류가 발생할 가능성이 큽니다. 이는 Maven Central이 Google Maven Repository에 있는 BOM 의존성의 버전을 자동으로 찾을 수 없기 때문입니다.</p>
<h4 id="에러-발생-이유">에러 발생 이유</h4>
<p>Maven Central은 Google Maven Repository에 있는 compose-bom 패키지의 버전을 자동으로 인식하지 못합니다. 따라서 BOM을 사용하는 경우 명시적으로 버전 정보를 제공해야 합니다.</p>
<h4 id="해결-방법">해결 방법</h4>
<p>아래와 같이 libs.toml 파일에서 각 의존성에 대한 명시적인 버전 정보를 설정하고, platform(libs.androidx.compose.bom)를 제거하여 문제를 해결할 수 있습니다.</p>
<p><strong>platform 사용 제거</strong></p>
<pre><code class="language-kotlin">implementation(platform(libs.androidx.compose.bom))</code></pre>
<p><strong>libs.toml 파일에서 버전 정보 설정</strong></p>
<p>libs.toml 파일에서 다음과 같이 각 의존성의 버전을 명시적으로 설정합니다.</p>
<pre><code class="language-bash">[versions]
compose-android = &quot;1.6.8&quot;

[libraries]
androidx-ui-graphics = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-graphics&quot;, version.ref = &quot;compose-android&quot; }
androidx-ui-tooling = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-tooling&quot;, version.ref = &quot;compose-android&quot; }
androidx-ui-tooling-preview = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-tooling-preview&quot;, version.ref = &quot;compose-android&quot; }
androidx-ui-test-manifest = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-test-manifest&quot;, version.ref = &quot;compose-android&quot; }
androidx-ui-test-junit4 = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-test-junit4&quot;, version.ref = &quot;compose-android&quot; }
</code></pre>
<h4 id="배포-성공">배포 성공</h4>
<p>네 지금까지 난관을 이겨내고 배포하신 여러분에게 칭찬의 박수를 보냅니다 👍
여러분도 이젠 어엿한 라이브러리 개발자가 되신 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/048279f3-a800-492f-9cb1-342bc22bad35/image.png" alt=""></p>
<p>혹시나 제 블로그를 보시고 배포에 성공/실패하셨다면 한마디씩 남겨주신다면 큰 힘/도움이 될 수 있을 것 같습니다. 감사합니다 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] Picker, NumberPicker, DatePicker 제작 과정기 1부]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-Year-Month-DatePicker-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-Year-Month-DatePicker-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 14 Aug 2024 15:10:38 GMT</pubDate>
            <description><![CDATA[<p>그렇습니다... 🥲
가끔 우리는 안드로이드 기본 컴포넌트에 없는 컴포넌트를 만들어야 할때가 있죠.</p>
<p>오늘은 조금 까다로웠던 Year, Month를 선택할 수 있도록 하는 DatePicker를 만드는 과정을 보여드릴려고 합니다.</p>
<p>일단 코드 분석 단계가 있기 때문에 (굉장히) 조금 많이 깁니다.
버티실 수 있는 강한 분만 읽으시고 아니면 그냥 제 레포에서 복붙해가십쇼.</p>
<p><a href="https://github.com/KwakEuiJin/Compose-DatePicker">https://github.com/KwakEuiJin/Compose-DatePicker
</a></p>
<h2 id="⚒️-compose-picker-제작-동기">⚒️ Compose Picker 제작 동기</h2>
<p>먼저 요구되는 디자인 가이드를 살펴보시죠</p>
<h3 id="🖼️-디자인-가이드">🖼️ 디자인 가이드</h3>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/3fd361fe-170d-4f1c-889e-04c3886e1195/image.png" alt=""></p>
<p>해당 사진은 디자인 가이드상 한 부분인 DatePicker입니다.</p>
<p>간단히 보면 Year, Month값을 NumberPicker로 각각 구현하면 될 것 같다고 생각하실겁니다.</p>
<h3 id="문제점">문제점</h3>
<p>하지만 Compose에서는 아쉽게도 NumberPicker를 지원하지 않습니다, 따라서 저희가 따로 커스텀한 Composable을 개발해야합니다.</p>
<p>저는 처음부터 컴포넌트를 만들어 나갈 정도의 실력이 없기 때문에 다음 단계를 거쳐 DatePicker를 구현해 볼 예정입니다.</p>
<h3 id="구현-단계">구현 단계</h3>
<ol>
<li>stackoverflow에서 찾은 예시를 토대로 Picker를 구현할 것입니다.
관련 링크: <a href="https://stackoverflow.com/questions/68187868/android-jetpack-compose-numberpicker-widget-equivalent">https://stackoverflow.com/questions/68187868/android-jetpack-compose-numberpicker-widget-equivalent</a></li>
<li>해당 Picker를 활용하여 Year, Month Picker 각각 구현합니다.</li>
<li>이를 통해 디자인 가이드를 충족합니다.</li>
</ol>
<h2 id="스택오버플로우-예시-코드-분석">스택오버플로우 예시 코드 분석</h2>
<blockquote>
<p>코드가 너무 길기 때문에 위 참고 링크를 통해 확인 부탁드립니다.
최대한 간단히 설명해보겠습니다.</p>
</blockquote>
<h3 id="picker-ui-구조-설명">Picker UI 구조 설명</h3>
<p>Picker UI는 화면에 리스트 형태로 아이템들을 표시하고, 사용자가 스크롤하여 원하는 항목을 선택할 수 있도록 만든 컴포저블입니다.</p>
<p>많은 코드 중 Picker 기능의 가장 핵심적인 LazyColumn만 짚고 넘어가보겠습니다.</p>
<pre><code class="language-kotlin">@Composable
LazyColumn(
    state = listState,
    flingBehavior = flingBehavior,
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
        .fillMaxWidth()
        .height(itemHeightDp * visibleItemsCount)
        .fadingEdge(fadingEdgeGradient)
) {
    items(listScrollCount) { index -&gt;
        Text(
            text = getItem(index),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            style = textStyle,
            modifier = Modifier
                .onSizeChanged { size -&gt; itemHeightPixels.value = size.height }
                .then(textModifier)
        )
    }
}</code></pre>
<p>LazyColumn은 리스트 형태로 아이템들을 세로로 배치하는 역할을 합니다. 이 부분이 Picker의 핵심입니다. 사용자가 이 리스트를 스크롤하면, 리스트 항목들이 위아래로 움직이며 선택할 수 있습니다.</p>
<h4 id="flingbehavior">flingBehavior</h4>
<p>여기서 flingBehavior라는 생소한 녀석을 볼 수 있는데요!!
flingBehavior란 Compose에서 스크롤 가능한 UI 요소(예: LazyColumn 또는 LazyRow)에서 사용자의 스크롤 동작을 제어하고, 스크롤이 멈출 때 아이템이 특정 위치에 정렬되도록 하는 중요한 역할을 합니다. </p>
<p>이는 특히, Picker UI에서 사용자가 스크롤할 때 스냅(Snap) 동작을 구현하는 데 필수적이죠</p>
<p>아래는 flingBehavior를 제거한 예시 영상입니다, 동작을 보시면 아시겠지만 Picker라고 하기엔 명시적인 아이템을 선택하지 못하는 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/bdfa2ca6-5b4a-4541-844c-752f134127d4/image.gif" alt=""></p>
<p>즉 우리는 flingBehavior를 통해 사용자가 스크롤을 멈출 때 애매하게 아이템이 멈추는 것이 아닌 컴포넌트 중앙에 요소가 정렬되도록 하여 *<em>어떤 숫자가 선택되었는지를 사용자에게 명시적으로 보여줄 수 있는 것을 알았습니다 *</em></p>
<h4 id="fadingedgegradient">fadingEdgeGradient</h4>
<p>fadingEdgeGradient는 Picker UI에서 리스트의 상단과 하단 부분에 부드러운 페이딩 효과를 적용하기 위해 사용됩니다. </p>
<p>이 효과는 위 영상에서 보이는 것 처럼 사용자가 스크롤할 때 리스트의 양 끝부분이 서서히 사라지게 보여, 시각적으로 부드럽고 깔끔한 느낌을 주는 역할을 합니다.</p>
<h4 id="height">height</h4>
<p>LazyColumn의 높이 계산식은 다음과 같습니다 </p>
<pre><code class="language-kotlin">height(itemHeightDp * visibleItemsCount)</code></pre>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/a4020a90-e56f-4b51-90ac-daeb9f827c06/image.png" alt=""></p>
<p>이를 통해 위 사진처럼 visibleItemsCount(예시: 3)에 해당하는 값만 보이도록 하여 Picker의 UI를 구현할 수 있는 것입니다.</p>
<h4 id="pickerstate">PickerState</h4>
<p>다음은 PickerState를 봅시다.</p>
<pre><code class="language-kotlin">class PickerState {
    var selectedItem by mutableStateOf(&quot;&quot;)
}
</code></pre>
<p>그냥 별거 없이 선택된 아이템을 저장하는 클래스입니다.
이를 recomposition 상황에서도 유지되도록 remember로 묶은 형태를 rememberPickerState() 로 정의했을 뿐입니다.</p>
<h3 id="🕐-picker-활용하기">🕐 Picker 활용하기</h3>
<h4 id="pickerexample-설명">PickerExample 설명</h4>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/695d1621-a04a-4c98-bea0-c5d9d2c7efe5/image.gif" alt=""></p>
<p>해당 스택오버플로우의 예제를 보면 Row를 사용하여 수평으로 두개의 Picker Composable을 배치하였으며, 각각의 Picker에서 선택한 아이템을 가져오기 위해 2개의 State를 선언한 것을 확인할 수 있습니다.</p>
<p>또한 각 Picker에 들어갈 item을 정의하기 위해 <code>List&lt;String&gt;</code> 형태의 변수를 선언해두었습니다.</p>
<pre><code class="language-kotlin">val values = remember { (1..99).map { it.toString() } }
val valuesPickerState = rememberPickerState()
val units = remember { listOf(&quot;seconds&quot;, &quot;minutes&quot;, &quot;hours&quot;) }
val unitsPickerState = rememberPickerState()</code></pre>
<p>여기까지 스택오버플로우에서 제시해준 Picker를 분석해보았습니다.
좀 길었는데요, 그래서 제가 이 Picker를 어떻게 커스텀해서 제 디자인 가이드에 알맞게 변경했는지 확인해보시죠.</p>
<h2 id="실제-디자인-가이드에-알맞게-picker를-커스텀">실제 디자인 가이드에 알맞게 Picker를 커스텀</h2>
<h3 id="picker-디자인-변경">Picker 디자인 변경</h3>
<p>먼저 년도(Year), 월(Month)를 나타내기 위해서는 2개의 Picker가 필요합니다</p>
<p><strong>특히 예제에는 이미 2개의 Picker가 있기 때문에 크게 어려운 점은 없었습니다...? 과연 그랬을까요?</strong></p>
<p>하하 일단 같이 살펴보시죠</p>
<h4 id="divider">Divider</h4>
<p>아래 디자인 가이드를 보시면 Year, Month 모두 Divider가 고정 dp값을 가진 컴포넌트 입니다. 
<img src="https://velog.velcdn.com/images/kej_ad/post/69eb6c3f-85e0-4368-ba75-32f007e29550/image.png" alt=""></p>
<p>즉 예제와 같이 fillMaxWidth가 아니며 중앙정렬이 필요하다는 것입니다.</p>
<p>저는 이러한 문제를 해결하기 위해 Box로 Divider를 한번 더 감싼 후 이를 Alignment.Center를 통해 상위 Box의 중앙에 Divider가 위치할 수 있도록 했습니다.</p>
<p>이때 y값의 offset을 조정하는 코드가 빠진 것을 볼 수 있는데요. 이 또한 Divider를 Box 내부에서 Top, Bottom에 제약을 걸고 Box자체의 height를 itemHeightDp로 잡아 더욱 간단하게 Picker에서 선택된 아이템을 나타내는 Divider를 구현했습니다.</p>
<pre><code class="language-kotlin">Box(
    modifier = Modifier
        .align(Alignment.Center)
        .fillMaxWidth()
        .height(itemHeightDp)
) {
    HorizontalDivider(
        color = dividerColor,
        thickness = 2.dp,
        modifier = Modifier
            .fillMaxWidth()
            .background(
                color = dividerColor,
                shape = RoundedCornerShape(10.dp)
            )
            .align(Alignment.TopCenter)
    )

    HorizontalDivider(
        color = dividerColor,
        thickness = 2.dp,
        modifier = Modifier
            .fillMaxWidth()
            .background(
                color = dividerColor,
                shape = RoundedCornerShape(10.dp)
            )
            .align(Alignment.BottomCenter)
    )
}</code></pre>
<p>2부에 계속...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Notification RemoteView 억까 해결기, has too high data size]]></title>
            <link>https://velog.io/@kej_ad/Android-Notification-RemoteView-%EC%96%B5%EA%B9%8C-%ED%95%B4%EA%B2%B0%EA%B8%B0-has-too-high-data-size</link>
            <guid>https://velog.io/@kej_ad/Android-Notification-RemoteView-%EC%96%B5%EA%B9%8C-%ED%95%B4%EA%B2%B0%EA%B8%B0-has-too-high-data-size</guid>
            <pubDate>Sat, 06 Jul 2024 11:18:24 GMT</pubDate>
            <description><![CDATA[<h2 id="notification-remoteview란">Notification RemoteView란?</h2>
<p>안드로이드 앱 개발 시 알림(Notification)을 커스터마이징하기 위해 <code>RemoteViews</code>를 사용하게 됩니다. </p>
<p><a href="https://developer.android.com/develop/ui/views/notifications/custom-notification">커스텀 알림 공식 문서 링크</a></p>
<p>이 RemoteView라는 건 쓸 수 있는 View 종류도 적으면서 툭하면 크래시 내뿜으면서 뻗는 개복치 같은 레이아웃입니다. </p>
<p>이<code>RemoteViews</code> 객체를 재사용할 경우 데이터 크기가 점점 증가하여 알림 서비스가 오류를 일으킬 수 있는데요. </p>
<p> 오늘은 <code>RemoteViews</code> 객체 재사용 시 데이터 크기 증가 문제의 원인과 해결 방법을 설명해보겠습니다.</p>
<h2 id="문제-발생-상황">문제 발생 상황</h2>
<h3 id="문제가-된-코드">문제가 된 코드</h3>
<p>앱 개발 중, Foreground service와 RemoteView로 이루어진 Notification 생성했습니다.</p>
<p>이를 updateProgress 함수를 통해 1초마다 프로그레스바를 업데이트하여 진행도를 유저에게 알려주는 로직이 포함되어있습니다.</p>
<pre><code class="language-kotlin">private suspend fun updateProgress(
    remoteViews: RemoteViews,
    expandedRemoteViews: RemoteViews,
    eventData: EventData,
) {
    withContext(Dispatchers.Main) {
        val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
        val currentEventData = eventData.current

        val progress = calculateProgress(now, currentEventData)

        expandedRemoteViews.setProgressBar(R.id.progressBar, 100, progress, false)
        val updateNotification = buildNotification(remoteViews, expandedRemoteViews, eventData)
        NotificationManagerCompat.from(context).notify(1, updateNotification)
    }
}
. . .


fun buildNotification(. . .) {
    NotificationCompat.Builder(. . .)
    . . .
    .setCustomContentView(remoteViews)
    .setCustomBigContentView(expandedRemoteViews)
    .build()
}
</code></pre>
<h3 id="에러-로그">에러 로그</h3>
<p>이 타이머를 돌리던 중 알림 데이터 크기 초과 오류가 발생했습니다. 로그는 다음과 같았습니다:</p>
<pre><code class="language-java">2024-07-06 19:01:43.027  2535-13342 NotificationService     system_server E  notification pkg : app.xxx.yyy has too high data size(179500) above 100000
2024-07-06 19:01:43.027  2535-13342 NotificationService     system_server E  notification key : 0|app.xxx.yyy.android|1|null|11031 has too high data size(179500) above 100000
</code></pre>
<p>이는 <code>RemoteViews</code> 객체가 데이터 크기 제한(100000)을 초과했음을 나타냅니다.</p>
<h2 id="원인-분석">원인 분석</h2>
<p>이런 어이없는 이슈가 발생하는 이유가 궁금하여 RemoteView가 뷰 객체에 데이터를 set할 때 어떤 로직이 내부에서 동작되는지 확인해보았습니다.</p>
<h3 id="remoteviews의-데이터-크기가-증가하는-이유">RemoteViews의 데이터 크기가 증가하는 이유</h3>
<p><code>RemoteViews</code> 객체를 재사용하면 데이터 크기가 증가하는 이유는 <code>RemoteViews</code>가 내부적으로 변경 사항을 누적하여 저장하기 때문입니다. </p>
<p><code>RemoteViews</code>는 안드로이드의 View 시스템에서 원격으로 UI를 업데이트할 수 있는 방법을 제공하며, 이 과정에서 변경 사항을 명령(Command) 객체로 저장하고 이를 누적하여 처리합니다.</p>
<h3 id="remoteviews의-내부-동작"><code>RemoteViews</code>의 내부 동작</h3>
<ol>
<li><strong>명령 객체(RemoteViews.Action)</strong>: <code>RemoteViews</code>는 변경 사항을 추적하기 위해 각 변경 사항을 Action 객체로 저장합니다. </li>
</ol>
<p>예를 들어, 제가 위에서 사용한 setProgressBar라는 Progress를 업데이트하는 명령이 있을 수 있습니다. </p>
<p>이러한 명령 객체는 <code>ReflectionAction</code> 클래스를 통해 <code>RemoteViews.Action</code> 타입으로 구현됩니다.</p>
<pre><code class="language-java">
    public void setProgressBar(@IdRes int viewId, int max, int progress,
            boolean indeterminate) {
        setBoolean(viewId, &quot;setIndeterminate&quot;, indeterminate);
        if (!indeterminate) {
            setInt(viewId, &quot;setMax&quot;, max);
            setInt(viewId, &quot;setProgress&quot;, progress);
        }
    }

    public void setBoolean(@IdRes int viewId, String methodName, boolean value) {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BOOLEAN, value));
    }

    public void setInt(@IdRes int viewId, String methodName, int value) {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
    }

    private void addAction(Action a) {
        if (hasMultipleLayouts()) {
            throw new RuntimeException(&quot;RemoteViews specifying separate layouts for orientation&quot;
                    + &quot; or size cannot be modified. Instead, fully configure each layouts&quot;
                    + &quot; individually before constructing the combined layout.&quot;);
        }
        if (mActions == null) {
            mActions = new ArrayList&lt;&gt;();
        }
        mActions.add(a);
    }
</code></pre>
<ol start="2">
<li><strong>명령 리스트 유지</strong>: <code>RemoteViews</code> 객체는 이러한 명령 객체들의 리스트를 유지합니다. 이 리스트가 커지면 <code>RemoteViews</code> 객체의 전체 크기도 커지게 됩니다.</li>
</ol>
<ol start="3">
<li><p><strong>명령 실행</strong>: <code>RemoteViews</code> 객체는 필요할 때 이 명령 리스트를 순차적으로 실행하여 UI를 업데이트합니다. 이는 <code>apply</code> 메서드를 통해 이루어집니다.</p>
<pre><code class="language-java"> // 내부에 더 많은 로직이 있지만 이해를 위해 요약하여 가져온 로직입니다.

     @Override
     public void apply(Context context, ViewGroup parent, OnClickHandler handler) {
         for (Action a : mActions) {
             a.apply(view, parent, handler);
         }
     }
 }
</code></pre>
</li>
</ol>
<pre><code>
이와 같이 명령 객체가 누적되면서 `RemoteViews` 객체의 크기가 증가하고, 데이터 크기 제한을 초과하게 됩니다.

### 해결 방법

매번 새로운 `RemoteViews` 객체를 생성하여 사용하면, 변경 사항이 누적되지 않아 데이터 크기 증가 문제를 해결할 수 있습니다.

### 문제 해결을 위한 코드 변경

다음은 기존 코드를 수정하여 매번 새로운 `RemoteViews` 객체를 생성하는 방식으로 변경한 예시입니다:
``` kotlin
private suspend fun updateProgress(eventData: EventData) {
    withContext(Dispatchers.Main) {
        val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
        val currentEventData = eventData.current
        val progress = calculateProgress(now, currentEventData)

        // RemoteView 객체 생성
        val remoteViews =
            createRemoteViews(R.layout.view_foreground_notification_layout, eventData)
        val expandedRemoteViews =
            createRemoteViews(R.layout.view_foreground_notification_expended_layout, eventData)
        expandedRemoteViews.setProgressBar(R.id.progressBar, 100, progress, false)
        val updateNotification = buildNotification(remoteViews, expandedRemoteViews, eventData)
        NotificationManagerCompat.from(context).notify(1, updateNotification)
    }
}
. . .
private fun createRemoteViews(
    layoutId: Int,
    eventData: EventData
): RemoteViews {
    val remoteViews = RemoteViews(context.packageName, layoutId)
    updateRemoteViews(remoteViews, eventData)
    return remoteViews
}</code></pre><p>이제 <code>RemoteViews</code> 객체를 매번 새로 생성하므로, 변경 사항이 누적되는 문제를 방지할 수 있습니다.</p>
<h2 id="결론">결론</h2>
<p>즉 <code>RemoteViews</code> 객체를 재사용하면 내부적으로 변경들의 명령 객체들이 누적되어 데이터 크기가 증가할 수 있습니다 🔥</p>
<p>이를 해결하기 위해 매번 새로운 <code>RemoteViews</code> 객체를 생성하여 사용하는 것이 좋습니다ㅋㅋ</p>
<p>아니 객체를 생성하는게 비효율적이라고 생각해서 재활용했더니 더 큰 문제가 발생하네요... </p>
<blockquote>
<p><strong>💡 역시 제가 만든 클래스가 아니라면 의도한대로 동작할거라고 지레짐작 하는 것은 위험하다는 것을 오늘 또 깨달았습니다</strong></p>
</blockquote>
<p>쨋든 시원한 결말은 아닌 것 같지만, 이렇게 하면 알림 데이터 크기 초과 문제를 해결할 수 있으며, 안정적인 알림 서비스를 제공할 수 있습니다.</p>
<p>감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Compsoe Jetpack Navigation Nested Graph와 Shared ViewModel]]></title>
            <link>https://velog.io/@kej_ad/Android-Compsoe-Jetpack-Navigation-Nested-Graph%EC%99%80-Shared-ViewModel</link>
            <guid>https://velog.io/@kej_ad/Android-Compsoe-Jetpack-Navigation-Nested-Graph%EC%99%80-Shared-ViewModel</guid>
            <pubDate>Thu, 09 May 2024 11:37:37 GMT</pubDate>
            <description><![CDATA[<p>Jetpack Navigation 기초 부분을 공부하고 싶으시다면 해당 링크를 참고해주세요!</p>
<p><strong><a href="https://velog.io/@kej_ad/AndroidCompose-Jetpack-Navigation-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">Jetpack Navigation이란?</a></strong></p>
<h1 id="nested-graph">Nested Graph</h1>
<h2 id="nested-graph란">Nested Graph란?</h2>
<p>Nested Graph은 한마디로 중첩된 그래프 구조를 가진 형태를 뜻합니다. 지난번 포스팅인 Jetpack Navigation 소개 포스팅 처럼 GraphBuilder.composable() 함수를 활용하면 Navigation 내의 destination(목적지)를 추가하여 Graph를 구성할 수 있게 됩니다.</p>
<p>중첩 그래프는 이러한 destination(목적지)의 집합체를 한번 더 Graph로 구성하여 추상화(공통적인 특성을 가진 요소를 상위 Type으로 wrapping했다) 시켰다 라고 이해해주시면 쉬우실겁니다.</p>
<p>간단한 예제를 보시죠</p>
<h3 id="기존-graph-예제">기존 Graph 예제</h3>
<p>아래 사진과 같이 기존 그래프의 경우에는 각각의 destination 별로 Graph를 구성했습니다. 즉 모두 같은 계층의 Graph에 존재하며 이동하는데에도 큰 제약은 없죠.</p>
<p>그렇기에 간단한 화면 구조에서는 오히려 효과적으로 화면 이동의 흐름을 파악할 수 있습니다.</p>
<p><strong>다만 복잡한 화면 구조</strong>에서는 stack을 관리하거나 화면별로 공통적인 data를 관리하는 구조가 더욱 복잡해집니다. 또한 반복되는 화면의 Flow를 재사용할 수 없는 구조라는 점이 가장 큰 단점이 되는 구조입니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/9f309c47-9657-48cf-ad02-53696f731a6d/image.png" alt=""></p>
<h3 id="nested-graph-예제">Nested Graph 예제</h3>
<p>아래 사진이 중첩 그래프를 적용했을 때의 화면 흐름입니다. </p>
<p><strong>유저 인증과 관련된 화면</strong>은 Login, 회원가입, 비밀번호 변경 화면으로 Auth Graph내부에 중첩되어 구현되어 있습니다.
<strong>Main 기능 관련된 화면</strong>은 홈, Playground, My page로 화면은 Main Graph내에 중첩되있는 구조입니다.
<img src="https://velog.velcdn.com/images/kej_ad/post/9d7c2f94-5c3a-4c39-bc26-35419ed00e46/image.png" alt=""></p>
<h3 id="nested-graph중첩-그래프의-장점">Nested Graph(중첩 그래프)의 장점</h3>
<p><strong>1. 모듈화 및 재사용성</strong></p>
<ul>
<li>플로우의 모듈화: 각 기능 플로우를 별도의 그래프로 나누어 관리함으로써 논리적 단위로 모듈화가 가능합니다. (예: Auth 플로우, Main 플로우 등)</li>
<li>재사용 가능: 중첩된 그래프는 다른 곳에서도 동일한 로직으로 재사용할 수 있습니다. (예: Auth 플로우를 앱의 여러 지점에서 재사용)</li>
</ul>
<p><strong>2. 캡슐화</strong></p>
<ul>
<li>내부 흐름의 숨김: 중첩 그래프의 내부 목적지는 외부에서 직접 접근할 수 없고, 중첩 그래프 자체를 통해서만 접근 가능합니다. </li>
<li>이를 통해 내부 흐름을 감추고 외부에서 접근 가능한 진입점만 노출할 수 있습니다.</li>
<li>내부 흐름은 변경하더라도 외부에 영향을 주지 않기 때문에 유지보수성이 향상됩니다.</li>
</ul>
<p><strong>3. 흐름 관리 간소화</strong></p>
<ul>
<li>독립적인 스타트 목적지: 각 중첩 그래프는 자신의 startDestination을 가질 수 있어 독립적인 흐름을 관리할 수 있습니다. (예: Auth 플로우에서는 첫 번째 로그인 화면이, Main 플로우에서는 Homt 화면이 startDestination으로 설정 가능)</li>
<li>흐름 추적 용이성: 중첩 그래프를 통해 플로우를 그룹화하면 전체 네비게이션 구조가 더 간결해져 흐름을 파악하기 쉬워집니다.</li>
</ul>
<p><strong>4. 코드 분리 및 가독성</strong></p>
<ul>
<li>코드 분리: 각 중첩 그래프를 별도의 확장 함수나 NavGraphBuilder로 분리하면 네비게이션 그래프의 코드가 훨씬 깔끔해집니다.</li>
<li>이를 통해 각 그래프에 특화된 확장 함수를 만들어 사용 가능하며, 네비게이션 그래프의 코드 복잡도를 줄일 수 있습니다.</li>
</ul>
<p><strong>5. 복잡한 플로우 처리에 유용</strong></p>
<ul>
<li>조건부 플로우 처리: 예를 들어 첫 로그인 시 온보딩 플로우를 실행하는 경우나, 게임이 끝났을 때 매치로 이동할지 등록 플로우로 이동할지 등 복잡한 조건을 쉽게 관리할 수 있습니다.</li>
</ul>
<h2 id="nested-graph-구현">Nested Graph 구현</h2>
<h3 id="nested-graph를-compose로-구현해보자">Nested Graph를 Compose로 구현해보자</h3>
<p>일반적인 Navigation Graph를 compose에서 구현하기 위해서는 <code>NavGraphBuilder.composable()</code> 함수를 썼습니다.</p>
<p>하지만 Nested Graph를 구현하려면 조금 다른 방법을 사용해야하는데요 !!
바로 <code>NavGraphBuilder.navigation()</code> 함수 내부에 <code>NavGraphBuilder.composalbe()</code>을 구현하면 됩니다.</p>
<p><strong>Nested Graph 전체 코드 예제</strong></p>
<pre><code class="language-kotlin">@Composable
fun Navigation(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        navigation(
            startDestination = AuthScreen.Login.route,
            route = AuthScreen.Auth.route
        ) {
            composable(AuthScreen.Login.route) {
                LoginScreen(
                    loginAction = {
                        navController.navigate(MainScreen.Home.route) {
                            popUpTo(AuthScreen.Auth.route) {
                                inclusive = true
                            }
                        }
                    }
                )
            }
            composable(AuthScreen.SignUp.route) {
                SignUpScreen()
            }
        }
        navigation(
            startDestination = MainScreen.Home.route,
            route = MainScreen.Main.route
        ) {
            composable(MainScreen.Home.route) {
                HomeScreen(
                    navigateToDetail = { navController.navigate(DetailScreen.Detail.route) },
                    navigateToPlayGround = { navController.navigate(MainScreen.PlayGround.route) }
                )
            }
            composable(MainScreen.MyPage.route) {
                DetailScreen(
                    navigateToHome = { navController.navigate(MainScreen.Home.route) },
                    navigateToPlayGround = { navController.navigate(MainScreen.PlayGround.route) }
                )
            }
            composable(MainScreen.PlayGround.route) {
                PlayGroundScreen(
                    navigateToHome = {
                        navController.navigate(MainScreen.Home.route) {
                            popUpTo(MainScreen.Home.route) {
                                inclusive = true
                            }
                        }
                    },
                    navigateToDetail = { navController.navigate(DetailScreen.Detail.route) }
                )
            }
        }

        composable(DetailScreen.Detail.route) {
            DetailScreen()
        }
    }
}</code></pre>
<h3 id="nested-graph에서-화면-이동하기">Nested Graph에서 화면 이동하기</h3>
<p>일반적인 Navigation Graph와 동일하게 Navigation Controller 를 사용해주시면 되는데요, 다만 한가지 차이점이 있습니다.</p>
<p>바로 Nested Graphs에서는 일반적인 방법으로는 외부 destination(목적지)이 Nested Graph 내부의 특정 destination에 직접 접근할 수 없다는 점입니다. </p>
<p>대신, Nested Graph 자체를 대상으로 navigate()하여 내부 흐름으로 진입해야하죠.</p>
<h3 id="최상위-그래프에서-nested-graph로-직접-이동해야-할-때">최상위 그래프에서 Nested Graph로 직접 이동해야 할 때</h3>
<p>위에서 설명한 것 처럼 Nested Graph의 내부 목적지에 직접 접근하지 못하기 때문에, Nested Graph 자체의 route를 사용해 접근해야합니다.</p>
<pre><code class="language-kotlin">navigation(
        startDestination = AuthScreen.Login.route,
        route = AuthScreen.Auth.route
    )
...

composable(MainScreen.Home.route) {
        HomeScreen(
            navigateToLogin = { 
                navController.navigate(AuthScreen.Auth.route) 
            }
        }

</code></pre>
<p>해당 예제 코드처럼 navigation 내에도 route를 AuthScreen.Auth.route 으로 설정해둔 것을 보실 수 있습니다.</p>
<p>이러한 구조에서  navController를 활용하여 AuthScreen.Auth.route 로 navigate 한다면 Auth Graph의 startDestination인 Login 화면으로 이동할 것 입니다.</p>
<p>내부 목적지가 추가되거나 변경될 수 있으며, 최상위 그래프는 이를 알 필요 없이 중첩 그래프 자체에만 navigate()하면 됩니다.</p>
<h3 id="최상위-그래프에서-nested-graph-내부의-특정-목적지로-이동해야-할-경우">최상위 그래프에서 Nested Graph 내부의 특정 목적지로 이동해야 할 경우</h3>
<p>그러나 네비게이션 그래프 구조를 설계하는 방법에 따라 특정 중첩 그래프 내부의 경로에 직접 접근할 수 있게 만드는 것이 가능합니다!!</p>
<p>공식 문서에서는 중첩된 네비게이션 그래프의 캡슐화를 강조하며, 외부에서 중첩 그래프 내부 목적지에 직접 접근하지 못하고 대신 중첩 그래프 전체에 navigate()할 것을 권장하고 있습니다.</p>
<p>하지만 실제로는 navigate(route/path) 의 방식으로 직접 Nested Graph 내부 특정 화면으로 이동할 수 있습니다.</p>
<pre><code class="language-kotlin">    composable(MainScreen.PlayGround.route) {
                PlayGroundScreen(
                    navigateToSignUp = {
                        val route = AuthScreen.Auth.route/AuthScreen.SignUp.route
                        navController.navigate(route) 
                    }
                )
            }
        }</code></pre>
<p>해당 예제를 보시면 AuthScreen.Auth.route/AuthScreen.SignUp.route의 route/path의 구조를 활용하여 Main Graph의 내부 destination인 Playgroun에서 Auth Graph의 SignUp destination으로 바로 이동이 가능합니다.</p>
<p>다만 권장사항을 따르면 네비게이션 구조를 더욱 깔끔하고 안전하게 유지할 수 있다는 점 꼭꼭 참고하여 개발해주세요!!</p>
<h1 id="viewmodel">ViewModel</h1>
<h2 id="jetpack-compose에서-viewmodel-사용-방법-및-원칙">Jetpack Compose에서 ViewModel 사용 방법 및 원칙</h2>
<p>ViewModel은 Jetpack Compose에서 주로 화면 UI 상태를 Composable에 노출하는 수단으로 사용됩니다. </p>
<p>기존에는 액티비티나 프래그먼트에서 UI 컨트롤러 역할을 수행하며 재사용 가능한 UI를 만드는 것이 복잡하고 어려웠으나, Jetpack Compose와 ViewModel의 조합을 통해 더 간단하고 직관적으로 UI를 만들 수 있습니다.</p>
<h3 id="문제점-발생">문제점 발생?</h3>
<p>하지만 심각한 문제가 하나 있죠... 바로 ViewModel을 Compose에서 사용할 때 가장 중요한 점은 Composable 자체에 ViewModel의 생명주기를 맞출 수 없다는 것입니다.</p>
<p>그렇기 때문에 ViewModel을 생성한 후 Composable에서 사용하고자 한다면 Screen Composable이 파괴되었음에도 계속 이전 데이터를 가지고 있는 ViewModel 인스턴스를 참조하게 될 것 입니다.</p>
<h3 id="이유는">이유는?</h3>
<p><strong>Composable은 ViewModelStoreOwner가 아니기 때문입니다.</strong></p>
<p>ViewModelStoreOwner는 ComponentActivity, Fragment, NavBackStackEntry 만의 subClass이기 때문입니다.</p>
<h3 id="viewmodelstoreowner는-뭔데">ViewModelStoreOwner는 뭔데?</h3>
<p><strong>ViewModelStoreOwner</strong>는 ViewModel을 저장하고 관리하는 컨테이너 역할을 하는 객체입니다. ViewModel을 제공하는 ViewModelProvider는 ViewModelStoreOwner를 통해 ViewModel을 관리하고 액세스합니다.</p>
<p>Activity, Fragment, NavBackStackEntry로 등에서 관리되는 ViewModelStore는 해당 스코프가 파괴될 때 clear() 메서드를 통해 정리됩니다.
예를 들어, Activity가 완전히 종료되거나 NavBackStackEntry가 제거될 때 clear()를 호출하여 ViewModel 인스턴스를 메모리에서 해제합니다.</p>
<h3 id="navigation에서-viewmodelstoreowner">Navigation에서 ViewModelStoreOwner</h3>
<p>Navigation Component에서 각 Navigation Destination은 BackStackEntry로 관리됩니다. NavBackStackEntry는 각 Entry별로 ViewModelStoreOwner 가지므로 목적지별로 ViewModel을 관리할 수 있습니다.</p>
<h3 id="compose-viewmodel-내부-작동-원리">Compose viewModel() 내부 작동 원리</h3>
<ul>
<li>CompositionLocal 제공<ul>
<li>Compose에서 NavBackStackEntry의 ViewModelStoreOwner를 CompositionLocal을 통해 사용하여 현재 BackStackEntry에 대한 ViewModel을 찾습니다.</li>
<li>LocalViewModelStoreOwner는 현재 BackStackEntry의 ViewModelStoreOwner를 제공합니다.</li>
</ul>
</li>
<li>viewModel() 함수 구현<ul>
<li>viewModel() 함수는 LocalViewModelStoreOwner에서 가져온 ViewModelStoreOwner를 사용해 ViewModelProvider를 통해 ViewModel을 가져옵니다.</li>
</ul>
</li>
<li>BackStackEntry와 ViewModel의 연동<ul>
<li>ViewModel의 생명주기는 NavBackStackEntry에 연결됩니다.
BackStackEntry가 제거되면 해당 Entry와 연관된 ViewModel이 자동으로 정리됩니다.</li>
</ul>
</li>
</ul>
<p><strong>viewModel 내부 코드</strong> </p>
<pre><code class="language-kotlin">@Composable
inline fun &lt;reified VM : ViewModel&gt; viewModel(
    key: String? = null,
    factory: ViewModelProvider.Factory? = null
): VM {
    val owner = checkNotNull(LocalViewModelStoreOwner.current) {
        &quot;No ViewModelStoreOwner was provided via LocalViewModelStoreOwner&quot;
    }
    return if (key != null) {
        ViewModelProvider(owner, factory ?: defaultViewModelProviderFactory(owner)).get(key, VM::class.java)
    } else {
        ViewModelProvider(owner, factory ?: defaultViewModelProviderFactory(owner)).get(VM::class.java)
    }
}</code></pre>
<p>이러한 이유로 Composable은 자체적으로 ViewModel을 관리하지 못하는데요, 따라서 두 개의 동일한 Composable이 Composition에서 동시에 존재하거나, 같은 ViewModelStoreOwner(액티비티, 프래그먼트, Navigation Destination 등)를 공유하는 경우에는 같은 ViewModel 인스턴스를 받게 됩니다.</p>
<h3 id="해결책">해결책</h3>
<p>바로 위에서 설명한 Compose Navigation을 활용하는 것입니다.</p>
<p>Jetpack Compose의 Navigation 라이브러리를 사용해 NavHost를 구성한 후 Navigation 목적지(destination) 내에서 viewModel()을 사용해 ViewModel을 제공합니다.</p>
<p><strong>Compose ViewModel With Navigation 예제</strong></p>
<pre><code class="language-kotlin">
@Composable
fun Navigation(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        composable(MainScreen.Home.route) {
            HomeScreen()
      }
  }

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 30.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(text = &quot;Home&quot;)
        Spacer(modifier = Modifier.height(8.dp))
    }
}</code></pre>
<p> Navigation destination 또는 그래프내에서 ViewModel을 구현함으로써 각 Composable에서 고유한 ViewModel 인스턴스를 제공할 수 있습니다.</p>
<p> 이를 통해 화면 전환 시에 NavBackStackEntry에 따라 ViewModel의 생명주기가 관리됩니다.</p>
<h3 id="반대로-생각해보면-어떨까">반대로 생각해보면 어떨까?</h3>
<p>그렇다면 반대로 비슷한 행위를 하는 혹은 Composable별로 데이터를 공유하는게 더 효율적인 경우에는 ViewModel을 하나의 인스턴스로 사용하는게 좋겠죠??</p>
<p>하지만 우리는 Navigation을 사용하기 때문에 오히려 하나의 인스턴스의 ViewModel을 만들기가 쉽지 않습니다.</p>
<p>그러나 위에서 배운 Navigation의 Nested Graph를 활용한다면 동일한 NavBackStackEntry를 활용하여 Auth Screen에서는 AuthViewModel을 사용할 수 있겠죠?? </p>
<h3 id="sharedviewmodel">SharedViewModel?</h3>
<p>쉽게 얘기해서 공유하는 ViewModel이라는 뜻인데 정식 명칭은 아닙니다. 그냥 제가 ViewModel을 공유한다는 차원에서 이렇게 부를거에요.</p>
<p>구현 방법은 다음과 같습니다.
NavBackStackEntry를 통해 parent의 route(중첩으로 감싼 주체 Graph 루트 예시로 MainScreen.Main)를 구하고 navController.getBackStackEntry를 통해 parentEntry를 구하여 ViewModel을 만들게 되면 상위 계층의 BackStackEntry를 참조하게 되기 때문에 내부의 Home, MyPage에서 동일한 ViewModel을 공유할 수 있는 것 입니다.</p>
<pre><code class="language-kotlin">
@Composable
inline fun &lt;reified T : ViewModel&gt; NavBackStackEntry.sharedViewModel(navController: NavController): T {
    val navGraphRoute = destination.parent?.route ?: return viewModel()
    val parentEntry = remember(this) {
        navController.getBackStackEntry(navGraphRoute)
    }
    return viewModel(parentEntry)
}

@Composable
NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = AuthScreen.Auth.route
    ) {
        navigation(
            startDestination = MainScreen.Home.route,
            route = MainScreen.Main.route
        ) {
            composable(MainScreen.Home.route) {
                val viewModel = it.sharedViewModel&lt;HomeViewModel&gt;(navController)
                HomeScreen(viewModel)
            }
            composable(MainScreen.MyPage.route) {
                val viewModel = it.sharedViewModel&lt;HomeViewModel&gt;(navController)
                DetailScreen(viewModel)
            }
    }
}</code></pre>
<p>오늘은 Nasted Graph(중첩 그래프) 부터 ViewModel 인스턴스를 Navigation Destination에 위치한 Screen Composable별로 공유하는 방법까지 알아보았습니다!</p>
<p>다음은 Compose Navigation SafeArgs 에 대해서 포스팅해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] Jetpack Navigation 사용해보기 (기초)]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-Jetpack-Navigation-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-Jetpack-Navigation-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 07 May 2024 12:15:55 GMT</pubDate>
            <description><![CDATA[<h1 id="jetpack-navigaiton">Jetpack Navigaiton</h1>
<h2 id="jetpack-navigation-이란">Jetpack Navigation 이란?</h2>
<p>Navigation은 공식문서를 직역해보자면 사용자가 앱 내 다양한 콘텐츠를 탐색하고 진입하며 돌아갈 수 있게 하는 상호작용을 말합니다.</p>
<p>이를 쉽게 얘기해보자면 Navigation이란 화면을 탐색하여 사용자가 앱 안에서 다른 화면으로 갈 수 있게 해주는 기능을 말합니다. 예를 들어, 메뉴에서 하나의 버튼을 눌러 다른 페이지로 이동하거나, 설정 화면으로 가서 이전 화면으로 돌아오는 것들 모두 포함되는 개념입니다.</p>
<p>Android Jetpack의 네비게이션 구성 요소 다음과 같습니다.</p>
<ol>
<li>Navigation 라이브러리</li>
<li>Safe Args Gradle 플러그인</li>
<li>그리고 앱 Navigation을 구현하는데 도움을 주는 도구</li>
</ol>
<h2 id="핵심-요소">핵심 요소</h2>
<h3 id="host">Host</h3>
<p>Host는 사용자가 앱을 사용하는 동안 현재 방문하고 있는 Navigation Destination(목적지)을 담고 있는 UI 요소입니다. 이는 NavHost라고도 불리며, 앱에서 화면이 전환될 때마다 현재 화면을 Host UI 요소 안에서 교체하여 보여줍니다. </p>
<p>예를 들어, 사용자가 A 화면에서 B 화면으로 이동할 때, 네비게이션 호스트는 A 화면을 제거하고 B 화면을 새로 표시합니다. (stack에서 제거의 경우 특정한 옵션이 필요)</p>
<p>Compose의 경우 <strong>NavHost</strong>를 활용합니다.</p>
<h3 id="그래프graph">그래프(Graph)</h3>
<p>그래프는 앱 내의 모든 네비게이션 목적지들과 이 목적지들이 어떻게 서로 연결되는지를 정의하는 데이터 구조입니다. 이 네비게이션 그래프는 각 화면(목적지)을 노드로 표현하며, 사용자가 한 화면에서 다른 화면으로 넘어갈 수 있는 경로(엣지)들을 정의합니다. 이를 통해 개발자는 앱의 전체 네비게이션 구조를 쉽게 설정하고 관리할 수 있습니다.</p>
<p><strong>NavGraph</strong>를 사용하여 이 구조를 설정합니다.</p>
<h3 id="컨트롤러controller">컨트롤러(Controller)</h3>
<p>컨트롤러는 목적지 간의 네비게이션을 총괄하는 역할을 하는 컴포넌트입니다. </p>
<p><strong>NavController</strong>라는 컴포넌트가 이 역할을 맡아, 사용자가 한 화면에서 다른 화면으로 넘어갈 수 있도록 도와주며, 딥 링크 처리, 뒤로 가기 스택의 관리 등의 기능을 제공합니다. </p>
<p>즉, NavController는 앱 내에서 사용자의 위치를 파악하고, 필요에 따라 적절한 화면(목적지)으로 이동시키는 역할을 합니다.</p>
<p>이 세 가지 주요 요소를 통해 Jetpack Navigation은 사용자가 앱 내에서 원활하고 직관적으로 다른 화면으로 이동할 수 있도록 지원하며, 복잡한 네비게이션 요구 사항을 간소화하여 개발자가 더 효율적으로 작업할 수 있게 도와줍니다.</p>
<h2 id="장점-및-특징">장점 및 특징</h2>
<p>Navigation 다음을 포함하여 다양한 이점과 기능을 제공합니다.</p>
<ol>
<li>Animations과 화면 전환: 애니메이션 및 화면 전환에 대한 표준화된 리소스를 제공합니다.</li>
<li>Deep linking: 사용자를 특정 화면으로 직접 연결하는 딥링크를 구현하고 처리합니다.</li>
<li>UI 패턴: Navigation Drawables, BottomNavigation과의 통합을 간편하게 지원합니다.</li>
<li>Safe Args: 대상 간에 데이터를 탐색하고 전달할 때 유형 안전성을 제공하는 Safe Args Gradle 플러그인이 포함되어 있습니다.</li>
<li>ViewModel 지원: ViewModel을 특정 Navigation 경로(그래프)와 연결하여, 그 경로에 있는 여러 화면(목적지)에서 같은 데이터를 공유하고 접근할 수 있게 합니다.</li>
</ol>
<h2 id="jepack-navigation-구현해보기">Jepack Navigation 구현해보기</h2>
<h3 id="초기-셋팅">초기 셋팅</h3>
<p><a href="https://developer.android.com/jetpack/androidx/releases/navigation">Jetpack Navigation Releas Note</a>
app 수준의 build.gradle 파일에 아래 종속성을 추가해줍니다.</p>
<pre><code class="language-kotlin">dependencies {
  def nav_version = &quot;2.7.7&quot; // 최신 버전은 릴리즈 노트를 확인해주세요

  // Jetpack Compose Integration
  implementation &quot;androidx.navigation:navigation-compose:$nav_version&quot;
}</code></pre>
<h3 id="navigation-controller-만들기">Navigation Controller 만들기</h3>
<p>Compose에서는 NavController 만들기가 매우 간단합니다, 아래와 같이 rememberNavController() 를 사용하여 만들면 됩니다.</p>
<pre><code class="language-kotlin">val navController = rememberNavController()</code></pre>
<p>주의할 점으로는 NavController의 경우 Composable 구조의 높은 계층에서 생성되어야한다는 점입니다. 왜냐하면 모든 컴포저블이 화면 이동을 위해 해당 controller를 참조해야하기 때문이죠!</p>
<p>또한 NavController는 Composable 외부에서 상태를 업데이트하기 위해 사용될 수 있으며 이는 상태 호이스팅의 원칙을 따릅니다.</p>
<p><a href="https://developer.android.com/develop/ui/compose/state#state-hoisting">상태 호이스팅이란?
</a></p>
<h3 id="navigation-graph-만들기">Navigation Graph 만들기</h3>
<p>Navigation Graph를 만들기 위해서는 NavHost Composable을 사용해야합니다.</p>
<pre><code class="language-kotlin">NavHost(
        navController = navController,
        startDestination = Screen.Home.route
    ) {
        composable(Screen.Home.route) {
        ...
        }
        composable(Screen.Detail.route) {
        ...
        }
        composable(Screen.PlayGround.route) {
        ...
        }
    }

 sealed class Screen(val route: String) {
    data object Home : Screen(&quot;home&quot;)
    data object Detail : Screen(&quot;detail&quot;)
    data object PlayGround : Screen(&quot;playground&quot;)
}</code></pre>
<p>NavHost 컴포저블은 NavController와 시작 목적지를 인자로 받습니다. 예를 들어, startDestination에 &quot;home&quot;이라는 route 문자열을 설정하면, 앱이 시작할 때 &quot;home&quot;이라는 목적지의 화면이 먼저 보여집니다.</p>
<p><strong>네비게이션 그래프 구성</strong>
NavHost의 내부 구성은 다음과 같습니다.</p>
<pre><code class="language-kotlin">@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    route: String? = null,
    enterTransition: (AnimatedContentTransitionScope&lt;NavBackStackEntry&gt;.() -&gt; EnterTransition) =
        { fadeIn(animationSpec = tween(700)) },
    exitTransition: (AnimatedContentTransitionScope&lt;NavBackStackEntry&gt;.() -&gt; ExitTransition) =
        { fadeOut(animationSpec = tween(700)) },
    popEnterTransition: (AnimatedContentTransitionScope&lt;NavBackStackEntry&gt;.() -&gt; EnterTransition) =
        enterTransition,
    popExitTransition: (AnimatedContentTransitionScope&lt;NavBackStackEntry&gt;.() -&gt; ExitTransition) =
        exitTransition,
    builder: NavGraphBuilder.() -&gt; Unit
) </code></pre>
<p>마지막 파라메터인 builder 블럭을 활용해 NavGraphBuilder.composable()을 호출하여 네비게이션 그래프에 목적지를 추가합니다. </p>
<p>즉, 위 코드에서는 sealed class로 구성한 Screen.Home.route 를 통해 &quot;home&quot; 문자열에 접근하고 HomeScreen Composable을 보여주도록 처리합니다.</p>
<p><strong>NavController와 NavHost의 상호작용</strong>
정리해보자면 NavController는 하나의 NavHost 컴포저블과 연관되어 있습니다.
NavController를 사용하여 목적지로 이동할 때, 해당 NavController는 연결된 NavHost와 상호작용하여 화면을 전환하는 것 입니다.</p>
<h3 id="navigate-to-a-destination">Navigate to a destination</h3>
<p>Navigation을 통해 목적지로 이동하는 방법은 navController를 사용하는 것 입니다. </p>
<pre><code class="language-kotlin">@Composable
fun Navigation() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = Screen.Home.route
    ) {
        composable(Screen.Home.route) {
            HomeScreen(
                navigateToDetail = { navController.navigate(Screen.Detail.route) },
                navigateToPlayGround = { navController.navigate(Screen.PlayGround.route) }
            )
        }
        composable(Screen.Detail.route) {
            DetailScreen(
                navigateToHome = { navController.navigate(Screen.Home.route) },
                navigateToPlayGround = { navController.navigate(Screen.PlayGround.route) }
            )
        }
        composable(Screen.PlayGround.route) {
            PlayGroundScreen(
                navigateToHome = { navController.navigate(Screen.Home.route) {
                    popUpTo(Screen.Home.route) {
                        inclusive = true
                    }
                }},
                navigateToDetail = { navController.navigate(Screen.Detail.route) }
            )
        }
    }
}</code></pre>
<p>위 코드와 같이 NavController.navigate(route) 함수를 통해 특정 화면으로 이동하는 것이 가능합니다, 다만 조금 의아하신 부분이 있을텐데요.</p>
<p>왜 navController를 넘기는 방식이 아닌 람다를 통해 Composable 외부에서 navigate 을 호출하는지 궁금하실 겁니다, <strong>Navigate 동작 상태 호이스팅을 해야하는 이유</strong>는 다음과 같은데요!</p>
<ol>
<li><strong>리컴포지션 중복 호출 문제</strong>: NavController의 메소드가 리컴포지션 때마다 실행될 가능성이 있습니다. 예를 들어, 상태 변경에 따라 HomeScreen, DetailScreen, PlayGroundScreen 등이 재구성될 때마다 NavController의 메소드 호출이 발생할 수 있습니다. 이는 특히 네비게이션 명령이 자주 사용되는 상황에서 성능 저하를 일으킬 수 있습니다.</li>
<li><strong>네비게이션 로직의 분산</strong>: 각 화면이 네비게이션 로직을 갖고 있어야 하기 때문에, 네비게이션 로직이 여러 곳에 분산되어 있습니다. 이는 유지보수를 어렵게 만들고, 네비게이션 로직의 일관성을 해칠 수 있습니다.</li>
</ol>
<h3 id="navigation을-활용하여-composalbe간에-데이터를-전달하는-방법">Navigation을 활용하여 Composalbe간에 데이터를 전달하는 방법</h3>
<p>Jetpack Navigation에서는 route에 argument를 추가하는 방식을 활용하여 Composable간의 데이터를 전달할 수 있습니다.</p>
<pre><code class="language-kotlin"># NavHost 블럭 내부 코드

composable(Screen.Home.route) {
    HomeScreen(
        onSubmitUserName = { userName -&gt;
            navController.navigate(Screen.Detail.route + &quot;/$userName&quot;)
        }
    )
}
composable(
    route = Screen.Detail.route + &quot;/{userName}&quot;,
    arguments = listOf(
        navArgument(&quot;userName&quot;) {
            type = NavType.StringType
            nullable = true
            defaultValue = null
        }
    )
) { entry -&gt;
    val userName = entry.arguments?.getString(&quot;userName&quot;)
    DetailScreen(
        name = userName.orEmpty()
    )
}

@Composable
fun HomeScreen(
    onSubmitUserName: (String) -&gt; Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = 30.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        var userName by remember { mutableStateOf(&quot;&quot;) }
        Text(text = &quot;Input User Name&quot;)
        Spacer(modifier = Modifier.height(8.dp))
        TextField(
            value = userName,
            onValueChange = {
                userName = it
            })
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick = { onSubmitUserName(userName) }) {
            Text(text = &quot;Submit&quot;)
        }
    }
}
</code></pre>
<p>위 코드를 예시로 들자면 userName을 route의 path로 추가하였으며, composable 함수의 argument 파라메터로 navArgument(&quot;userName&quot;)을 통해 type, nullable, default value를 설정하여 DetailScreen Composable이 userName을 전달받을 수 있습니다.</p>
<p>직접적으로 데이터를 전달하는 부분은 navController.navigate() 함수를 통해 route를 <code>Screen.Detail.route + &quot;/$userName&quot;</code>로 전달하는 부분인데요, 쉽게 보자면 HomeScreen에서 TextField의 문자열 값을 Button Click시 navigate를 통해 전달하는 것 입니다.</p>
<h2 id="navigation-backstack-관리">Navigation BackStack 관리</h2>
<p>NavController.navigate() 메소드를 통해 BackStack을 효율적으로 관리한다면 보다 세밀한 네비게이션 제어가 가능합니다! </p>
<p>아래 에시들을 하나씩 살펴보시죠!</p>
<p>*<em>특정 목적지까지의 모든 목적지를 백 스택에서 제거한 후 다른 목적지로 이동 예제
*</em></p>
<pre><code class="language-kotlin">// &quot;destination_a&quot;까지의 모든 목적지를 백 스택에서 제거하고 &quot;destination_c&quot;로 이동
// a -&gt; b -&gt; c 스택이 쌓여야하지만
// a -&gt; c popUpTo를 활용하여 a까지의 모든 스택(b)를 제거하고 c로 이동
navController.navigate(&quot;destination_c&quot;) {
    popUpTo(&quot;destination_a&quot;)
}</code></pre>
<p>이 호출은 &quot;destination_a&quot; 이전까지의 모든 목적지를 백 스택에서 제거한 다음, &quot;destination_c&quot;로 이동합니다. &quot;destination_a&quot;는 백 스택에 남아있습니다.</p>
<p><strong>특정 목적지를 포함하여 그 이전까지의 모든 목적지를 백 스택에서 제거한 후 다른 목적지로 이동 예제</strong></p>
<pre><code class="language-kotlin">// &quot;destination_a&quot;를 포함해 그 이전까지의 모든 목적지를 백 스택에서 제거
하고 &quot;destination_c&quot;로 이동
// a -&gt; b -&gt; c
// c
navController.navigate(&quot;destination_c&quot;) {
    popUpTo(&quot;destination_a&quot;) { inclusive = true }
}</code></pre>
<p>이 설정은 &quot;destination_a&quot;를 포함하여 그 이전의 모든 목적지를 백 스택에서 제거한 후, &quot;destination_c&quot;로 이동합니다. 여기서 inclusive = true는 &quot;destination_a&quot;도 백 스택에서 제거한다는 것을 의미합니다.</p>
<p><strong>목적지가 이미 최상위에 있지 않은 경우에만 해당 목적지로 이동하여 중복 이동을 방지 예제</strong></p>
<pre><code class="language-kotlin">// &quot;search&quot; 목적지로 이동하지만, 이미 &quot;search&quot;가 최상위에 있으면 중복 추가하지 않음
navController.navigate(&quot;search&quot;) {
    launchSingleTop = true
}</code></pre>
<p>launchSingleTop = true 설정은 이미 &quot;search&quot; 목적지가 최상위에 있을 경우, 새로운 &quot;search&quot;를 스택에 추가하지 않고 기존의 것을 재사용합니다. 이는 같은 목적지가 중복으로 쌓이는 것을 방지할 수 있습니다.</p>
<p>오늘은 간단하게 기초적으로 Navigation을 구축하고 활용하는 방법을 알아보았는데요, 다음에는 NesteadNavigation, Navigation Bar, Share ViewModel 등의 개념을 살펴보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] 애니메이션 빠르게 알아보기]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 02 May 2024 14:04:00 GMT</pubDate>
            <description><![CDATA[<p>Jetpack Compose에서의 애니메이션 기능은 매우 다양하며 사용할 수 있는 여러 가지 메커니즘이 있습니다. 본 글에서는 Jetpack Compose의 애니메이션 기능을 활용하여 UI 구성요소에 다양한 애니메이션을 적용하는 방법에 대해 간략하게 설명하겠습니다.</p>
<h2 id="애니메이션의-appearing보여짐과-disappearing사라짐">애니메이션의 appearing(보여짐)과 disappearing(사라짐)</h2>
<p>AnimatedVisibility 컴포저블을 사용하면 컴포넌트를 화면에 표시하거나 숨길 때 애니메이션을 적용할 수 있습니다. 이 컴포저블은 내부적으로 자식 컴포저블의  보여짐과 사라짐을 처리합니다.</p>
<img src = "https://developer.android.com/static/develop/ui/compose/images/animations/animated_visibility_column.gif" />

<pre><code class="language-kotlin">var visible by remember {
    mutableStateOf(true)
}
AnimatedVisibility(visible) {
    // 여기에 컴포저블을 배치
}</code></pre>
<h2 id="투명도-애니메이션">투명도 애니메이션</h2>
<p>animateFloatAsState 함수를 사용하여 컴포저블의 투명도에 시간에 따라 변화하는 애니메이션을 적용할 수 있습니다. 이 방법은 컴포저블이 화면에서 보이지 않을 때도 공간을 차지한다는 단점이 있습니다.</p>
<img src = "https://developer.android.com/static/develop/ui/compose/images/animations/animated_visibility_alpha.gif"/>

<pre><code class="language-kotlin">val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f
)

Box(
    modifier = Modifier
        .alpha(animatedAlpha)
        .background(Color.Green)
)</code></pre>
<h2 id="배경색-애니메이션">배경색 애니메이션</h2>
<p>animateColorAsState 함수를 이용하면 배경색에 부드러운 색 변화 애니메이션을 적용할 수 있습니다. 이는 Modifier.background를 사용하는 것보다 성능이 우수합니다.</p>
<img src = "https://developer.android.com/static/develop/ui/compose/images/animations/animated_forever.gif" />

<pre><code class="language-kotlin">val animatedColor by animateColorAsState(
    targetValue = if (visible) Color.Green else Color.Blue
)

Box(
    modifier = Modifier.background(animatedColor)
)</code></pre>
<h2 id="컴포저블-크기에-애니메이션-적용">컴포저블 크기에 애니메이션 적용</h2>
<p>animateContentSize를 사용하면 컴포저블의 크기 변화에 애니메이션을 적용할 수 있습니다. 이는 크기 변경이 자연스럽게 느껴지도록 합니다.</p>
<img src= "https://developer.android.com/static/develop/ui/compose/images/animations/animated_content_size.gif"/>


<pre><code class="language-kotlin">var expanded by remember { mutableStateOf(false) }

Box(
    modifier = Modifier
        .animateContentSize()
        .size(if (expanded) Dp(400) else Dp(200))
        .clickable { expanded = !expanded }
)</code></pre>
<h2 id="컴포저블의-위치-애니메이션">컴포저블의 위치 애니메이션</h2>
<p>컴포저블의 위치 변경에도 애니메이션을 적용할 수 있습니다. animateIntOffsetAsState와 Modifier.offset을 조합하여 사용하세요.
<img src ="https://developer.android.com/static/develop/ui/compose/images/animations/animated_offset.gif" /></p>
<pre><code class="language-kotlin">var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = &quot;offset&quot;
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)
</code></pre>
<p>이상의 내용은 Jetpack Compose를 사용하여 애니메이션을 적용하는 간단한 예시입니다. Compose는 훨씬 다양한 애니메이션 옵션을 제공하므로, 보다 복잡한 UI 상호작용과 생동감 있는 애니메이션을 구현할 수 있습니다. Jetpack Compose의 전체 애니메이션 기능을 활용하려면 공식 문서를 참조하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] state(상태)와 remember 그리고 MutableState 란?]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-state%EC%83%81%ED%83%9C%EC%99%80-remember-%EA%B7%B8%EB%A6%AC%EA%B3%A0-MutableState-%EB%9E%80</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-state%EC%83%81%ED%83%9C%EC%99%80-remember-%EA%B7%B8%EB%A6%AC%EA%B3%A0-MutableState-%EB%9E%80</guid>
            <pubDate>Thu, 25 Apr 2024 14:29:53 GMT</pubDate>
            <description><![CDATA[<p>이미 이 글을 읽는 당신은 너무나 잘 알겠지만 ^&amp;^ Jetpack Compose는 선언형 UI입니다. </p>
<p>Jetpack Compose에서 StateFul한, 즉 데이터 변경 가능성이 있는 UI에 데이터를 Binding하기 위해서는 <strong>State(상태)</strong>라는 개념을 활용하여 데이터를 갱신 해주어야 데이터 최신화가 가능합니다.</p>
<p>이러한 과정에서 필요한 상태 관리는 Android 개발 패러다임에 많은 변화를 가져왔다고 개인적으로도 많이 느끼고 있어 이와 같은 주제를 택하게 되었습니다</p>
<p>Jetpack Compose는 이러한 State(상태)를 보다 명확하게 관리하도록 도와주는 다양한 API를 제공합니다. 오늘은 Jetpack Compose에서 State와 State를 관리하는 핵심 기능 중 하나인 remember에 대해 자세히 설명해보겠습니다!</p>
<h2 id="state-and-composition">State and composition</h2>
<p>Jetpack Compose에서의 상태 관리는 여러 방식으로 이루어지며, 이 중에서 <strong>remember와 mutableStateOf API</strong>는 컴포저블 함수에서 상태를 효과적으로 다루는 데 중요한 역할을 합니다.</p>
<h3 id="state상태란">State(상태)란?</h3>
<p>앱에서 시간에 따라 변할 수 있는 모든 값들을 의미합니다. </p>
<blockquote>
<p>예시💡</p>
</blockquote>
<ul>
<li>네트워크 연결이 설정되지 않았을 때 나타나는 스낵바</li>
<li>블로그 포스트와 관련 댓글</li>
<li>사용자가 클릭할 때 재생되는 버튼의 리플 애니메이션</li>
<li>이미지 위에 사용자가 그릴 수 있는 스티커</li>
</ul>
<p>특히 Compose는 선언형 UI 이므로 이를 업데이트하는 유일한 방법은 새 인수로 동일한 컴포저블을 호출하는 것입니다. </p>
<p>새 인수로 컴포저블을 다시 호출하게 된다면 상태가 업데이트될 때마다 <strong>recomposition(재구성)이 발생</strong>합니다. </p>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-kotlin">@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = &quot;Hello!&quot;,
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = &quot;&quot;,
            onValueChange = { },
            label = { Text(&quot;Name&quot;) }
        )
    }
}</code></pre>
<p>그렇다면 해당 예시 코드를 잠깐 보실까요?</p>
<p>결과적으로, XML 기반의 명령형 뷰에서처럼 TextField와 같은 요소들이 자동으로 업데이트되지 않습니다. 컴포저블은 새로운 상태를 명시적으로 전달받아야 해당 상태에 맞게 업데이트될 수 있습니다.</p>
<p>그 이유는 TextField 컴포저블이 자체적으로 업데이트되지 않고 value 매개변수가 변경될 때 업데이트되기 때문입니다. <strong>이는 Compose에서 Composition 및 Recomposition 작동하는 방식 때문</strong>입니다.</p>
<blockquote>
<p>핵심 용어 📖</p>
</blockquote>
<ul>
<li>구성(Composition): Compose 함수들이 실행되어 UI 요소를 트리 구조로 구성하는 것을 포함합니다.</li>
<li>초기 구성(Initial Composition):
이는 컴포저블 함수들이 처음 실행될 때 일어나는 과정입니다. 이때, Compose는 주어진 컴포저블 함수들을 통해 애플리케이션의 UI를 구축하고, 각 UI 요소를 적절한 위치와 함께 트리에 추가합니다.</li>
<li>재구성(Recomposition):
데이터나 상태가 변경될 때, Compose는 영향 받은 UI 부분만을 효율적으로 업데이트하기 위해 해당 컴포저블 함수를 다시 실행합니다. 이 과정을 통해 앱은 최신 상태를 반영하여 사용자에게 보여질 수 있습니다.</li>
</ul>
<h2 id="remember의-역할">remember의 역할</h2>
<p>Composable에서 State를 관리하는 방법은 대표적으로 remember가 있는데요, remember API의 기본적인 기능은 객체를 메모리에 저장하는 기능입니다.</p>
<p>이를 통해 remember는 컴포저블이 초기 구성(Initial Composition)에서 실행될 때 계산된 값을 <strong>&quot;기억&quot;</strong>하고, 재구성 시에 이 값을 <strong>유지하여 반환</strong>합니다. 이는 Compose의 효율성을 높여주는 메커니즘으로, 불필요한 계산을 방지하고 성능을 최적화합니다.</p>
<p>따라서, remember를 사용할 때는 그것이 재구성 시에 다시 호출되지 않고, 초기에 저장된 값을 계속해서 반환한다는 점을 이해하는 것이 중요합니다. 이는 상태 관리를 효과적으로 돕고, Compose UI의 성능을 유지하는 데 도움을 줍니다.</p>
<blockquote>
<p>remember는 해당 객체를 Composition에 저장하며, remember를 호출한 컴포저블이 Composition에서 제거되면 저장된 객체를 &quot;잊어버립니다&quot;(즉, 객체를 메모리에서 제거합니다).</p>
</blockquote>
<h2 id="mutablestate의-사용">MutableState의 사용</h2>
<p>mutableStateOf는 변화 가능한 상태를 생성하고, 이 상태가 변경될 때 관련된 컴포저블 함수들을 재구성하도록 합니다. 이는 Compose 런타임과 통합된 관찰 가능한 타입(MutableState<T>)을 통해 이루어집니다.</p>
<h3 id="예시-코드-1">예시 코드</h3>
<pre><code class="language-kotlin">interface MutableState&lt;T&gt; : State&lt;T&gt; {
    override var value: T
}
</code></pre>
<h3 id="mutablestate의-recomposition재구성-유도">MutableState의 Recomposition(재구성) 유도</h3>
<blockquote>
<p>오잉 근데 MutableState는 어떻게 상태가 변경된 것을 컴포저블에 알리고 함수를 재구성 할 수 있도록 하는건가요??</p>
</blockquote>
<p>mutableStateOf를 통해 생성된 MutableState<T> 객체가 컴포저블 함수들을 재구성(recomposition)할 수 있는 주된 이유는 이 객체가 <strong>관찰 가능한 상태(observable state)</strong>를 제공하기 때문입니다. </p>
<h3 id="관찰-가능한-상태-observable-state">관찰 가능한 상태 (Observable State)</h3>
<p>MutableState<T>는 상태의 변화를 자동으로 감지하고 이에 반응할 수 있는 구조로 설계되어 있습니다. 이 객체의 value 속성에 변화가 발생하면, Compose 런타임은 그 변화를 감지하고 value를 사용하는 모든 컴포저블 함수들을 자동으로 재구성하도록 스케줄링합니다. </p>
<p>이러한 방식은 다음과 같은 흐름으로 작동합니다:</p>
<ol>
<li>상태 변경 감지: MutableState<T> 내의 value가 변경될 때, 이 상태 객체는 등록된 리스너들(여기서는 컴포저블 함수들)에게 변화를 알립니다.<ol start="2">
<li>Recomposition 트리거: 상태를 사용하는 컴포저블 함수들은 변화를 통지받고, 필요에 따라 자신을 재구성하기 위한 요청을 Compose 런타임에 보냅니다.</li>
<li>최소한의 UI 업데이트: Compose는 선언적 UI 접근 방식을 사용하여, 실제로 변경이 필요한 UI 부분만을 업데이트합니다. 이는 전체 UI를 다시 그리는 대신, 변경된 데이터에 의존하는 UI 요소만을 재구성하여 성능을 최적화합니다.</li>
</ol>
</li>
</ol>
<p>MutableState 객체를 선언하는 방법은 아래와 같이 세 가지가 있습니다:</p>
<pre><code class="language-kotlin">val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }</code></pre>
<h3 id="🔫-간단히-짚어보는-구조-분해-선언-componentn-함수들의-사용">🔫 간단히 짚어보는 구조 분해 선언 (componentN 함수들의 사용)</h3>
<p>Kotlin에서 구조 분해 선언은 복합적인 값을 각각의 변수로 분해하여 할당할 수 있게 해줍니다.</p>
<p>  각 요소를 개별 변수에 할당하려면 해당 타입은 component1(), component2(), 등의 메서드를 제공해야 합니다.</p>
<p>MutableState<T>는 아래와 같은 두 개의 메서드를 통해 구조 분해를 지원합니다</p>
<ul>
<li>component1() 함수는 MutableState<T>의 value를 반환합니다. 이는 상태의 현재 값을 얻는 데 사용됩니다.</li>
<li>component2() 함수는 value의 값을 설정하는 함수를 반환합니다. 이는 상태를 업데이트할 때 사용되는 세터(setter) 함수입니다.</li>
</ul>
<p>이 두 함수 덕분에, MutableState<T> 객체는 구조 분해를 통해 value와 세터 함수를 쉽게 할당할 수 있게 되며, 이는 코드의 가독성과 사용의 용이성을 높입니다.</p>
<h2 id="remember와-remembersaveable의-차이">remember와 rememberSaveable의 차이</h2>
<p>remember는 Recomposition을 거쳐도 상태를 유지하지만, Configuration Change 즉, 구성 변경(예: 화면 회전)이 발생하면 상태를 유지하지 못합니다. 이에 대한 해결책으로 rememberSaveable을 사용할 수 있습니다. </p>
<p>  rememberSaveable은 자동으로 Bundle에 저장될 수 있는 값들을 저장하며, 커스텀 저장 객체를 통해 다른 유형의 값들도 저장할 수 있습니다.</p>
<p>아래는 remember와 mutableStateOf를 사용한 간단한 예시입니다:</p>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-kotlin">  @Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        val (name, setName) = remember { mutableStateOf(&quot;&quot;) }
        if (name.isNotEmpty()) {
            Text(
                text = &quot;Hello, $name!&quot;,
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = setName,
            label = { Text(&quot;Name&quot;) }
        )
    }
}</code></pre>
<p>이 예시에서는 사용자가 입력한 이름을 저장하고, 이름이 비어 있지 않은 경우에만 Text를 표시합니다.</p>
<h3 id="주의사항">주의사항</h3>
<p>가변 객체를 상태로 사용하는 것은 데이터가 잘못되거나 오래된 것으로 보이게 할 수 있으므로, 가능하면 State&lt;List<T>&gt;와 같은 관찰 가능한 데이터 홀더와 listOf()와 같은 불변 컬렉션을 사용하는 것이 좋습니다.</p>
<blockquote>
<p>더 나아가 ImmutableList가 무엇인지도 살펴보면 좋을 것 같습니다! Compose에서는 List또한 안정된 타입이 아니기에 불필요한 리컴포지션이 발생할 가능성이 있기 때문이죠!</p>
</blockquote>
<p>Jetpack Compose의 remember와 mutableStateOf는 Compose의 선언적 UI 패턴에 맞게 상태를 관리하고 UI를 동적으로 API입니다. </p>
<p>  이로 인해서 저희 안드로이드 개발자들은 쉽게 사용자 경험을 향상시키고, 코드의 복잡성을 줄일 수 있다고 생각됩니다! 다같이 열심히 공부해서 좋은 프로젝트 설계 실력을 만들어 나가봅시다!</p>
<p><a href="https://developer.android.com/develop/ui/compose/state">참고 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] Compose에서 BottomSheet 만드는 방법 (ModalBottomSheet)]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-Compose%EC%97%90%EC%84%9C-BottomSheet-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95-ModalBottomSheet</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-Compose%EC%97%90%EC%84%9C-BottomSheet-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95-ModalBottomSheet</guid>
            <pubDate>Thu, 18 Apr 2024 11:59:53 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Compose에서 ModalBottomSheet를 만드는 방법에 대해 포스팅하고자합니다.</p>
<p>해당 예제들은 Material 3 환경에서 제작된 점 참고해주시면 감사하겠습니다</p>
<h2 id="modalbottomsheet-composable-사용하기">ModalBottomSheet Composable 사용하기</h2>
<p>보통 Android 기본 디자인 시스템을 활용하여 BottomSheet를 제작할때는 ModalBottomSheet 키워드가 들어간 컴포저블을 활용합니다.
대표적으로 ModalBottomSheetLayout과 ModalBottomSheet가 있는데요!!</p>
<h3 id="modalbottomsheet란">ModalBottomSheet란?</h3>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/694afe0d-c2a6-4137-8b91-481211d345b6/image.png" alt=""></p>
<p>ModalBottomSheet란 안드로이드에서 BottomSheet을 구현하기 위해 제공하는 컴포저블입니다.</p>
<p>아래 예제를 통해 같이 살펴보시죠!  </p>
<pre><code class="language-kotlin">ModalBottomSheet(onDismissRequest = { /* 바텀 시트가 Dismiss된 경우 호츌*/ }) {
    // Sheet content
}</code></pre>
<p>기본적인 형태를 살펴본다면 ModalBottomSheet는  onDismissRequest를 람다를 매개변수로 필수 구현해야하는 컴포저블임을 알 수 있습니다.</p>
<p>내부 Sheet content는 기본적으로 Column Scope로 되어있기 때문에 수직적인 구조의 컴포저블을 간편하게 배치 할 수 있습니다.</p>
<h3 id="modalbottomsheet의-특징">ModalBottomSheet의 특징</h3>
<p>그런데 보통 BottomSheet의 특정 조건에서만 보여져야하는 특성을 가지고 있습니다. 그렇기 때문에 보통 Boolean State 변수를 통해 Visible을 관리하게 됩니다.</p>
<pre><code class="language-kotlin">// 예시
var showBottomSheet by remember { mutableStateOf(false) }
if (showBottomSheet) {
    ModalBottomSheet(
        onDismissRequest = {
            showBottomSheet = false
        }
      ) {
        // Sheet content
        Text(&quot;Hello&quot;)
        Spacer(modifier = Modifier.height(100.dp))
    }
}
</code></pre>
<img src="https://velog.velcdn.com/images/kej_ad/post/a2a9e5db-b34c-4c9c-af0d-b74ac7acd970/image.gif" width="30%" height="10%">




<h3 id="modalbottomsheet-sheetstate">ModalBottomSheet SheetState</h3>
<p> 하지만 위와 같은 Boolean 변수 만을 사용해서는 제한적인 컨트롤만 가능합니다. </p>
<p> 예를 들어 버튼을 통해서 BottomSheet를 숨기는 작업을 하고 싶을 때 showBottomSheet를 false로 설정만 하게 된다면 말 그대로 BottomSheet가 축소되는 것이 아닌 바로 사라져버리는 부자연스러운 현상이 발생되는 것이죠 ㅠ.ㅠ</p>
<pre><code class="language-kotlin">if (showBottomSheet) {
        ModalBottomSheet(
            onDismissRequest = {
                showBottomSheet = false
            }
        ) {
            // Sheet content
            Button(onClick = {
                showBottomSheet = false
            }) {
                Text(&quot;Hide bottom sheet&quot;)
            }
            Spacer(modifier = Modifier.height(100.dp))
        }
    }</code></pre>
<img src="https://velog.velcdn.com/images/kej_ad/post/157c3298-853a-4bf8-a3c0-13884842e636/image.gif" width="30%">


<p>그렇다면 어떤 방법을 사용해야 자연스러운 BottomSheet의 동작 컨트롤이 가능할까요? </p>
<p>ModalBottomSheet는 또한 확장과 축소 동작의 컨트롤을 SheetState를 통해서 관리하며 <strong>rememberSheetState</strong> 인스턴스를 통해서 사용할 수 있습니다!!</p>
<p>아래 예제를 같이 보시죠 허허</p>
<pre><code class="language-kotlin">val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
Scaffold(
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text(&quot;Show bottom sheet&quot;) },
            icon = { Icon(Icons.Filled.Add, contentDescription = &quot;&quot;) },
            onClick = {
                showBottomSheet = true
            }
        )
    }
) { contentPadding -&gt;
    // Screen content

    if (showBottomSheet) {
        ModalBottomSheet(
            onDismissRequest = {
                showBottomSheet = false
            },
            sheetState = sheetState
        ) {
            // Sheet content
            Button(onClick = {
                scope.launch { sheetState.hide() }.invokeOnCompletion {
                    if (!sheetState.isVisible) {
                        showBottomSheet = false
                    }
                }
            }) {
                Text(&quot;Hide bottom sheet&quot;)
                Spacer(modifier = Modifier.height(100.dp))
            }
        }
    }
}</code></pre>
<img src="https://velog.velcdn.com/images/kej_ad/post/d87d463d-acb3-4e30-9195-8883d8dad130/image.gif" width="30%">


<p>해당 예제에서 <strong>coroutine scope</strong>을 사용한 이유는 <strong>hide()가 suspend 함수</strong>이기 때문입니다. </p>
<p>또한 invokeOnCompletion 을 통해 hide(BottomSheet가 축소되는 동작과 애니메이션) 함수가 완료되면 기존에 활용하연 s<strong>howBottomSheet 변수 또한 false로 초기화</strong>를 하는 것입니다.</p>
<p>이렇게 되면 이제 자연스러움 BottomSheet의 동작을 컨트롤 할 수 있게 됩니다!!</p>
<p>이상으로 포스팅을 마치도록 하겠습니다 ㅎㅎ 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] TabRow Indicator Color 변경 및 커스텀]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-TabRow-Indicator-Color-%EB%B3%80%EA%B2%BD-%EB%B0%8F-%EC%BB%A4%EC%8A%A4%ED%85%80</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-TabRow-Indicator-Color-%EB%B3%80%EA%B2%BD-%EB%B0%8F-%EC%BB%A4%EC%8A%A4%ED%85%80</guid>
            <pubDate>Thu, 08 Feb 2024 06:11:15 GMT</pubDate>
            <description><![CDATA[<h2 id="tabrow-색상-지정">TabRow 색상 지정</h2>
<p>Android의 Material3 디자인 시스템에서는 TabRow의 indicator 파라미터를 통해, TabRowDefaults 객체를 사용하여 기본 Indicator를 구현할 수 있습니다. </p>
<p>이 객체는 다양한 스타일의 탭 Indicator를 제공하며, 해당 글에서는 SecondaryIndicator를 사용하여 Indicator 색상을 변경하는 방법을 소개하겠습니다.</p>
<h2 id="material3-tabrow와-tab">Material3 TabRow와 Tab</h2>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/6c712bd3-335e-4a81-90b7-726d280d5c46/image.png" alt="">
위 사진과 같이 Material3 디자인에서는 Primary, Secondary Tab 디자인이 구분되어있으며,  Tab Indicator를 통해 선택된 탭을 구분할 수 있습니다. </p>
<h2 id="예제-코드">예제 코드</h2>
<pre><code class="language-kotlin">    val selectedIndex = remember { mutableStateOf(0) }
    val days = listOf(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;,).toPersistentList()

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(
            selectedTabIndex = selectedIndex.value,
            containerColor = Color.Gray,
            contentColor = Color.White,
            indicator = { tabPositions -&gt;
                TabRowDefaults.SecondaryIndicator(
                    modifier = Modifier.tabIndicatorOffset(tabPositions[selectedIndex.value]),
                    color = Color.Red
                )
            }
        ) {
            days.forEachIndexed { index, day -&gt;
                Tab(
                    selected = selectedIndex.value == index,
                    onClick = { selectedIndex.value = index },
                    modifier = Modifier.padding(16.dp)
                ) {
                    Text(text = &quot;Day ${day}&quot;)
                }
            }
        }
    }</code></pre>
<h3 id="preview">Preview</h3>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/2d9c2d37-d2d3-4298-b34d-58c19821c73b/image.png" alt=""></p>
<h2 id="구현-방법">구현 방법</h2>
<p>예제 코드와 사진에서 보신다면 아시겠지만 container, content Color는 각 Tab의 바탕 색상과 내부 아이콘 및 글자 색상인 것을 유추할 수 있을 것입니다.</p>
<p>또한 SecondaryIndicator의 파라메터를 보신다면 tabIndicatorOffset 함수를 통해 indicator의 위치를 TabPosition을 활용하여 index에 따라 그려지는 것을 볼 수 있습니다.</p>
<p>이렇게 그려지는 indicator의 색상을 color 파라메터를 통해 컨트롤 한다면 <strong>TabRow 내의 Indicator 색상을 변경</strong>할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/c29b60a2-881e-48f6-83a6-c45fe62ef7df/image.gif" alt=""></p>
<p>앞으로 간략히 Compose 관련된 포스팅을 지속할 예정입니다. 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ Kotlin의 object 및 companion object 와 Java의 static 의 차이점]]></title>
            <link>https://velog.io/@kej_ad/Kotlin%EC%9D%98-object-%EB%B0%8F-companion-object-%EC%99%80-Java%EC%9D%98-static-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@kej_ad/Kotlin%EC%9D%98-object-%EB%B0%8F-companion-object-%EC%99%80-Java%EC%9D%98-static-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Wed, 18 Oct 2023 14:36:10 GMT</pubDate>
            <description><![CDATA[<h1 id="1-static-java"><strong>1. static (Java)</strong></h1>
<p><strong>정의:</strong> Java에서, <strong><code>static</code></strong>은 &#39;클래스 레벨&#39;에서 변수나 메서드를 정의할 때 사용되는 수식어입니다.</p>
<p><strong>특징:</strong></p>
<ul>
<li><strong>메모리:</strong> <strong><code>static</code></strong> 멤버는 클래스가 메모리에 로드될 때 초기화됩니다. 그 결과, 클래스의 모든 인스턴스가 공유하는 하나의 메모리 위치에 존재합니다.</li>
<li><strong>접근:</strong> 클래스 인스턴스 없이도 접근이 가능합니다.</li>
<li><strong>제한:</strong> Java에서는 <strong><code>static</code></strong> 클래스를 정의할 수 없습니다. 오직 변수, 메서드, 중첩된 내부 클래스만 <strong><code>static</code></strong>으로 선언될 수 있습니다.</li>
</ul>
<pre><code class="language-kotlin">public class Example {
    public static int a = 10;

    public static void staticMethod() {
        System.out.println(&quot;staticMethod 입니다&quot;);
    }
}</code></pre>
<h1 id="2-object-kotlin"><strong>2. object (Kotlin)</strong></h1>
<p><strong>정의:</strong> <strong><code>object</code></strong>는 Kotlin에서 싱글턴 패턴을 직접 구현하는 키워드입니다. 이는 특정 클래스의 단 하나의 인스턴스만을 생성하고 사용하고자 할 때 사용됩니다.</p>
<p><strong>특징:</strong></p>
<ul>
<li><p><strong>메모리:</strong> <strong><code>object</code></strong>는 첫 접근 시점에 생성되며, 프로그램의 생명주기 동안 그 인스턴스는 유지됩니다.</p>
</li>
<li><p><strong>생성자:</strong> 별도의 생성자를 정의할 수 없습니다. 즉, <strong><code>object</code></strong>는 파라미터를 가진 생성자를 갖지 않습니다.</p>
</li>
<li><p><strong>확장:</strong> <strong><code>object</code></strong>는 다른 클래스나 인터페이스를 상속하거나 구현할 수 있습니다.</p>
</li>
<li><p><strong>Object expressions and declarations:</strong> object는 객체 선언, 익명 객체로 정의될 수 있습니다.</p>
<p>  <a href="https://kotlinlang.org/docs/object-declarations.html">Object expressions and declarations | Kotlin</a></p>
</li>
</ul>
<pre><code class="language-kotlin">object KotlinSingleton {
    val singletonVar: Int = 10

    fun singletonMethod() {
        println(&quot;This is inside a singleton object.&quot;)
    }
}</code></pre>
<h3 id="3-companion-object-kotlin"><strong>3. companion object (Kotlin)</strong></h3>
<p><strong>정의:</strong> <strong><code>companion object</code></strong>는 Kotlin 클래스 내부에서 &#39;클래스 레벨&#39;의 함수나 변수를 정의하기 위해 사용됩니다.</p>
<p><strong>특징:</strong></p>
<ul>
<li><strong>메모리:</strong> <strong><code>companion object</code></strong>의 멤버들은 해당 클래스의 인스턴스 없이 접근 가능합니다. 이는 Java의 <strong><code>static</code></strong>과 비슷한 역할을 합니다.</li>
<li><strong>명명:</strong> 기본적으로 <strong><code>Companion</code></strong>이라는 이름을 갖지만, 필요에 따라 다른 이름을 지정할 수 있습니다.</li>
<li><strong>확장:</strong> <strong><code>companion object</code></strong>는 다른 클래스나 인터페이스를 상속하거나 구현할 수 있습니다.</li>
<li><strong>디컴파일:</strong> 디컴파일시에 Companion이라는 <code>static</code> 중첩 클래스의 인스턴스가 생성되는 것이기 때문에 java의 <code>static</code> 변수, 함수와는 엄연히 다릅니다.</li>
</ul>
<pre><code class="language-kotlin">class KotlinExample {
    companion object {
        val companionVar: Int = 10

        fun companionMethod() {
            println(&quot;This is inside a companion object.&quot;)
        }
    }
}

// 디컴파일

public final class KotlinExample {
   @NotNull
   private static final int companionVar = 10;
   @NotNull
   public static final KotlinExample.Companion Companion = new KotlinExample.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      @NotNull
      public final String getcompanionVar() {
         return User.companionVar;
      }

      public final void companionMethod() {
         println(&quot;This is inside a companion object.&quot;);
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
   }
}</code></pre>
<h1 id="✅-결론">✅ 결론</h1>
<p>Java의 <strong><code>static</code></strong>은 클래스 레벨의 변수나 메서드를 위해 사용되며, Kotlin에서는 <strong><code>companion object</code></strong>로 동일한 기능을 수행한다고 보시면 됩니다.  다만 <code>**companion object**</code>는 <code>**static**</code>과 달리 객체로써 생성이 된다는 것 입니다. 그렇기에 <strong><code>@JvmField</code>, <code>@JvmStatic</code></strong> 다음과 같은 어노테이션을 통해 자바의 static 변수와 같이 사용할 수 있습니다.</p>
<p><strong><code>object</code></strong>는 Kotlin에서 싱글턴 패턴의 구현을 위해 사용되며, 이를 통해 하나의 인스턴스만을 갖는 객체를 생성할 수 있습니다. → <strong><a href="https://www.notion.so/92dd7c0ad834458ba7a9c4b30e946e51?pvs=21">참고: 불필요한 객체 생성의 피해</a></strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Kotlin] Kotlin KTX를 이용한 SharedPreferences 활용하기]]></title>
            <link>https://velog.io/@kej_ad/Kotlin-KTX%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-SharedPreferences-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/Kotlin-KTX%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-SharedPreferences-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 17 Oct 2023 06:25:31 GMT</pubDate>
            <description><![CDATA[<h1 id="kotlin-ktx를-이용한-sharedpreferences-활용하기"><strong>Kotlin KTX를 이용한 SharedPreferences 활용하기</strong></h1>
<h2 id="🚆-ktx란">🚆 KTX란?</h2>
<p>Kotlin Android Extensions의 약자로 Android API에 대한 Kotlin 확장을 제공하는 라이브러리입니다.</p>
<h2 id="📝-sharedpreferences-란">📝 <strong>SharedPreferences 란?</strong></h2>
<p>Android에서는 사용자의 설정이나 앱의 내부 상태를 (key, value) 형식으로 저장하는 방법 중 하나로 <strong><code>SharedPreferences</code></strong>를 사용합니다.</p>
<h2 id="kotlin-ktx-기본-설정"><strong>Kotlin KTX 기본 설정</strong></h2>
<p>먼저, <strong><code>SharedPreferences</code></strong>를 KTX와 함께 사용하려면 의존성을 추가해야 합니다.</p>
<pre><code class="language-kotlin">// build.gradle(app)
dependencies {
    implementation &quot;androidx.core:core-ktx:x.y.z&quot; // 버전은 프로젝트 상황에 맞게~!!
}</code></pre>
<h2 id="사용-방법"><strong>사용 방법</strong></h2>
<h3 id="기본-sharedpreferences-생성"><strong>기본 SharedPreferences 생성</strong></h3>
<pre><code class="language-kotlin">val sharedPreferences = context.getSharedPreferences(&quot;my_prefs&quot;, Context.MODE_PRIVATE)</code></pre>
<p>KTX를 사용하면 아래와 같이 간단하게 가져올 수 있습니다.</p>
<pre><code class="language-kotlin">val sharedPreferences = context.defaultSharedPreferences</code></pre>
<h3 id="값-저장하기"><strong>값 저장하기</strong></h3>
<p>기존 방법:</p>
<pre><code class="language-kotlin">with(sharedPreferences.edit()) {
    putString(&quot;key&quot;, &quot;value&quot;)
    apply()
}</code></pre>
<p>KTX를 사용한 방법:</p>
<pre><code class="language-kotlin">sharedPreferences.edit {
    putString(&quot;key&quot;, &quot;value&quot;)
}</code></pre>
<p>Q: ?? 근데 <strong>저장</strong>은 어떻게 하나요?? apply하고 commit이 보이지 않는데요??</p>
<p>A: KTX 확장 함수는 내부적으로 <strong><code>apply()</code></strong>를 호출하므로 별도로 호출할 필요가 없습니다.</p>
<pre><code class="language-kotlin">@SuppressLint(&quot;ApplySharedPref&quot;)
public inline fun SharedPreferences.edit(
    commit: Boolean = false,
    action: SharedPreferences.Editor.() -&gt; Unit
) {
    val editor = edit()
    action(editor)
    if (commit) {
        editor.commit()
    } else {
        editor.apply()
    }
}</code></pre>
<aside>
💡 commit을 호출하기 위해서는 commit 파라메터를 true로 설정해주시면 됩니다.

<p>apply와 commit의 차이를 아시는 분께 저와 점심을 드실 수 있는 기회를 드리겠습니다. 물론 사드리는건 아닙니다 하핳</p>
</aside>

<h3 id="값-가져오기"><strong>값 가져오기</strong></h3>
<pre><code class="language-kotlin">val value = sharedPreferences.getString(&quot;key&quot;, &quot;default_value&quot;)</code></pre>
<p>KTX를 사용하면 위 코드와 동일하게 동작합니다. 이 부분은 KTX 확장이 없어도 동일하게 사용됩니다.</p>
<h3 id="값-삭제하기"><strong>값 삭제하기</strong></h3>
<p>기존 방법:</p>
<pre><code class="language-kotlin">with(sharedPreferences.edit()) {
    remove(&quot;key&quot;)
    apply()
}</code></pre>
<p>KTX를 사용한 방법:</p>
<pre><code class="language-kotlin">sharedPreferences.edit {
    remove(&quot;key&quot;)
}</code></pre>
<h2 id="✅-결론">✅ <strong>결론</strong></h2>
<p>Kotlin KTX는 Android의 여러 API에 대한 편리한 확장을 제공합니다. <strong><code>SharedPreferences</code></strong> 도 이러한 KTX를 활용하여 코드를 더 간결하고 읽기 쉽게 만들어봐요!! </p>
<p>특히 기존 코드와 KTX를 활용한 코드를 비교하면서 차이점을 확인하면 좋겠습니다~!!! 다들 화이팅</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Kotlin] 상속과 위임 feat. ViewBinding]]></title>
            <link>https://velog.io/@kej_ad/AndroidKotlin-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%9C%84%EC%9E%84</link>
            <guid>https://velog.io/@kej_ad/AndroidKotlin-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%9C%84%EC%9E%84</guid>
            <pubDate>Fri, 13 Oct 2023 17:08:29 GMT</pubDate>
            <description><![CDATA[<h2 id="✅-상속-inheritance">✅ 상속 (Inheritance)</h2>
<p>상속은 기본적으로 객체 지향 프로그래밍의 핵심 개념 중 하나로, 하나의 클래스가 다른 클래스의 속성 및 메서드를 이어받아 사용하는 것을 의미합니다.</p>
<h3 id="✓-기본-상속의-사용">✓ 기본 상속의 사용</h3>
<p>Kotlin에서는 :를 사용해 상속을 표현합니다.</p>
<pre><code class="language-kotlin">open class Parent {
    fun parentFunction() {
        println(&quot;This is a parent function&quot;)
    }
}

class Child : Parent()

fun main() {
    val child = Child()
    child.parentFunction() // This is a parent function
}</code></pre>
<blockquote>
<p>✅ 여기서 open 키워드는 Parent 클래스가 상속 가능하다는 것을 나타냅니다. Kotlin에서는 기본적으로 모든 클래스가 final로 선언되어 있어, 상속을 위해서는 open을 명시해야 합니다.</p>
</blockquote>
<h2 id="✅-위임-delegation">✅ 위임 (Delegation)</h2>
<p>위임은 특정 객체의 기능이나 속성의 사용 권한을 다른 객체에게 넘기는 패턴을 의미합니다. Kotlin에서는 위임 패턴을 쉽게 구현할 수 있도록 by 키워드를 제공합니다.</p>
<h3 id="property-위임">Property 위임</h3>
<p>Kotlin에서 속성 위임은 프로퍼티의 getter와 setter를 다른 객체로 위임하는 것입니다.</p>
<pre><code class="language-kotlin">class Example {
    var p: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty&lt;*&gt;): String {
        return &quot;$thisRef, thank you for delegating &#39;${property.name}&#39; to me!&quot;
    }

    operator fun setValue(thisRef: Any?, property: KProperty&lt;*&gt;, value: String) {
        println(&quot;$value has been assigned to &#39;${property.name}&#39; in $thisRef.&quot;)
    }
}</code></pre>
<h3 id="위임을-사용한-구현inheritance-by-delegation">위임을 사용한 구현(Inheritance by Delegation)</h3>
<p>상속은 <strong>&quot;is-a&quot;</strong> 관계를 의미합니다. 예를 들어 Bird는 Animal이라고 할 수 있습니다. 하지만 때로는 &quot;has-a&quot; 관계로도 많은 일을 수행할 수 있습니다. </p>
<p>예를 들어, Car는 Engine을 갖고 있다고 할 수 있죠. 위임은 이 <strong>&quot;has-a&quot;</strong> 관계를 활용하여 특정 객체의 기능을 다른 객체에 위임하는 것을 말합니다.</p>
<pre><code class="language-kotlin">interface Engine {
    fun start()
    fun stop()
}

class DieselEngine : Engine {
    override fun start() {
        println(&quot;Diesel Engine started!&quot;)
    }

    override fun stop() {
        println(&quot;Diesel Engine stopped!&quot;)
    }
}

class ElectricEngine : Engine {
    override fun start() {
        println(&quot;Electric Engine started!&quot;)
    }

    override fun stop() {
        println(&quot;Electric Engine stopped!&quot;)
    }
}</code></pre>
<h4 id="1-car-클래스-생성-및-engine-위임">1. Car 클래스 생성 및 Engine 위임</h4>
<p>Car 클래스는 Engine의 기능을 필요로 합니다. 그러나 Car 자체가 엔진을 어떻게 시작하고, 어떻게 멈추는지 알 필요는 없습니다. 대신, 그 일을 <strong>Engine 구현체에게 위임</strong>합니다.</p>
<pre><code class="language-kotlin">class Car(engine: Engine) : Engine by engine</code></pre>
<p>이렇게 하면 Car는 start와 stop 메서드를 직접 구현할 필요가 없습니다. 대신, 생성 시점에 어떤 엔진을 사용할지 결정하고 그 엔진의 start와 stop을 호출하게 됩니다.</p>
<h4 id="2-메인-함수에서의-사용">2. 메인 함수에서의 사용</h4>
<pre><code class="language-kotlin">fun main() {
    val dieselCar = Car(DieselEngine())
    val electricCar = Car(ElectricEngine())

    dieselCar.start()    // 출력: Diesel Engine started!
    electricCar.start()  // 출력: Electric Engine started!

    dieselCar.stop()     // 출력: Diesel Engine stopped!
    electricCar.stop()   // 출력: Electric Engine stopped!
}</code></pre>
<p>결과적으로 Car는 어떤 종류의 엔진을 갖고 있는지 알 필요가 없습니다. 단순히 주어진 엔진의 기능을 사용(위임)하면 됩니다. 이렇게 위임을 활용하면, 코드의 재사용성이 향상되고 확장성 또한 높아집니다.</p>
<h2 id="✅-android-viewbinding에서-활용하기">✅ Android ViewBinding에서 활용하기</h2>
<h3 id="위임을-활용한-viewbinding">위임을 활용한 ViewBinding</h3>
<pre><code class="language-kotlin">inline fun &lt;T : ViewBinding&gt; AppCompatActivity.viewBinding(
    crossinline inflater: (LayoutInflater) -&gt; T,
) = lazy(LazyThreadSafetyMode.NONE) {
    inflater.invoke(layoutInflater)
}</code></pre>
<p>이 함수는 viewBinding이라는 확장 함수로, AppCompatActivity에 적용됩니다. 제네릭 타입 T를 사용하여 뷰 바인딩의 타입을 유동적으로 받아올 수 있습니다. lazy delegate 활용하여 뷰 바인딩 객체가 필요할 때만 초기화됩니다.</p>
<h3 id="사용-방법">사용 방법</h3>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {
    private val binding by viewBinding(ActivityMainBinding::inflate)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        // 나머지 로직
    }
}</code></pre>
<p>MainActivity에서는 viewBinding 확장 함수를 활용하여 ActivityMainBinding을 쉽게 초기화할 수 있습니다.</p>
<h2 id="✅-결론">✅ 결론</h2>
<p><strong>상속</strong>은 클래스 간의 관계를 구축하고, 코드 재사용을 가능케 하는 반면, <strong>위임</strong>은 코드의 복잡성을 줄이고, 런타임에 동작을 변경하는 등 유연한 코드 구조를 도와줍니다. 이 두 개념은 모두 코드의 재사용성을 향상시키는 데 있어 중요한 역할을 하므로, 적절히 혼용하여 효율적인 코드 설계를 진행하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose] AlertDialog 사용하기]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-AlertDialog-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-gzpx8dhd</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-AlertDialog-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-gzpx8dhd</guid>
            <pubDate>Mon, 02 Oct 2023 13:28:20 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! Compose에서 Android 시스템 기본 다이얼로그인 Alert Dialog에 대해 알아보고자 합니다. 이 글에서는 Compose Alert Dialog의 기본 개념과 사용법에 대해 자세히 다루겠습니다.</p>
<h2 id="✅-compose-dialog란">✅ Compose Dialog란?</h2>
<p>Compose Alert Dialog는 Android Jetpack Compose 라이브러리의 일부로, 대화 상자를 만들기 위한 강력하고 유연한 도구입니다. 이를 통해 앱 내에서 다양한 종류의 대화 상자를 생성하고 제어할 수 있습니다. 예를 들면 경고 메시지, 선택 옵션, 입력 폼 등을 포함합니다.</p>
<h2 id="✅-compose-alert-dialog-사용하기">✅ Compose Alert Dialog 사용하기</h2>
<h3 id="✓-alert-dialog-예제">✓ Alert Dialog 예제</h3>
<p>다음으로, 대화 상자의 내용과 동작을 정의하는 컴포저블을 작성합니다. 예를 들어 경고 메시지 Dialog를 만든다면 아래와 같은 예제가 될 것입니다.</p>
<pre><code class="language-kotlin">@Composable
fun AlertDialogExample() {
    val showDialog = remember { mutableStateOf(false) }

    if (showDialog.value) {
        AlertDialog(
            onDismissRequest = { showDialog.value = false },
            title = { Text(text = &quot;경고&quot;) },
            text = { Text(text = &quot;작업을 진행하시겠습니까?&quot;) },
            confirmButton = {
                Button(
                    onClick = { 
                    showDialog.value = false 
                    // 확인 동작
                }) {
                    Text(&quot;확인&quot;)
                }
            },
            dismissButton = {
                Button(
                    onClick = {
                    showDialog.value = false
                    // 취소 동작
                }) {
                    Text(&quot;취소&quot;)
                }
            }
        )
    }
}</code></pre>
<p>AlertDialog에는 onDismissRequest, title, text, confirmButton, dismissButton 등의 파라미터가 있습니다.</p>
<ul>
<li><strong>onDismissRequest</strong>: 대화상자 바깥쪽을 터치하거나 뒤로 가기 버튼을 누를 때 호출됩니다.</li>
<li><strong>title</strong>: 대화상자의 제목을 설정합니다.</li>
<li><strong>text</strong>: 대화상자의 내용을 설정합니다.</li>
<li><strong>confirmButton</strong>: 확인 버튼을 설정합니다. 사용자가 이 버튼을 클릭하면 showDialog.value를 false로 설정하여 대화상자를 닫습니다.</li>
<li><strong>dismissButton</strong>: 취소 버튼을 설정합니다. 사용자가 이 버튼을 클릭하면 showDialog.value를 false로 설정하여 대화상자를 닫습니다.</li>
</ul>
<h3 id="✓-alert-dialog-호출">✓ Alert Dialog 호출</h3>
<p>마지막으로, 앱 내에서 해당 함수를 호출하여 대화 상자가 나타나도록 설정합니다.</p>
<pre><code class="language-kotlin">@Composable
fun App() {
    // ...

    AlertDialogExample()

    // ...
}</code></pre>
<h2 id="✅-추가적인-기능과-커스터마이징">✅ 추가적인 기능과 커스터마이징</h2>
<p>Compose에서는 AlertDialog외에 Dialog라는 Composable을 통해 많은 유용한 기능과 커스터마이징 옵션을 제공합니다. </p>
<p>더 예제 코드는 <a href="https://github.com/vanpra/compose-material-dialogs">Compose Material Dialogs GitHub 저장소</a>에서 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose]: Compose AsyncImage, 컴포즈 Url, Uri로 이미지 로딩시키기]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-Compose-Loading-Image</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-Compose-Loading-Image</guid>
            <pubDate>Sun, 17 Sep 2023 14:54:49 GMT</pubDate>
            <description><![CDATA[<p>Compose에서 Url, Uri를 통해 이미지를 불러오기 위해서는 Image 가 아닌 AsyncImage를 통해 이미지를 Loading 시켜야합니다. </p>
<p>그렇기 위해서는 대표적으로 Coil, Glide라는 써드파티 이미지 라이브러리를 활용해야하는데요, 예제는 Coil을 사용한 예제입니다.</p>
<pre><code class="language-kotlin">// build.gradle(app)
implementation(&quot;io.coil-kt:coil-compose:2.4.0&quot;)


// Screen
AsyncImage(
    model = &quot;https://example.com/image.jpg&quot;,
    contentDescription = null,
)</code></pre>
<p>위 예제와 같이 coil라이브러리를 추가해 준 후에 AscyncImage 내부에 model 파라메터로 Image를 로딩할 수 있는 어떠한 Url, Uri 를 불러오게 하면 됩니다.</p>
<p>AsyncImage는 자동으로 이미지를 비동기적으로 로드하고 표시합니다. 이렇게 간단한 몇 줄의 코드로 원격 이미지를 쉽게 로드하고 표시할 수 있습니다.</p>
<p>참고 자료: <a href="https://developer.android.com/jetpack/compose/graphics/images/loading">https://developer.android.com/jetpack/compose/graphics/images/loading</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android에서 Coroutine 활용하기]]></title>
            <link>https://velog.io/@kej_ad/Android%EC%97%90%EC%84%9C-Coroutine-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/Android%EC%97%90%EC%84%9C-Coroutine-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Sep 2023 14:43:41 GMT</pubDate>
            <description><![CDATA[<h1 id="android-coroutine-사용법">Android Coroutine 사용법</h1>
<h2 id="activity-및-fragment">Activity 및 Fragment</h2>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/f15607ba-d44a-4229-ba57-656f7ff6fa82/image.png" alt=""></p>
<ul>
<li>coroutine 은 앱이 백그라운드 상황에서 동작되는 것을 방지하기 위해 LifeCycle의 확장변수로 corotineScope을 선언하고 있다.</li>
<li>해당 coroutine scope는 onDestroy() 상황에서 Cancel 된다.</li>
<li>coroutine Default Context 는 Dispatcher.Main.imemdiate이다.</li>
<li>추가적으로 (발표와 관련 없음) Fragment에서는 Fragment 객체의 수명주기가 아닌 Fragment View의 수명주기를 따라가야하기 때문에 viewOwnerLifeCycle의 lifeCycleScope를 활용하는 것이 좋다</li>
</ul>
<h4 id="예시fragmentextensionkt">예시(FragmentExtension.kt)</h4>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/66bda307-fe34-4dd6-ae78-b9d9ee663351/image.png" alt=""></p>
<h2 id="android-viewmodel에서-coroutine-사용-방법">Android ViewModel에서 coroutine 사용 방법</h2>
<ul>
<li>viewModelScope의 경우 lifeCycleScope과 동일하게 coroutine Default Context 는 Dispatcher.Main.imemdiate이다.</li>
<li>해당 ViewModel의 coroutine scope는 onCleared() 상황에서 Cancel 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/2f92cdc8-4a1b-4252-a75b-1d863df4e797/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 공유하기에 내 앱 뜨게 하기(사진 공유, 링크 공유)]]></title>
            <link>https://velog.io/@kej_ad/Android-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0%EC%97%90-%EB%82%B4-%EC%95%B1-%EB%9C%A8%EA%B2%8C-%ED%95%98%EA%B8%B0%EC%82%AC%EC%A7%84-%EA%B3%B5%EC%9C%A0-%EB%A7%81%ED%81%AC-%EA%B3%B5%EC%9C%A0</link>
            <guid>https://velog.io/@kej_ad/Android-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0%EC%97%90-%EB%82%B4-%EC%95%B1-%EB%9C%A8%EA%B2%8C-%ED%95%98%EA%B8%B0%EC%82%AC%EC%A7%84-%EA%B3%B5%EC%9C%A0-%EB%A7%81%ED%81%AC-%EA%B3%B5%EC%9C%A0</guid>
            <pubDate>Tue, 12 Sep 2023 14:18:16 GMT</pubDate>
            <description><![CDATA[<h2 id="✅-안드로이드-공유하기란">✅ 안드로이드 공유하기란?</h2>
<p>안드로이드 운영 체제에서는 사용자가 갤러리 앱, 웹 브라우저 등에서 &quot;공유하기&quot; 옵션을 선택할 때, 바텀 다이얼로그를 통해 여러 앱을 선택할 수 있도록 지원합니다. 이러한 공유 메커니즘에 자신의 앱을 추가하고 싶다면, 다음과 같이 설정하면 됩니다.</p>
<img src="https://velog.velcdn.com/images/kej_ad/post/9e8c973a-0c2f-4aa7-a3d0-ed1361552d91/image.png" width="30%" height="30%">

<h2 id="✅-구현하는-방법">✅ 구현하는 방법</h2>
<h3 id="내-앱을-공유하기-앱-리스트에서-보여주기">내 앱을 공유하기 앱 리스트에서 보여주기</h3>
<p>우선, 공유할 Activity가 실행될 때 호출되는 AndroidManifest.xml 파일 내부에 intent-filter를 설정해야 합니다.</p>
<pre><code class="language-kotlin">&lt;application
...
    &lt;activity
            android:name=&quot;.feature.home.photo.AddPhotoActivity&quot;
            android:exported=&quot;true&quot; &gt;
            &lt;intent-filter&gt;
                &lt;action android:name=&quot;android.intent.action.SEND&quot; /&gt;
                &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
                &lt;data android:mimeType=&quot;image/*&quot; /&gt;
            &lt;/intent-filter&gt;
        &lt;/activity&gt;
...
 &lt;/application&gt;</code></pre>
<blockquote>
<p>🛠️ 참고: <strong>android:exported=&quot;true&quot;로 설정</strong>해야만 다른 앱에서 이 Activity에 접근할 수 있습니다. 이 설정이 false로 되어있다면 공유하기 리스트에 나의 앱이 보이지 않습니다.</p>
</blockquote>
<h3 id="mime-type">MIME type</h3>
<p>MIME type은 <strong>Multipurpose Internet Mail Extensions</strong>의 약자로, 파일이나 데이터의 포맷을 설명하는 <strong>문자열 식별자</strong>입니다. 이는 웹 브라우저, 이메일 클라이언트, 다른 소프트웨어들이 어떻게 특정 형식의 데이터를 처리할 것인지를 결정하는 데 사용됩니다.</p>
<p>MIME type은 일반적으로 type/subtype 형식으로 이루어져 있습니다. 여기서 type은 데이터의 큰 카테고리를 나타내고, subtype은 그 카테고리 내에서의 구체적인 파일 포맷을 나타냅니다.</p>
<p>예시는 다음과 같습니다.</p>
<ul>
<li>text/plain: 일반 텍스트 파일</li>
<li>text/html: HTML 문서</li>
<li>image/jpeg: JPEG 이미지 파일</li>
<li>audio/mp3: MP3 오디오 파일</li>
</ul>
<blockquote>
<p>🙋‍♂️ 현재 저의 사이드 프로젝트에서는 <strong>사진 공유 기능</strong>을 필요로 하고 있기 때문에 모든 이미지 파일에 접근하기 위해 <strong>image/*</strong> MIME type을 intent-filter에 선언하였습니다.</p>
</blockquote>
<ul>
<li>만약 링크공유라면 text/plain을 사용하시면 됩니다.</li>
</ul>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types">참고 링크</a></p>
<h3 id="activity-에서-intentfilter로-들어온-데이터-핸들링">Activity 에서 intentFilter로 들어온 데이터 핸들링</h3>
<p>Activity에서는 설정한 intent-filter와 일치하는 데이터가 Intent로 전달되면, 아래와 같이 데이터를 핸들링할 수 있습니다.</p>
<pre><code class="language-kotlin">    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val action: String = intent.action ?:&quot;&quot;
        val type: String? = intent.type

        if (Intent.ACTION_SEND == action &amp;&amp; type != null) {
            if (&quot;image/*&quot; == type) {
                val imageUri = intent.getParcelableExtra&lt;Uri&gt;(Intent.EXTRA_STREAM)
                handleSendImage(imageUri)
            }
        }
    }

    private fun handleSendImage(imageUri: Uri?) {
        if (imageUri != null) {
            Timber.d(&quot;imageUri: $imageUri&quot;)
        }
    }</code></pre>
<p>intent.action과 intent.type을 확인한 후, 해당 데이터를 적절하게 처리합니다.</p>
<h2 id="✅-결과물">✅ 결과물</h2>
<p>해당 사진과 같이 저의 사이드 프로젝트인 Pophory 앱이 갤러리 공유하기 리스트에 뜬 것을 확인할 수 있습니다. 
<img src="https://velog.velcdn.com/images/kej_ad/post/e05871dd-9749-4a6f-a22a-b212274567f5/image.jpeg" width="30%" height="30%"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android/Compose]: Button에 색상(Color) 지정하기]]></title>
            <link>https://velog.io/@kej_ad/AndroidCompose-Button%EC%97%90-%EC%83%89%EC%83%81Color-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kej_ad/AndroidCompose-Button%EC%97%90-%EC%83%89%EC%83%81Color-%EC%A7%80%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Sep 2023 14:56:45 GMT</pubDate>
            <description><![CDATA[<h2 id="button에-색상-지정">Button에 색상 지정</h2>
<p>Button Composable 내부를 들여다보면 colors를 지정할 수 있다. 해당 파라메터로 ButtonColors 클래스를 넘겨주면 됩니다. </p>
<p>기본값은 <strong>ButtonDefaults.buttonColors()</strong>로 되어있는데 구현 로직을 따라가다보면 시스템 기본 컬러로 지정되어 있는 것을 확인할 수 있습니다.</p>
<pre><code class="language-kotlin">@Composable
fun Button(
    ... 
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    ...
)

@Composable
    fun buttonColors(
        containerColor: Color = FilledButtonTokens.ContainerColor.toColor(),
        contentColor: Color = FilledButtonTokens.LabelTextColor.toColor(),
        disabledContainerColor: Color =
            FilledButtonTokens.DisabledContainerColor.toColor()
                .copy(alpha = FilledButtonTokens.DisabledContainerOpacity),
        disabledContentColor: Color = FilledButtonTokens.DisabledLabelTextColor.toColor()
            .copy(alpha = FilledButtonTokens.DisabledLabelTextOpacity),
    ): ButtonColors = ButtonColors(
        containerColor = containerColor,
        contentColor = contentColor,
        disabledContainerColor = disabledContainerColor,
        disabledContentColor = disabledContentColor
    )</code></pre>
<h3 id="buttoncolors-의-구현-로직">ButtonColors 의 구현 로직</h3>
<p>그렇다면 우리는 Button에 직접 컬러를 적용하기 위해서 ButtonColors 인스턴스를 생성하여 Button 의 colors 파라메터로 집어넣어주기만 하면 끝입니다. </p>
<p>다만 ButtonColors 는 internal constructor로 해당 클래스의 생성자가 <strong>같은 모듈 내부에서만 호출</strong>할 수 있습니다. 그렇기에 해당 클래스는 같은 모듈 내부에서만 인스턴스화 할 수 있고, <strong>그 외부에서는 인스턴스를 생성할 수 없습니다.</strong></p>
<pre><code class="language-kotlin">@Immutable
class ButtonColors internal constructor(
    private val containerColor: Color,
    private val contentColor: Color,
    private val disabledContainerColor: Color,
    private val disabledContentColor: Color,
)</code></pre>
<p>따라서 위의 기본값에서 사용된 <strong>ButtonDefaults.buttonColors()</strong>를 통해 아래 4가지 컬러를 지정해야합니다. </p>
<ul>
<li><strong>containerColor</strong> : containerColor는 버튼의 배경색을 지정합니다. 이 색상은 버튼이 활성화되어 있을 때 (즉, 클릭 가능한 상태일 때) 적용됩니다.</li>
<li><strong>contentColor</strong>: contentColor는 버튼 내용의 색상을 지정합니다. 이 색상은 버튼의 텍스트나 아이콘 같은 내부 요소에 적용됩니다. containerColor와 잘 어울리도록 설정되어야 하며, 대조가 되어야 사용자가 쉽게 구분할 수 있습니다.</li>
<li><strong>disabledContainerColor</strong>: disabledContainerColor는 버튼이 비활성화된 상태일 때의 배경색을 지정합니다. 이 색상은 버튼이 클릭할 수 없는 상태일 때 사용됩니다.</li>
<li><strong>disabledContentColor</strong>:disabledContentColor는 버튼이 비활성화된 상태일 때 내용의 색상을 지정합니다. 버튼 내의 텍스트나 아이콘 등이 이 색상을 사용하게 됩니다. 보통은 disabledContainerColor와 어느 정도 대조를 이루되, 비활성화 상태임을 뚜렷이 나타낼 수 있도록 설정됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kej_ad/post/02653d95-b911-45c5-aef4-7a0167aaf0ac/image.png" alt=""></p>
<h3 id="실제-사용-예제">실제 사용 예제</h3>
<p>실제 사이드 프로젝트에서 사용하고 있는 버튼으로 ButtonDefaults.buttonColors() 를 사용하여 버튼에 색상을 지정한 예제입니다.</p>
<pre><code class="language-kotlin">Button(
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(
            containerColor = Main100,
            contentColor = Color.White,
            disabledContainerColor = Gray200,
            disabledContentColor = Color.White,
        ),
        onClick = {
            onReviewRegisterClicked()
        },
        shape = RoundedCornerShape(12.dp),
        content = {
            Text(
                text = stringResource(R.string.register),
                style = TextStyle(
                    fontSize = 16.sp,
                    fontWeight = FontWeight(500),
                    color = Color.White,
                )
            )
        }
    )</code></pre>
<ul>
<li>Enabled
<img src="https://velog.velcdn.com/images/kej_ad/post/1348df33-7fd0-4b32-b1b7-1cccbcbbfc53/image.png" alt=""></li>
<li>Disabled
<img src="https://velog.velcdn.com/images/kej_ad/post/d3526f4a-2fe1-48e6-b380-252b00cd7f45/image.png" alt=""></li>
</ul>
<p>XML 에서는 BackgrdundTint만 적용해주면 되었었는데, Compose는 기본적인 색상 변경하는 코드마저도 구글링을 해야한다니... 역시 아직 배울 점이 너무나도 많은 것 같습니다.</p>
]]></description>
        </item>
    </channel>
</rss>