<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>king-jungin.log</title>
        <link>https://velog.io/</link>
        <description>🕶안드로이드 개발자입니다! 🕶</description>
        <lastBuildDate>Fri, 21 Jul 2023 09:35:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>king-jungin.log</title>
            <url>https://velog.velcdn.com/images/king-jungin/profile/3fe2e8e2-dbba-42d4-bfc7-6874670d8979/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. king-jungin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/king-jungin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Android] Clean Architecture 적용 회고에 대한 주저리..]]></title>
            <link>https://velog.io/@king-jungin/Android-Clean-Architecture-%EC%A0%81%EC%9A%A9-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@king-jungin/Android-Clean-Architecture-%EC%A0%81%EC%9A%A9-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Fri, 21 Jul 2023 09:35:11 GMT</pubDate>
            <description><![CDATA[<p>GitHub 안드로이드 리포지토리를 탐험 하다보면 많은 프로젝트가 <code>Android App Architecture</code> 또는 <code>MVVM + Clean Architecture</code> 로 구성되어 있습니다.</p>
<p>평소 요즘 트렌드인 두 가지의 아키텍처를 동료들과 적용해보고 싶었던 저였지만, 이를 곧 바로 상용 서비스에 도입하기에는 리스크가 크다고 판단되어 엄두를 내지 못하고 있었습니다.</p>
<p>이러한 와중에 서버/ios 팀과 함께 하나의 사내 프로젝트를 만드는 TF팀이 결성되어, Android  앱의 <code>Clean Architecture</code> 앱 구조 설계를 진행하게 되었습니다.</p>
<p>이때 설계를 진행하며 느꼈던 바를 간단히 공유하고자 합니다.✍️</p>
<hr>
<h2 id="그대들-clean-architecture-를-왜-쓰는가">그대들, Clean Architecture 를 왜 쓰는가?</h2>
<blockquote>
<p>💡 왜 <code>Clean Architecture</code> 를 적용했나요?</p>
</blockquote>
<p>안드로이드 앱 아키텍처와 비교 했을때 제일 중요한 차이점은 <code>Domain</code> 레이어의 여부입니다.
클린 아키텍처는 비즈니스 로직을 구현한 <code>Domain</code> 레이어가 필수입니다.
안드로이드 앱 아키텍처는 이 <code>Domain</code> 레이어가 선택 사항이고, 비즈니스 로직이 복잡하지 않아 <code>data</code> 레이어 만으로 충분하다면 구현하지 않아도 됩니다.</p>
<p>이 두 가지의 아키텍처는 각각의 특징이 있습니다.</p>
<p>안드로이드 앱 아키텍처의 경우</p>
<ul>
<li><code>Domain</code> 레이어가 선택 사항인 안드로이드 앱 아키텍처는 상대적으로 구현이 간편할 수 있다.</li>
</ul>
<p>클린 아키텍처의 경우</p>
<ul>
<li><code>Domain</code> 가 필수이므로 비즈니스 로직의 용이한 재사용</li>
<li><code>Domain</code> 은 의존성이 없는 독립적 레이어이므로 손쉬운 테스트 코드 작성</li>
<li>간단한 데이터 호출도 UseCase 등을 추가해야 하므로 의미없는 복잡성이 증가할 수 있다.</li>
</ul>
<p>결국 두 아키텍처 모두 관심사 분리를 통한 쉬운 테스트가 그 기본 목적이기 때문에 좀 더 필요에 맞는 아키텍처를 적용하는게 옳아 보입니다.</p>
<hr>
<h2 id="🚿-usecase-에서-flow-를-노출시키던데">🚿 UseCase 에서 Flow 를 노출시키던데..?</h2>
<p>GitHub 의 샘플 프로젝트들은 아래와 같이 <code>Flow</code> 를 반환하도록 하는 코드가 많습니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/68dc4b6b-afcc-4f1c-a908-4e93a0a98653/image.png" width="70%"/></p>
<p>단순히 One-shot 형태로 데이터를 요청하는데, 비동기적 스트림 형태로 값을 방출하는 <code>Flow</code> 를 사용하는 이유가 있을까요?
이를 위해 많은 샘플 코드를 비교 및 의논하며 직접 구현해본 결과, 아래와 같은 깨달음을 얻었습니다.</p>
<blockquote>
<p>🎯 채팅 메시지 또는 지속적 위치 조회와 같이 비동기적 응답이 필요한 경우에만 <code>UseCase</code>, <code>Repository</code> 등에서 반환 값으로 노출하는게 좋다.</p>
</blockquote>
<p> <code>User</code>  단 하나를 반환해야하는 <code>UseCase</code> 는 아래와 같이 <code>Flow</code> 를 반환하지 않고 <code>User</code> Domain Model 또는 Result 와 같은 랩핑된 타입만 반환시키는게 여러 비동기적 결과들의 값들을 통합하는데 있어 더 유리하다고 생각합니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/d11afb76-5246-4842-8b4e-a3a3aab7a52f/image.png" width="70%"/></p>
<p><em>(작성하고 보니 아키텍처랑은 관계가 없어보이네요..)</em></p>
<hr>
<h2 id="😱-에러-처리는-어떻게-했어요">😱 에러 처리는 어떻게 했어요??</h2>
<p>우리는 어떤 기능이 서버를 통해 이루어진다면, 미리 정의된 에러 코드를 판별하여 예외 상황을 처리해야함을 알고있습니다.</p>
<p>이번 프로젝트의 경우 서버팀의 API 응답 규격에 따라 미리 <code>정의된 에러</code>가 <code>String</code> 형태로 응답되는 형식이였고, 이때 &quot;로그인 실패&quot; 에러 <code>String</code> 을 응답받는다면 이를 &#39;<code>Presentation</code> 레이어 까지 <code>String</code> 의 형태로 전달해야하는지&#39; 에 대한 고민이 있었습니다.</p>
<p>단순 <code>String</code> 으로 에러를 전달한다면 아래와 같은 이점이 있습니다.</p>
<ul>
<li><code>Presentation</code>, <code>domain</code>, <code>data</code> 각 레이어에 별도 에러 타입을 구현하지 않아도 되어 구현이 편하다.</li>
</ul>
<p><code>별도 에러 타입</code>을 정의하여 전달한다면 아래와 같은 이점이 있습니다.</p>
<ul>
<li>서버 <code>API 호출</code> 에러 코드 뿐 아니라 <code>DB</code> 등 여러 데이터 처리 중 발생하는 예외 상황을 가르키는 에러 타입을 만들 수 있어 <code>확장성</code>에 용이하다.</li>
</ul>
<p>우리는 이 중 <code>확장성</code> 에 초점을 맞춰, 별도의 에러 타입을 정의하기로 결정하였습니다.</p>
<p>이 에러 타입은 <code>domain</code> 레이어에 정의되어, <code>data</code> 레이어에서 에러 String 또는 여러 데이터를 판단하여 변환시키고, 이를 <code>presentation</code> 레이어까지 전달할 수 있도록 구현되었습니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/2ff3c1fb-d40a-4529-8e78-9f6c90f8b47f/image.png" width="60%"/></p>
<p><code>DomainError</code> 로 정의된 인터페이스를 각 서버 또는 기능별로 사용할 수 있도록 구현합니다. <em>(TokenServerError 는 토큰 발급, 갱신 중 발생하는 에러입니다.)</em></p>
<p>이를 <code>Domain</code> 레이어 상 공통된 규격으로 사용 할 수 있도록 <code>Repository</code> 가 반환해야하는 타입으로 <code>DomainResult</code> 라는 클래스로 랩핑하였습니다. (성공 결과 처리는 덤입니다)
<img src="https://velog.velcdn.com/images/king-jungin/post/7871c34b-aef8-4a54-bbfd-c8f5b78f6632/image.png" width="70%"/></p>
<p>에러 타입 변환을 쉽게 할 수 있도록 별도의 Mapping 클래스를 만들어 사용합니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/6e813766-879a-4dd1-9f77-65ceee1c03f3/image.png" width="70%"/></p>
<p>이를 통해 구현된 <code>Repository</code> 모습입니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/ef3016d0-cfcf-4a0b-bdad-1db7e6e2f39a/image.png" width="90%"/></p>
<p>이렇게 구현해본 감상으로는.. 에러 타입을 잘 작성한다면 좋은 방식이 될 듯 합니다.
다만, Repository 나 기능별로 에러 타입과 Mapping 클래스를 추가해야 할 수 있다는 점에서는 손이 많이 가는 방식이지 않나 싶습니다.</p>
<p>결론적으로, 어떻게 에러 처리를 구현해야할지 좀 더 고민해보는 숙제가 된 듯 합니다.🥲</p>
<hr>
<h2 id="끝으로">끝으로..</h2>
<p>클린 아키텍처를 적용하며 겪었던 시행착오는 수없이 많았습니다. 대부분이 아키텍처에 대한 이해도 부족으로 인한 것이였지만..</p>
<p>이 외에도 앱 구성요소 (서비스, 브로드캐스트 등) 을 통한 처리는 어떻게 이루어져야 할까요?
고민이 깊어지는 밤입니다..🌠🌠🌠</p>
<p>앞으로도 공유하고 싶은 글이 있으면 언제든지 작성하도록 하겠습니다!
여러분 모두 하시는 일, 원하시는 꿈 모두 잘되길 바랍니다.🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 양 옆이 미리보이는 ViewPager2 만들기]]></title>
            <link>https://velog.io/@king-jungin/Android-%EC%96%91-%EC%98%86%EC%9D%B4-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EC%9D%B4%EB%8A%94-ViewPager2-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@king-jungin/Android-%EC%96%91-%EC%98%86%EC%9D%B4-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EC%9D%B4%EB%8A%94-ViewPager2-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 06 Jul 2023 07:02:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/king-jungin/post/e94f3fc2-f6f0-4948-9b7c-458aaaad7fc4/image.gif" alt="여기어때 메인 화면"></p>
<p><strong>여기어때 메인 화면.gif</strong></p>
<hr>
<p>우리는 UX 요구사항에 맞춰 위와 같은 ViewPager 를 개발할 필요가 있습니다.😱
과연 어떻게 만들까요?</p>
<h1 id="📋결론-요약">📋결론 요약</h1>
<p><strong>Q. 양 옆이 미리보이는 ViewPager 는 어떻게 만들 수 있을까요?</strong></p>
<ul>
<li>View Pager2 에 몇가지 설정을 추가해서 만들 수 있습니다.</li>
</ul>
<p><strong>Q. 어떤 설정이요?</strong></p>
<ul>
<li><code>ItemDecoration</code> 과 ViewPager2 의 <code>setPageTransformer</code> 를 이용하면 됩니다.</li>
</ul>
<p><strong>Q. 더 간단한 방법 없나요?</strong></p>
<ul>
<li><a href="https://github.com/jeonjungin/PreviewOffsetViewPager">이 라이브러리를 쓰세요!</a></li>
</ul>
<h1 id="💻구현">💻구현</h1>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/b83daab7-1e23-424d-9fc9-373b08f31384/image.png" alt=""></p>
<p>먼저, Item이 3개가 있는 ViewPager 를 생각해봅시다.
ViewPager 는 Item View 의 높이 너비가 Match_parent 가 되어야 합니다.
따라서, ViewPager의 너비를 모두 채우는 Item 이 될 수 밖에 없습니다.</p>
<p><strong>그렇다면 어떻게 해야할까요?</strong></p>
<p>Item View 에 padding 을 정의하거나, ItemDecoration 으로 아이템 사이의 Margin 을 지정합니다.
우리는 ItemDecoration 을 통해 아이템 너비를 지정하겠습니다.</p>
<p><strong>우선, Item 의 왼쪽 오른쪽 마진을 지정할 ItemDecoration 을 정의합니다.</strong></p>
<pre><code class="language-kotlin">    private class PageDecoration(private val margin: Int): RecyclerView.ItemDecoration() {

        override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
            outRect.left = margin
            outRect.right = margin
        }
    }</code></pre>
<p><strong>이를 ViewPager 에 설정해줍니다.</strong> </p>
<pre><code class="language-kotlin">private fun initViewPager(itemMargin: Int) {
        val decoration = PageDecoration(itemMargin)
        binding.viewPager.addItemDecoration(decoration)
}</code></pre>
<p>이렇게 적용을 하면 left, right 마진이 설정된 ViewPager 를 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/king-jungin/post/ef72f643-f83a-4d11-9159-0240d8c563a4/image.png" alt=""></p>
<p>이제 <code>1st</code> 와 <code>3rd</code> 가 화면에 보이도록 <code>2nd</code> 에 가깝게 붙이고 싶습니다.
<strong>어떻게 할까요?</strong></p>
<p><strong>바로, ViewPager2 의 <code>setPageTransformer</code> 를 사용하면 됩니다.</strong></p>
<pre><code class="language-kotlin">    private fun initViewPager(itemMargin: Int) {
        // ..
        viewPager.setPageTransformer { page, position -&gt;
            // ..
        }
    }
</code></pre>
<p><code>setPageTransformer</code> 메서드는 <code>PageTransFormer</code> 인터페이스를 인자로 전달받는 메서드입니다.</p>
<pre><code class="language-kotlin">    public interface PageTransformer {
        void transformPage(@NonNull View page, float position);
    }</code></pre>
<p><code>PageTransformer</code> 의 <code>transformPage</code> 메서드는 ViewPager 가 스크롤되어 각 아이템의 위치가 변경될 때 마다 호출되는 콜백 메서드입니다.</p>
<p><code>transformPage</code> 의 <code>position</code> 은 현재 포커스된 가운데 아이템은 0f, 이를 기준으로 왼쪽 아이템은 음수값, 우측 아이템은 0보다 큰 양수값이 전달됩니다.</p>
<p>이를 이용해, 인자로 넘어오는 Page 의 <code>position</code> 를 계산하여 <code>View</code> 의 위치를 변경 할 수 있습니다.</p>
<p><strong>최종 적용된 코드는 아래와 같습니다.</strong></p>
<pre><code class="language-kotlin">    // 인자 `previewWidth` 는 미리 보여야 하는 양옆 아이템의 너비입니다.
    private fun initViewPager(previewWidth: Int, itemMargin: Int) {
        val decoMargin = previewWidth + itemMargin
        val pageTransX = decoMargin + previewWidth
        val decoration = PageDecoration(decoMargin)

        binding.viewPager.also {
            it.offscreenPageLimit = 1
            it.addItemDecoration(decoration)
            it.setPageTransformer { page, position -&gt;
                page.translationX = position * - pageTransX
            }
        }
    }</code></pre>
<p><code>initViewPager</code> 메서드를 설명하자면,</p>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/7368cb29-800e-46db-9a1a-61aeaecb390a/image.png" alt=""></p>
<ul>
<li>ItemDecoration 으로 설정하는 마진 값 <code>decoMargin</code> 은 적용할 아이템 사이 마진 값과 미리 보여야 하는 아이템 너비를 더하여 적용합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/d4540a2e-5409-4634-b109-3c80d6477592/image.png" alt=""></p>
<ul>
<li><code>View.translationX</code> 메서드를 통해 Item View 들의 위치를 변경합니다.</li>
<li>View 의 위치는 <code>position</code> 와 음수값 <code>pageTransX</code> 를 곱하여 적용합니다. (<code>position</code> 이 양수인 경우 왼쪽으로 옮기기 위해서 음수 값, 음수인 경우 오른쪽으로 옮기기 위해 양수 값이 필요하므로 음수값 <code>pageTransX</code> 를 곱해줍니다.)</li>
<li><code>pageTransX</code> 는 각 아이템이 <code>decoMargin * 2</code> 만큼 떨어져 있으므로, <code>decoMargin</code> 과 <code>itemMargin</code> 을 더하여 <code>itemMargin</code> 만큼만 보이도록 설정합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/3b265db6-e926-4c5c-8a5e-c17e0a61be3f/image.gif" width="50%"></img></p>
<h1 id="📚라이브러리">📚라이브러리</h1>
<p>위 구현 방식이 귀찮거나, 여러 화면에서 쓰여 Custom View 로써 사용해야하는 사람이 있을겁니다.
이런 분들을 위해 라이브러리를 배포했습니다!!👏</p>
<p>🔗<a href="https://github.com/jeonjungin/PreviewOffsetViewPager">GitHub 링크</a></p>
<p>유용하셨다면 🌠STAR 를 꾹.. 눌러주세요. 큰 힘이 됩니다!🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dependency Injection 이 왜 필요한가요?? .. (3)]]></title>
            <link>https://velog.io/@king-jungin/Dependency-Injection-%EC%9D%B4-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94-..-3</link>
            <guid>https://velog.io/@king-jungin/Dependency-Injection-%EC%9D%B4-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94-..-3</guid>
            <pubDate>Tue, 23 May 2023 09:48:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/king-jungin/post/b8f98b0d-c9a3-4094-b5c7-e61282868ea7/image.png" alt=""></p>
<p><strong>DI 에 대한 세번째 포스팅입니다.</strong></p>
<h1 id="의존성-주입">의존성 주입</h1>
<h2 id="테스트-코드">테스트 코드</h2>
<p>우리는 이전 포스팅 까지 의존성 주입이 필요한 이유에 대해 알아보았습니다.
결국, 궁극적으로 Testable 한 코드 작성이 그 이유였는데요, <strong>이제 실제 테스트 코드는 어떻게 해야 작성 할 수 있는지 예제를 통해 알아보겠습니다!</strong></p>
<blockquote>
<p>이번 예제에서 테스트 코드는 <strong>Junit4, Mockito, Mockito-kotlin2, Hamcrest</strong> 를 통해 구현합니다.</p>
</blockquote>
<h3 id="예제는-여기">예제는 여기</h3>
<p>🔗 <a href="https://github.com/jeonjungin/DIExample">https://github.com/jeonjungin/DIExample</a>
<em>(포스팅의 코드 조각은 예제보다 더 간소화되었습니다.)</em></p>
<h3 id="앱-요구사항">앱 요구사항</h3>
<pre><code>1. 포켓몬 ID 를 입력하여 포켓몬을 검색할 수 있다.
2. 포켓몬 ID 는 TextField(EditText) 에 입력하여 검색한다.
3. 검색된 포켓몬 이미지와 ID, 이름, HP 를 표시한다.
4. 검색 중 검색 실패, 네트워크 에러 등의 사유 발생시 Empty 상태로 표시한다.</code></pre><img src="https://velog.velcdn.com/images/king-jungin/post/c34ea3bd-64e0-4841-9a07-d10b96f567a4/image.png" width="50%" height="50%">

<p><em>예제 화면</em></p>
<h3 id="테스트-내용">테스트 내용</h3>
<p>우리는 이번 예제에서 <strong>ViewModel</strong> 을 테스트할 것 입니다.</p>
<p><strong>ViewModel</strong> 을 테스트하기 위해서는 어떤것이 필요할까요?
먼저 보통 앱들의 구조를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/bd525398-942c-4473-94c2-6d1456cfd3e4/image.png" alt=""></p>
<p>우리는 줄 곧 <strong>MVVM 형식의 앱</strong>을 개발하고 있습니다.
위 그림에서 보듯, <code>ViewModel</code> 은 UI 표시 및 상호작용을 위한 <code>View</code> 에 <code>상태</code> 를 나타내어 화면을 표시합니다.
또한, <code>Model</code> 에 데이터를 요청하고, 이를 정제하는 역할 또한 하고 있습니다.</p>
<p><strong><code>ViewModel</code></strong> 이 가지는 <strong>의존성 측면</strong>에서 보자면,</p>
<ol>
<li><code>ViewModel</code> 은 <code>Flow</code> 등을 통한 Observe 패턴으로 인해 <strong><code>View</code> 에 대한 의존성을 가지고 있지 않습니다.</strong></li>
<li><code>ViewModel</code> 은 <strong><code>Model</code> 에 대한 의존성이 있습니다.</strong></li>
</ol>
<p>따라서, <code>ViewModel</code> 를 테스트하기 위해 중요한것은 <strong><code>Model</code> 에 대한 의존성을 줄이는 것에 있습니다.</strong></p>
<p>이제, 예제를 통해 <code>Model</code> 에 대한 <strong>의존성을 줄이는 방법</strong>에 대해 알아보겠습니다.</p>
<h3 id="예제">예제</h3>
<p><strong>MainViewModel.kt</strong></p>
<pre><code class="language-kotlin">@HiltViewModel
class MainViewModel @Inject constructor(
    private val repo: PokemonRepository
): ViewModel() {

    private val _uiState = MutableStateFlow(CardUiState.Empty)
    val uiState = _uiState.asStateFlow()

    fun updateUi(id: String) {
        viewModelScope.launch {
            val pokemon = repo.fetchPokemon(id)
            _uiState.emit(CardUiState.Valid(pokemon))
        }
       }
    // ... 생략
}</code></pre>
<p>이 클래스는 포켓몬을 검색하고, 이를 UI 에 표시하기 위한 데이터를 가지는 역할을 수행합니다.
따라서, &#39;포켓몬 검색 기능&#39; 을 위한 의존성이 필요한데, 이를 생성자의 인자인 <code>repository</code> 를 통해 주입받고 있습니다.
<code>PokemonRepository</code> 인터페이스는 <code>Model</code> 을 추상화한 클래스입니다.
<strong><code>Model</code> 이 구체화된 Class 가 아닌, 추상화된 Class 를 주입받으므로, <code>Model</code> 에 대한 의존성이 느슨하다고 할 수 있습니다.</strong></p>
<p><strong>PokemonRepository.kt</strong></p>
<pre><code class="language-kotlin">interface PokemonRepository {

    suspend fun fetchPokemon(id: String): RepoResult&lt;Pokemon&gt;
}</code></pre>
<p>요구 사항 중 1번 &#39;포켓몬 ID 를 통한 검색&#39; 기능을 추상화한 인터페이스 및 구현체 클래스입니다. <strong>(<code>Model</code> 의 추상화)</strong>
해당 클래스를 통해 <strong><code>Model</code> 에 관한 로직을 구현하거나, 테스트를 위한 모의 클래스를 구현</strong>할 수 있습니다.</p>
<p>이렇게 정의된 코드를 가지고 MainViewModel 에 대한 간단한 Unit Test 코드를 작성해보겠습니다.</p>
<h3 id="unit-test">Unit Test</h3>
<pre><code class="language-kotlin">class MainViewModelTest {

    private lateinit var viewModel: MainViewModel
    private val repository = mock&lt;PokemonRepository&gt;()

    @Before
    fun setup() {
        viewModel = MainViewModel(repository)
    }
    // ... 생략
}</code></pre>
<p><code>PokemonRepository</code> 는 테스트용 클래스를 직접 정의할 수 있지만, 귀찮으니 Mockito 를 통해 모의 객체를 할당해주었습니다.
할당한 모의 객체를 <code>MainViewModel</code> 에 주입하여 객체를 생성합니다.
이렇게 테스트 코드 작성을 위한 준비가 모두 끝났습니다.</p>
<p>이제, 실제 테스트 로직을 작성해보겠습니다.</p>
<pre><code class="language-kotlin">    @Test
    fun `정상 값 설정 테스트`() = runTest {
        // given
        val mockResult = RepoResult.Success(
            value = Pokemon(
                id = &quot;1&quot;,
                name = &quot;JungIn&quot;,
                type = &quot;Fire&quot;,
                hp = 100
            )
        )
        whenever(repository.fetchPokemon(&quot;1&quot;)).thenReturn(mockResult)

        // when
        viewmodel.updateUi(&quot;1&quot;)

        // then
        val cardState = viewModel.uiState.first()
        assertThat(cardState, IsInstanceOf(CardUiState.Valid::class.java))
    }</code></pre>
<p>Mocktito-kotlin2 의 <code>whenever</code> 메서드를 이용해 <code>repository.fetchPokemon()</code> 메서드의 반환 값을 지정해줍니다. (given)
<code>viewModel.updateUi()</code> 를 통해 <code>MainViewModel</code> 의 &#39;포켓몬 검색 기능&#39; 을 수행하도록 트리거합니다. (when)
<code>assertThat()</code> 메서드를 통해 <code>viewModel.uiState</code> 의 상태를 확인하고 테스트를 종료합니다. (then)</p>
<p>이렇게 테스트가 통과되고, 이렇게 검증된 MainViewModel 을 View 에 활용할 수 있게 됩니다.</p>
<p>어떻나요? 참 쉽죠?😄</p>
<p>만약, <code>Model</code> 에 대한 의존성을 <code>ViewModel</code> 이 가지고 있었다면 테스트가 가능했을까요?</p>
<pre><code class="language-kotlin">@HiltViewModel
class MainViewModel @Inject constructor(
): ViewModel() {

    private val repo = PokemonRepository(...)

    private val _uiState = MutableStateFlow(CardUiState.Empty)
    val uiState = _uiState.asStateFlow()
    // ... 생략
}</code></pre>
<p>위 코드에서 보시다 싶이 프로퍼티 <code>repo</code> 를 조작할 방법이 없습니다.
따라서.. <code>Model</code> 의 반환 값을 가정한 여러 테스트가 불가능합니다.</p>
<h3 id="끝으로">끝으로</h3>
<p>이렇게 보니 의존성 주입도, 테스트 코드 작성도 참 간단합니다.
다만 우리는 시간에 쫓겨.. 귀찮음으로 인해.. 의존성 주입을 위한 구조 설계나, 더 나아가 테스트 코드 작성을 외면하곤 합니다.</p>
<p>그렇지만, 잘 설계된 구조와 테스트 코드는 앞으로의 코딩 생활에 있어 많은 시간을 단축시켜줄 것이므로🔥
🔥<strong>의존성 주입 패턴과 테스트 코드 작성을 생활화 합시다</strong>🔥</p>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/a25889e1-3541-47e3-8ed8-d56161867013/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dependency Injection 이 왜 필요한가요?? .. (2)]]></title>
            <link>https://velog.io/@king-jungin/Dependency-Injection-%EC%9D%B4-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94-..-2</link>
            <guid>https://velog.io/@king-jungin/Dependency-Injection-%EC%9D%B4-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94-..-2</guid>
            <pubDate>Tue, 16 May 2023 05:59:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/king-jungin/post/f4d6a669-ad43-4983-8803-6369bbd914aa/image.png" alt=""></p>
<p><strong>DI 에 대한 두번째 포스팅입니다.</strong></p>
<p>첫번째 게시글은 아래를 링크로!
<a href="https://velog.io/@king-jungin/Dependency-Injection-%EA%B0%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94..-1">https://velog.io/@king-jungin/Dependency-Injection-%EA%B0%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94..-1</a></p>
<h1 id="의존성-주입">의존성 주입</h1>
<h2 id="예시">예시</h2>
<h3 id="testable">Testable</h3>
<p>이전 포스팅에서 의존성 주입은 <strong>Tesable</strong> 한 코드를 작성할 수 있다고 했습니다.
과연 <strong>Testable</strong> 한 코드는 무엇일까요?</p>
<p>아래에 요구사항에 따른 코드가 있습니다.</p>
<p><strong><em>요구사항</em></strong></p>
<pre><code>1. 랜덤한 숫자를 가진 랜덤박스가 있습니다.
2. 이 랜덤박스는 랜덤한 숫자가 90 이상인 경우에만 상품이 들어있습니다.</code></pre><p><strong><em>코드</em></strong></p>
<pre><code class="language-kotlin">object Goods

class RandomBox {
    private val score = Random(System.currentTimeMillis()).nextInt(100)

    fun unBoxing() = if (score &gt;= 90) {
        Goods
    } else {
        null
    }
}</code></pre>
<p>자! 이제 우리는 이 코드가 잘 동작하는지 확인하고 싶습니다.
그 중에 실제로 랜덤한 숫자 <strong><code>score</code> 가 <code>90</code> 이상이면 <code>Goods</code> 를 반환해 주는지 테스트</strong>하고 싶은데요.</p>
<p>여기서 문제가 있습니다.</p>
<p><code>score</code> 가 <code>90</code> 이상인 상황을 테스트하기 위해서는</p>
<ol>
<li><strong>디버깅을 통해 90 이상이 나올때까지 돌려본다😱</strong></li>
</ol>
<p>바람직하지 않습니다. 언제 나올줄 알고 돌려보나요...</p>
<ol start="2">
<li><strong><code>Random</code> 로직을 주석 처리하고 <code>score</code> 에 정수 90을 넣어 재빌드한다.🤮</strong></li>
</ol>
<p>이 또한 바람직하지 않습니다.
물론 현재로선 간단하겠지만, 로직이 복잡해지면 주석 처리할 곳이 많아져 <strong>테스트가 심히 어려워집니다.</strong></p>
<p>그렇다면 어떻게 해야할까요??</p>
<h3 id="의존성-주입으로-해결이-가능합니다">의존성 주입으로 해결이 가능합니다.</h3>
<p><strong><em>RandomInt.kt</em></strong></p>
<pre><code class="language-kotlin">interface RandomInt {
    operator fun invoke(): Int
}

class RandomIntImpl: RandomInt {
    override fun invoke() = Random(System.currentTimeMillis()).nextInt(100)
}</code></pre>
<p><strong><em>RandomBox2.kt</em></strong></p>
<pre><code class="language-kotlin">class RandomBox2(
    random: RandomInt
) {
    private val score = random()

    fun unBoxing() = if (score &gt;= 90) {
        Goods
    } else {
        null
    }
}</code></pre>
<p><strong><em>Main.kt</em></strong></p>
<pre><code class="language-kotlin">fun main(args: Any) {
    val goods = RandomBox2(RandomIntImpl()).unBoxing()
    // ...
}</code></pre>
<p>위 코드는 <strong>의존성 주입</strong>으로 구현된 코드입니다.</p>
<ul>
<li><code>RandomInt</code> 는 랜덤 정수를 반환하는 로직을 추상화한 interface 입니다.</li>
<li>기존 랜덤 로직은 <code>RandomIntImpl</code> 에서 구현합니다.</li>
<li><code>RandomBox2</code> 는 추상화된 <code>RandomInt</code> interface 를 주입받아 <code>score</code> 에 값을 할당합니다.</li>
</ul>
<p>즉, 랜덤 정수를 추출하는 로직을 추상화하여 주입하고 있으므로 다양한 알고리즘의 랜덤 로직을 주입하여 사용할 수 있고, 테스트용 랜덤 로직을 작성하여 주입할 수도 있게 되었습니다.</p>
<p>아래는 <code>score</code> 가 <code>90</code> 이상인 Case 테스트의 예시입니다.</p>
<p><strong><em>RandomIntTestImpl</em></strong></p>
<pre><code class="language-kotlin">class RandomIntTestImpl: RandomInt {
    override fun invoke() = 90
}</code></pre>
<p><strong><em>main.kt</em></strong></p>
<pre><code class="language-kotlin">fun main(args: Any) {
    val goods = RandomBox2(RandomIntTestImpl()).unBoxing()
    // ...
}</code></pre>
<p><code>RandomIntTestImpl</code> 클래스가 정수 <code>90</code> 만을 반환도록 구현되어 있으므로, 이를 주입하여 <code>Goods</code> 가 반환되는지에 대한 테스트가 가능하게 되었습니다.</p>
<p>이를 응용하여 <strong>Junit</strong> 및 <strong>Mockito</strong> 등을 활용한 <strong>Unit Test</strong> 가 가능합니다.</p>
<p><strong>--&gt; 다음 포스팅에 계속..</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dependency Injection 이 왜 필요한가요??.. (1)]]></title>
            <link>https://velog.io/@king-jungin/Dependency-Injection-%EA%B0%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94..-1</link>
            <guid>https://velog.io/@king-jungin/Dependency-Injection-%EA%B0%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80%EC%9A%94..-1</guid>
            <pubDate>Tue, 16 May 2023 03:46:21 GMT</pubDate>
            <description><![CDATA[<p><img src="blob:https://velog.io/36d797a1-e040-44d9-89ec-0fec9a29f67e" alt="업로드중.."></p>
<h1 id="개요">개요</h1>
<p>본 포스팅은 아래의 목적을 가집니다.</p>
<ul>
<li>DI (Dependency Injection) 이 무엇인지 이해한다.</li>
<li>DI 의 목적을 이해한다.</li>
<li>DI 의 활용법을 알아본다. (with Hilt for Android)</li>
</ul>
<h1 id="의존성-주입">의존성 주입</h1>
<p>DI 는 무엇일까요?🧐
문자 그대로 <strong>의존성(종속성) 주입</strong>을 뜻합니다.</p>
<p>그럼 여기서 <strong>의존성은</strong> 무엇일까요?
의존의 <strong>사전적 정의</strong>는 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/king-jungin/post/f1edbb13-df8b-4291-900c-845b26e4cb66/image.png" alt=""></p>
<p>사람에 빗대자면,
<strong>사람이 살아가기 위해서는 심장이 필요하다.</strong>
사람은 심장이 없으면 살 수 없으므로 <strong>&#39;사람은 심장에 의존한다&#39;</strong> 로 표현할 수 있습니다.</p>
<p>그렇다면, <strong>의존성 주입</strong>이란 무엇일까요?
위에서 말한 <strong>심장</strong> 을 <strong>인공심장</strong>등으로 <strong>교체하는 행위</strong>를 뜻합니다.</p>
<p>(엣지러너의 산데비스탄 처럼??)
<img src="blob:https://velog.io/34d0f7fa-d104-4e9a-83dc-14c738e2729b" alt=""></p>
<h2 id="예시">예시</h2>
<h3 id="의존성">의존성</h3>
<p>아래와 같은 요구사항이 있다고 가정합니다.</p>
<pre><code>1. 정인이는 일할때 카페라떼를 마십니다.</code></pre><p>이를 코드로 표현하자면 아래와 같습니다.</p>
<pre><code class="language-kotlin">class JungIn {
    val latte = CafeLatte()

    fun work() {
        latte.drink()
        // ...
    }
}</code></pre>
<p>위 코드에서 <code>JungIn</code> 은 <code>work</code> 를 수행하기 위해 <code>CafeLatte.drink()</code>를 실행해야 합니다.
이는 <strong>&#39;클래스 <code>JungIn</code> 은 클래스 <code>CafeLatte</code> 에 의존성이 있다.&#39;</strong> 로 표현할 수 있습니다.</p>
<h3 id="주입">주입</h3>
<p>이때 기획팀 의견으로 요구사항이 아래와 같이 변경되었습니다.</p>
<pre><code>1. 정인이는 일할때 카페라떼를 마십니다.
2. 쿨라임 피지오도 마십니다.</code></pre><p>이를 코드로 표현하면 <code>CafeLatte</code> 와 <code>CoolLimePizzo</code> 클래스에 의존성이 있는 <code>JungIn</code> 클래스가 정의됩니다.</p>
<pre><code class="language-kotlin">interface Beverage {
    fun drink()
}

class CafeLatte: Beverage {
    override fun drink() {
        print(&quot;yammy&quot;)
    }
}

class CoolLimePizzo: Beverage {
    override fun drink() {
        print(&quot;soooooo good&quot;)
    }
}

class JungIn {
    val latte = CafeLatte()
    val pizzo = CoolLimePizzo()

    fun work() {
        latte.drink()
        pizzo.drink()
        // ...
    }
}
</code></pre>
<p>이때 우리는 아래와 같은 생각을 하게 됩니다.</p>
<p><strong>&#39;정인이가 다른 음료수를 마실수도 있겠구나!&#39;</strong>😡
<strong>&#39;음료수가 계속 추가되면 계속 수정해야겠네..&#39;</strong>😇</p>
<p>그렇게 바뀐 코드는 아래와 같습니다.</p>
<pre><code class="language-kotlin">class JungIn(private val beverages: List&lt;Beverage&gt;) {

    fun work() {
        beverages.forEach { 
            it.drink()
        }
        // ...
    }
}</code></pre>
<p>프로퍼티 <code>CafeLatte</code> 와 <code>CoolLimePizzo</code> 는 제거되고 음료수는 생성자를 통해 <code>Beverage</code> interface 리스트로 넘겨받도록 변경되었습니다.</p>
<p><strong>즉, <code>CafeLatte</code> <code>CoolLimePizzo</code> 와의 강한 결합은 제거되고 <code>Beverage</code> 와의 느슨한 결합이 생성되었습니다.</strong></p>
<p>이 <strong>느슨한 결합</strong>은 개발자가 <strong>매번 새로운 음료수를 프로퍼티로 작성할 필요도</strong>, 음료수의 <strong>생성자가 변경되어도 대응할 필요가 없는</strong> 유연한 코드 작성이 가능케 합니다.</p>
<p>이와같이 인스턴스를 클래스 내에서 생성하지 않고, 생성자 등으로 주입 받는 행위를 <strong>의존성 주입</strong> 이라고 합니다.</p>
<p>우리는 <strong>의존성 주입</strong>을 통해 <strong>유연하고 Testable</strong> 한 코드를 작성할 수 있습니다.</p>
<h3 id="testable">Testable</h3>
<p>그럼.. 의존성 주입으로 유연한 코드 작성이 가능해지는 건 알겠는데, <strong>Testable 한 코드는 뭘까요??</strong></p>
<p><strong>--&gt; 다음 포스팅에서 이어집니다..</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[펼쳐지고 닫히는 TextView를 원하시나요? (like. Youtube)]]></title>
            <link>https://velog.io/@king-jungin/%ED%8E%BC%EC%B3%90%EC%A7%80%EA%B3%A0-%EB%8B%AB%ED%9E%88%EB%8A%94-TextView%EB%A5%BC-%EC%9B%90%ED%95%98%EC%8B%9C%EB%82%98%EC%9A%94-like.-Youtube</link>
            <guid>https://velog.io/@king-jungin/%ED%8E%BC%EC%B3%90%EC%A7%80%EA%B3%A0-%EB%8B%AB%ED%9E%88%EB%8A%94-TextView%EB%A5%BC-%EC%9B%90%ED%95%98%EC%8B%9C%EB%82%98%EC%9A%94-like.-Youtube</guid>
            <pubDate>Mon, 08 May 2023 05:12:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/king-jungin/post/9856c83f-a884-458b-92c5-6904753d9e51/image.jpg" alt=""></p>
<p><strong>긴~글을 줄여서 &#39;더보기&#39; 버튼과 함께 표시해야 한다구요?</strong>
<strong>TextView 두개를 겹쳐서 구현하기엔 너무 짜치다구요~~?</strong></p>
<h1 id="🔥🔥🔥-소개합니다-readmoreview-🔥🔥🔥">🔥🔥🔥 소개합니다! ReadMoreView 🔥🔥🔥</h1>
<blockquote>
<p> ReadMoreView 는 TextView 를 상속받아 만들었어요!
 사용법도 무척 간단하답니다~</p>
</blockquote>
<p><strong>백문이 불여일견.. 먼저 보시죠</strong>
<img src="https://velog.velcdn.com/images/king-jungin/post/768f5e4f-2bd8-4fb1-bfa2-d7888df60c9c/image.gif" width="60%"></p>
<p>ReadMoreView 의 요구사항은 아래와 같습니다.</p>
<ul>
<li>&#39;닫힘&#39; 상태 글의 maxLines 를 지정하여 글의 끝에 말줄임(ellipsize) 표시</li>
<li>글 끝에 &#39;닫힘&#39;, &#39;열림&#39; 상태 변경 버튼 추가</li>
</ul>
<h1 id="구현-초기">구현 초기</h1>
<p>처음에는 아래와 같은 xml 레이아웃을 inflate 하는 간단한 Custom View 를 구상했습니다.</p>
<pre><code class="language-xml">&lt;layout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;data&gt;
        &lt;import type=&quot;android.view.View&quot;/&gt;
        &lt;variable
            name=&quot;text&quot;
            type=&quot;String&quot; /&gt;
        &lt;variable
            name=&quot;expand&quot;
            type=&quot;Boolean&quot; /&gt;
    &lt;/data&gt;

    &lt;androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;&gt;

        &lt;TextView
            android:id=&quot;@+id/tvCollapse&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:visibility=&quot;@{expand ? View.GONE : View.VISIBLE}&quot;
            android:ellipsize=&quot;end&quot;
            android:maxLines=&quot;3&quot;
            android:text=&quot;@{text}&quot; /&gt;

        &lt;TextView
            android:id=&quot;@+id/tvExpand&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:visibility=&quot;@{expand ? View.VISIBLE : View.GONE}&quot;
            android:text=&quot;@{text}&quot; /&gt;

    &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
&lt;/layout&gt;</code></pre>
<ul>
<li>&#39;닫힘&#39;, &#39;열림&#39; 상태는 각각의 <code>TextView</code> 를 통해 표시하고, <code>visibility</code> 를 변경하여 구현</li>
<li>&#39;닫힘&#39;, &#39;열림&#39; 버튼은 <code>Spannable</code> 을 통해 구현</li>
</ul>
<p>그러나 이 방법은 <code>TextView</code> 를 상속받아 구현한게 아니므로 <code>textSize</code> 등의 속성을 직접 정의하고 각 View 에 연결해줘야 하는 번거로움이 있었고..🥲
<strong>직접 구현을 해보고 싶은</strong> 열정이 더 컸으므로.. 직접 View 를 상속받아 만들게 되었습니다!🔥</p>
<h1 id="구현">구현</h1>
<p>ReadMoreView 는 상속받을 수 없는 <code>TextView</code> 를 대신해 <code>AppCompatTextView</code> 를 상속받아 구현되었습니다.
따라서, <strong><em>FontFamily</em></strong> 등의 <code>TextView</code> 속성을 그대로 사용할 수 있습니다. (간편하죠? 😄)
&#39;닫힘&#39;, &#39;열림&#39; 버튼의 색상등 속성도 변경할 수 있구요..</p>
<p><strong>ReadMoreView 의 동작 단계는 아래와 같습니다.</strong></p>
<ol>
<li><p><code>View.onMeausre</code> 에서 Text 가 입력될 너비 구하기</p>
<pre><code class="language-kotlin"> override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
     val givenWidth = MeasureSpec.getSize(widthMeasureSpec)
     val textWidth = givenWidth - compoundPaddingStart - compoundPaddingEnd
     if (textWidth == oldTextWidth) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         return
     }

     updateDisplayText(textWidth)

     super.onMeasure(widthMeasureSpec, heightMeasureSpec)
 }
</code></pre>
</li>
</ol>
<pre><code>`MeasureSpec.getSize()` 로 너비를 가져와, Padding 을 제외한 영역의 너비를 `updateDisplayText` 에 전달하여 다음 단계를 진행합니다.


2.  구한 너비와 MaxLine 및 `StaticLayout` 를 활용하여 닫힘 상태의 String 구하기
```kotlin
private fun updateDisplayText(textWidth: Int = measuredWidth - compoundPaddingStart - compoundPaddingEnd) {
        //... 생략

        val lastEllipsizeWidth = if (btnLocation is BtnLocation.NextLine) {
            0
        } else {
            getEllipsizeWidth() + getColBtnTextWidth(expandBtnText, btnSizePx)
        }

        val collapseLayout = getCollapseStaticLayout(originalText?: &quot;&quot;, textWidth, colMaxLine, lastEllipsizeWidth)
        val collapseContentText = &quot;${collapseLayout.text}&quot;

        // ...생략
    }</code></pre><pre><code class="language-kotlin">private fun getCollapseStaticLayout(text: CharSequence, textWidth: Int, maxLine: Int, ellipsizeWidth: Int): StaticLayout {
        val ellipsizedWidth = textWidth - ellipsizeWidth
        return StaticLayout.Builder
            .obtain(text, 0, text.length, paint, textWidth.coerceAtLeast(0))
            .setEllipsize(TextUtils.TruncateAt.END)
            .setEllipsizedWidth(ellipsizedWidth)
            .setMaxLines(maxLine)
            .setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
            .build()
        // ... 생략
    }</code></pre>
<p><code>StaticLayout</code> 가 생소하신 분도 있으실텐데요, <code>Canvas</code> 에 텍스트를 그려줄때 많이 사용합니다.
ReadMoreView 에서는 줄바꿈 처리와 ellipszie 처리된 String 을 얻기위해 사용했습니다!</p>
<p><code>getCollapseStaticLayout()</code> 메서드에 원본 String 과 삽입될 너비와 MaxLine 을 전달하여 ellipsize 가 적용된 StaticLayout 을 얻고, <code>text</code> 프로퍼티로 String 을 얻습니다.</p>
<ol start="3">
<li><p><code>Spannable</code> 을 활용하여 글 끝에 &#39;닫힘&#39;, &#39;열림&#39; 버튼 삽입하기</p>
<pre><code class="language-kotlin">private fun getContentSpannable(content: String, btnText: String, isExpandable: Boolean): SpannableStringBuilder {
    return SpannableStringBuilder().apply {
        append(content)

        if (btnText.isEmpty() || !isExpandable) {
            return@apply
        }

        if (btnLocation is BtnLocation.NextLine) {
            append(&quot;\n&quot;)
        }

        append(btnText)

        val btnStart = this.length - btnText.length
        val btnEnd = btnStart + btnText.length

        setSpan(UnderlineSpan(), btnStart, btnEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        setSpan(
            AbsoluteSizeSpan(btnSizePx),
            btnStart,
            btnEnd,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        setSpan(object : ClickableSpan() {
            override fun onClick(widget: View) {
                toggle()
            }

            override fun updateDrawState(ds: TextPaint) {
                super.updateDrawState(ds)
                ds.color = btnColor
            }
        }, btnStart, btnEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

    }
}</code></pre>
<p><code>getContentSpannable()</code> 메서드는 입력된 String 과 버튼 텍스트를 입력받아, 기 지정된 버튼 Text의 속성을 적용하여 SpannableString 을 반환합니다.</p>
</li>
<li><p><code>TextView.setText</code> 메서드로 현재 상태의 String 삽입하기</p>
</li>
</ol>
<pre><code class="language-kotlin">    private fun updateDisplayText(textWidth: Int = measuredWidth - compoundPaddingStart - compoundPaddingEnd) {
        // ... 생략

        val collapseSpannable = getContentSpannable(ellipsizedOrNotText.toString(), expandBtnText, isExpandable)

        text = when (state) {
            MoreState.COLLAPSED -&gt; collapseSpannable
            MoreState.EXPANDED -&gt; expandSpannable
        }

        // ... 생략
    }</code></pre>
<p>현재 &#39;열림&#39; 상태인 경우 <code>expandSpannable</code> 를, &#39;닫힘&#39; 상태인 경우 <code>collapseSpannable</code> 을 <code>TextView.setText()</code> 를 통해 <code>TextView</code> 에 삽입합니다.</p>
<p>어떤가요? <code>StaticLayout</code> 을 활용한 부분이 조금 생소할 뿐 간단한 구현 내용입니다👏👏👏</p>
<h1 id="종속성-및-사용법">종속성 및 사용법!</h1>
<p>ReadMoreView <strong>종속성 설정 방법</strong>, 여러 기능과 사용법, 자세한 구현 내용이 궁금하다면 아래 github 를 참고해주세요~!
📎 <a href="https://github.com/jeonjungin/ReadMoreView">ReadMoreView github 링크!!</a>📎</p>
<p>유용하셨다면 Star 도...🌠</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android MQTT 라이브러리를 os 12 이상에서 사용한다면??]]></title>
            <link>https://velog.io/@king-jungin/Android-MQTT-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-os-12-%EC%9D%B4%EC%83%81%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%9C%EB%8B%A4%EB%A9%B4</link>
            <guid>https://velog.io/@king-jungin/Android-MQTT-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-os-12-%EC%9D%B4%EC%83%81%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%9C%EB%8B%A4%EB%A9%B4</guid>
            <pubDate>Fri, 28 Apr 2023 05:16:41 GMT</pubDate>
            <description><![CDATA[<p align="center"><img src="https://velog.velcdn.com/images/king-jungin/post/fa2fe4b1-b8ca-4311-9e7d-49d79b1695d9/image.png" width="70%"></p>


<p>이번 8월 31일부터 스토어 정책 상 안드로이드 os 13 타겟팅이 <strong>필수 항목</strong>으로 변경됩니다. 😂
따라서 os 12 와 os 13 타겟팅을 대응하시는 분들이 많을텐데요!</p>
<p>MQTT 라이브러리 (org.eclipse.paho.android.service.MqttService) 를 사용하고 계신 분들!
MQTT 라이브러리를 새로이 적용하시는 분들은 아래 에러 메시지를 보실 수 있습니다 😅</p>
<pre><code>java.lang.IllegalArgumentException: app id: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
    Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.</code></pre><blockquote>
<p>안드로이드 os 12 이상을 타겟팅하는 앱은 PendingIntent 의 flag 에 <strong>변경 가능 여부 flag를 뜻하는 FLAG_IMMUTABLE 나 FLAG_MUTABLE 를 추가</strong>해야합니다.</p>
</blockquote>
<p>에러 메시지 내용에 따라 &#39;PendingIntent&#39; 의 flag 를 변경해주어야 하는데요..
문제가 되는 부분은 MQTT 라이브러리의 아래 코드입니다.😥</p>
<h3 id="alarmpingsenderkt">AlarmPingSender.kt</h3>
<pre><code class="language-kotlin">    @Override
    public void start() {
        //...
        pendingIntent = PendingIntent.getBroadcast(service, 0, new Intent(
                action), PendingIntent.FLAG_UPDATE_CURRENT);

        //...
    }</code></pre>
<p>네.. 원격 저장소를 통해 받은 라이브러리이므로 수정을 할 수 없으니 두가지 선택지가 있습니다.</p>
<ol>
<li>github 를 방문하여 소스를 복사하고 수정한다.</li>
<li>MQTT 를 대체한다..😱</li>
</ol>
<p>두번째 방법은 있을 수 없는 일이므로 첫번째 방법을 수행해야겠죠?</p>
<p>첫번째 방법을 위해 GitHub 의 어느 한 귀인께서! PendingIntent 만을 수정한 라이브러리를 배포해주셨습니다.👏👏👏</p>
<p><strong>Github 주소🔥</strong>
<a href="https://github.com/hannesa2/paho.mqtt.android">https://github.com/hannesa2/paho.mqtt.android</a></p>
<p>심지어 StackOverFlow 의 어느 귀인께서는 <strong>마이그레이션 방법</strong>까지 친히 알려주시네요🔥
<a href="https://stackoverflow.com/a/71839305">https://stackoverflow.com/a/71839305</a></p>
<p>유용하셨다면.. 개발자들의 삽질 시간을 아껴주시는 이 고마운 귀인분들께 감사 인사와 함께 GitHub Star 를 눌러봅시다🌠🌠🌠🌠</p>
]]></description>
        </item>
    </channel>
</rss>