<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dear.jjwim</title>
        <link>https://velog.io/</link>
        <description>🐰 피드백은 언제나 환영합니다</description>
        <lastBuildDate>Fri, 04 Nov 2022 18:02:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dear.jjwim</title>
            <url>https://velog.velcdn.com/images/dear_jjwim/profile/a61c1b5a-dd87-4a3c-a89c-1124cb852ab1/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Dear.jjwim. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dear_jjwim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[안드로이드 Glide SVG 이미지 로드 관련 오류 해결 - Failed to create image decoder with message 'unimplemented']]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Glide-SVG-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%93%9C-%EA%B4%80%EB%A0%A8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Failed-to-create-image-decoder-with-message-unimplemented</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Glide-SVG-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%93%9C-%EA%B4%80%EB%A0%A8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Failed-to-create-image-decoder-with-message-unimplemented</guid>
            <pubDate>Fri, 04 Nov 2022 18:02:53 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-문제">🚨 문제</h1>
<p><em>현재 Glide <code>4.14.2</code> 버전을 사용중</em></p>
<pre><code class="language-kotlin">fun ImageView.load(drawable: Int) {
    Glide.with(this)
        .load(drawable)
        .transition(DrawableTransitionOptions.withCrossFade())
        .diskCacheStrategy(DiskCacheStrategy.ALL)
        .timeout(5000)
        .into(this)
}</code></pre>
<pre><code>D/skia: --- Failed to create image decoder with message &#39;unimplemented&#39;</code></pre><p>앱이 다운된다거나, 실행한 앱 내의 이미지 로드에 실패한 것은 아니고 단순히 로그에 찍히는 거라 찾다보니 이대로 그냥 남겨두기도 한다는 얘기도 있었다 😅 ...! 하지만 &#39;버전에 따라 다르지 않을까&#39; 혹은 &#39;운이 좋아서 실행되는 거 아닐까&#39; 싶어 꼭 해결해야겠다고 생각했다.</p>
<p>여러 상황을 시도해보니 서버부터 받은 url이나 Uri를 로드할 때는 정상 작동하다가 <strong>로컬의 drawable에 있는 SGV 이미지(VectorDrawable)를 로드하면서</strong> 위와 같은 문구가 로그에 찍히는 것을 확인할 수 있었다.</p>
<br>

<h1 id="📝-해결">📝 해결</h1>
<p>검색해보니 해당 유형의 이미지를 decode 하는 과정에서 Glide 내에 버그가 있다는 의견이 많았다. v4 버전의 Glide 사용자 중에 SVG 파일, VectorDrawable을 로드하면서 비슷한 문제를 겪고 있는 사례가 많았고, 버전에 따라 Exception ➡️ 앱이 다운되는 경우도 있다고 한다.</p>
<pre><code class="language-kotlin">fun ImageView.load(drawable: Int) {
    val image = ContextCompat.getDrawable(context, drawable) as VectorDrawable
    Glide.with(this)
        .load(image)
        .transition(withCrossFade())
        .diskCacheStrategy(DiskCacheStrategy.ALL)
        .timeout(5000)
        .into(this)
}</code></pre>
<p><strong>이런 경우라면 <code>load()</code> 안에 drawable 값을 그대로 넣지 말고 ContextCompat을 사용해 Resource에서 직접 가져와서 넣어주면 해결된다❗️</strong> <code>placeholder()</code>를 작성할 때도 마찬가지로 적용할 수 있다.</p>
<p><em>상황에 따라 as VectorDrawable (캐스팅) 부분은 생략해도 된다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Fragment - show, hide 상태 체크하기]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Fragment-show-hide-%EC%83%81%ED%83%9C-%EC%B2%B4%ED%81%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Fragment-show-hide-%EC%83%81%ED%83%9C-%EC%B2%B4%ED%81%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 31 Oct 2022 20:20:24 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p>바텀네비케이션으로 하단 메뉴 탭을 구현하던 도중, Fragment 상태에 따라 StatusBar의 색상과 해당 탭의 Fragment 배경색을 변경해줘야했다. StatusBar는 Activity에서 변경 가능하지만 배경색의 경우 Fragment에 바인딩 된 뷰를 건드려야 했기 때문에 난감했다.</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

...

    private fun showFragment(fragment: Fragment, tag: String) {
        val findFragment = supportFragmentManager.findFragmentByTag(tag)
        supportFragmentManager.fragments.forEach { fm -&gt;
            supportFragmentManager.beginTransaction().hide(fm).commitAllowingStateLoss()
        }
        findFragment?.let {
            // 프래그먼트 상태 정보가 있는 경우, 보여주기만
            supportFragmentManager.beginTransaction().show(it).commitAllowingStateLoss()
        } ?: kotlin.run {
            // 프래그먼트 상태 정보가 없는 경우, 추가
            supportFragmentManager.beginTransaction()
                .add(R.id.fragmentContainer, fragment, tag)
                .commitAllowingStateLoss()
        }
    }

}</code></pre>
<p>처음에 해당 Fragment에서 <code>onResume()</code>을 통해 갱신해주면 되지 않을까 했지만 위처럼 단순히 우리 눈에 show, hide 되도록 작성된 코드라서 (상태 유지를 위해) <code>onResume()</code>가 호출되지 않았다.</p>
<h1 id="해결">해결</h1>
<pre><code class="language-kotlin">// Fragment 상태(show, hide)가 변경될 때마다 호출
override fun onHiddenChanged(hidden: Boolean) {
    super.onHiddenChanged(hidden)

    if (hidden) { // hide일 때
        setBaseStatusBar()
    } else { // show일 때
        changeStatusBarForTime()
    }
}</code></pre>
<p>역시나 다행히.. 이런 경우를 위해 <code>onHiddenChanged()</code> 라는 함수가 존재했다 ^_ㅠ
hidden 값을 이용해 조건문으로 원하는 처리를 해주면 된다. 위 코드는 실제 적용한 코드이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 RecyclerView GridLayoutManager 항상 일정한 아이템 간격 적용하기 (Column Space) - ItemDecoration]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView-GridLayoutManager-%ED%95%AD%EC%83%81-%EC%9D%BC%EC%A0%95%ED%95%9C-%EC%95%84%EC%9D%B4%ED%85%9C-%EA%B0%84%EA%B2%A9-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-Column-Space-ItemDecoration</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView-GridLayoutManager-%ED%95%AD%EC%83%81-%EC%9D%BC%EC%A0%95%ED%95%9C-%EC%95%84%EC%9D%B4%ED%85%9C-%EA%B0%84%EA%B2%A9-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-Column-Space-ItemDecoration</guid>
            <pubDate>Mon, 31 Oct 2022 15:18:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/c6b5a3b4-5635-4cb1-a4f6-e5d3907187a4/image.png" alt=""></p>
<p>보통 갤러리 어플들을 확인해보면 Grid 형태에, 일정한 간격으로 margin이 적용되어있는 모습을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/827f3dc1-251b-4c35-b589-403f892023bf/image.png" alt=""></p>
<p>RecyclerView을 이용해 이를 구현할 수 있는데 해당 아이템에 xml 코드에서 margin 값을 적용하게 되면 고정값을 지니게 되므로 아이템이 이어지는 부분끼리는 margin 값이 2배가 되어 위의 갤러리처럼 일정한 간격을 유지할 수 없어진다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/414dbcbc-8069-4f57-a2b0-c7b80dd091fd/image.png" alt=""></p>
<p>일정 간격을 유지하기 위해서는 아이템이 맞닿는 부분에 계산 과정을 한 번 거쳐 margin 값을  적용해줄 필요가 있다. 이를 위해 <strong>ItemDecoration</strong>을 사용해 볼 것이다.</p>
<h2 id="itemxml">item.xml</h2>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:background=&quot;?attr/selectableItemBackground&quot;&gt;

    &lt;androidx.cardview.widget.CardView
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;&gt;

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

            &lt;ImageView
                android:id=&quot;@+id/wearImageView&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;0dp&quot;
                android:adjustViewBounds=&quot;true&quot;
                android:src=&quot;@color/gray_500&quot;
                android:scaleType=&quot;centerCrop&quot;
                app:layout_constraintDimensionRatio=&quot;w,1:1&quot;
                app:layout_constraintEnd_toEndOf=&quot;parent&quot;
                app:layout_constraintStart_toStartOf=&quot;parent&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot;
                tools:src=&quot;@color/gray_200&quot; /&gt;

        &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;

    &lt;/androidx.cardview.widget.CardView&gt;

&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre>
<p>먼저 RecyclerView Adapter에서 바인딩할 레이아웃을 작성한다.
<em>(Adapter는 해당 포스팅에 따로 작성하지 않을 것입니다! 만약 작성법을 찾으신다면 <a href="https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView-Adapter-View-Binding-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">참고</a>해주세용)</em></p>
<p><strong>최상위 레이아웃인 ConstraintLayout을 <code>android:layout_width= &quot;match_parent&quot;</code> 로 설정해준 이유는 아이템의 크기를 반응형으로 보여주기 위함이다.</strong> 고정값이어도 상관 없지만 필자의 경우, 모든 디바이스에서 아이템끼리의 margin 간격이 반드시 16dp로 유지하면서 디바이스 크기에 의해 width 크기가 결정되도록 하기 위해 이렇게 설정했다.</p>
<p>ImageView의 모양을 정사각형(1:1) 비율로 적용하고 싶다면 <code>app:layout_constraintDimensionRatio=&quot;w,1:1&quot;</code> 을 작성해주면 된다.</p>
<h2 id="itemdecoration">ItemDecoration</h2>
<p>ItemDecoration 클래스는 말 그대로 <strong>아이템을 꾸며주는 역할을 하는 RecyclerView 내부의 추상 클래스</strong>이다. 주로 아이템 간 구분선이나 여백을 설정할 때 자주 응용된다. ItemDecoration는 총 3개의 함수를 제공해준다.</p>
<ul>
<li>onDraw() : 항목을 배치하기 전에 호출된다.</li>
<li>onDrawOver() : 모든 항목이 배치된 후에 호출된다.</li>
<li>getItemOffsets() : 각 항목을 배치할 때 호출된다.</li>
</ul>
<p>이 중에 <code>getItemOffsets()</code>을 이용할 것이다.</p>
<pre><code class="language-kotlin">internal class GridSpacingItemDecoration(
    private val spanCount: Int, // Grid의 column 수
    private val spacing: Int // 간격
) : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position: Int = parent.getChildAdapterPosition(view)

        if (position &gt;= 0) {
            val column = position % spanCount // item column
            outRect.apply {
                // spacing - column * ((1f / spanCount) * spacing)
                left = spacing - column * spacing / spanCount
                // (column + 1) * ((1f / spanCount) * spacing)
                right = (column + 1) * spacing / spanCount
                if (position &lt; spanCount) top = spacing
                bottom = spacing
            }
        } else {
            outRect.apply {
                left = 0
                right = 0
                top = 0
                bottom = 0
            }
        }
    }
}   </code></pre>
<p>Grid 아이템의 컬럼(RecyclerView에서 spanCount) 개수와 간격을 전달 받아 계산할 수 있도록 작성했다.</p>
<pre><code class="language-kotlin">binding.recyclerView.addItemDecoration(
    GridSpacingItemDecoration(spanCount = 2, spacing = 16f.fromDpToPx())
)

...

// 해당 함수는 util 패키지에 작성한 확장함수입니다 :)
fun Float.fromDpToPx(): Int = 
    (this * Resources.getSystem().displayMetrics.density).toInt()</code></pre>
<p>이런식으로 호출해주면 끝❗️</p>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/e07ffc0f-34de-404c-b87f-ebced83308fa/image.png" alt=""></p>
<p>작업중인 APP에 적용해 데려와보았다 :)</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/db354e3f-53d0-4eb7-b1d8-671330f10ab5/image.png" alt=""></p>
<pre><code class="language-kotlin">GridSpacingItemDecoration(spanCount = 3, spacing = 16f.fromDpToPx())</code></pre>
<p>요건 spanCount을 3으로 전달한 경우인데, 앞에서 언급한 것처럼 여백 16dp는 고정으로 가져가고 디바이스 크기에 의해 width 크기가 결정되는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Hilt 공부를 위한 첫 걸음]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Hilt-%EA%B3%B5%EB%B6%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%B2%AB-%EA%B1%B8%EC%9D%8C</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Hilt-%EA%B3%B5%EB%B6%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%B2%AB-%EA%B1%B8%EC%9D%8C</guid>
            <pubDate>Wed, 19 Oct 2022 09:30:17 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>앞으로 진행하는 프로젝트에 Hilt를 적용해보고 싶은 개발자가 옥수환님께서 발표하신 <a href="https://youtu.be/gkUCs6YWzEY">드로이드나이츠 2020 - Hilt와 함께 하는 안드로이드 의존성 주입</a> 영상을 보고 정리한 글입니다. 혹시 잘못 이해하고 작성한 부분이나 오타에 대한 피드백을 주신다면 감사히 받겠습니다 🙇🏻‍♀️</p>
</blockquote>
<hr>
<h1 id="의존성-주입-dependency-injection">의존성 주입 (Dependency Injection)</h1>
<p>생성자 또는 메서드 등을 통해 외부로부터 생성된 객체를 전달받는 것</p>
<h3 id="의존성-주입의-특징">의존성 주입의 특징</h3>
<ul>
<li>클래스 간 결합도를 느슨하게 한다.</li>
<li>인터페이스 기반으로 설계되며, 코드를 유연하게 한다.</li>
<li>Stub 또는 Mock 객체를 사용하여 단위 테스트를 하기 더욱 쉬워진다.</li>
</ul>
<h3 id="안드로이드에서-의존성-주입이-어려운-이유">안드로이드에서 의존성 주입이 어려운 이유</h3>
<ul>
<li>Android 클래스가 프레임워크에 의해 인스턴스화 됨
➡️ 개발자가 해당 클래스 내부에 생성자를 만들거나 생성자의 매개변수를 전달할 방법이 없다.</li>
<li>Factory를 API28부터 제공하지만 현실적이지 않음</li>
</ul>
<br>

<h1 id="dagger2">Dagger2</h1>
<p>자바와 안드로이드를 위한 강력하고 빠른 의존성 주입 프레임워크</p>
<ul>
<li>컴파일 타임에 어노테이션 프로세스를 사용하여 의존성 주입에 관련된 모든 코드 생성
➡️ 명확하고 디버깅 가능하며 리플렉션을 사용하거나 런타임에 바이트 코드를 생성하지 않는다.</li>
<li>생명주기와 계층 별로 잘 정리된 오브젝트 그래프에서 객체들을 공유할 수 있는 방법을 제공한다.</li>
<li>작은 라이브러리 크기</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>배우기 어렵고, 프로젝트 설정이 힘들다.</li>
<li>간단한 프로그램을 만들 때는 번거롭다.</li>
<li>같은 결과에 대한 다양한 방법이 존재한다.</li>
</ul>
<p>개발자 중 49%가 DI 솔루션 개선을 요청함 😵</p>
<br>

<h2 id="의존성-주입-프레임워크의-궁극적인-목표">의존성 주입 프레임워크의 궁극적인 목표</h2>
<ul>
<li>정확한 사용방법을 제안</li>
<li>쉬운 설정 방법</li>
<li>중요한 것들에 집중할 수 있도록 함<ul>
<li>프로젝트 빌드를 시작한 뒤에 사소한 문제로 인해 컴파일 에러 발생
→ 장점이자 단점, 너무 빈번해지면 개발자의 생산성을 저하시키는 요소가 된다.</li>
</ul>
</li>
</ul>
<br>

<h1 id="hilt-🗡">Hilt 🗡</h1>
<p>DI를 사용하는 표준적인 방법을 제공한다.
➡️ 중구난방이었던 Dagger의 사용법을 획일적이게 만든다는 것 ❗️</p>
<br>

<h2 id="hilt의-목표">Hilt의 목표</h2>
<ul>
<li>Dagger 사용의 단순화</li>
<li>표준화된 컴포넌트 세트와 스코프로 설정과 가독성 / 이해도 쉽게 만들기</li>
<li>쉬운 방법으로 다양한 빌드 타입에 대해 다른 바인딩 제공</li>
</ul>
<br>

<h2 id="특징">특징</h2>
<ul>
<li>Dagger2 기반의 라이브러리</li>
<li>표준화된 Dagger2 사용법을 제시</li>
<li>보일러 플레이트 코드 감소
➡️ Google IO 앱을 Hilt로 리팩토링한 결과, 의존성 주입 코드를 75% 삭제했다고 함 👀</li>
<li>프로젝트 설정 간소화</li>
<li>쉬운 모듈 탐색과 통합</li>
<li>개선된 테스트 환경</li>
<li>Android Studio의 지원
➡️ 4.2 버전 이후 부터는 Hilt의 오브젝트 그래프를 가시화하여 볼 수 있음</li>
<li>AndroidX 라이브러리의 호환 (WorkManager, ViewModel 등)</li>
</ul>
<br>

<h2 id="object-graph">Object graph</h2>
<pre><code class="language-kotlin">// @Inject : 의존성 주입을 받겠다.
class MemoRepository @Inject constructor(
    private val db: MemoDatabase
) {
    fun load(id: String) {...}
}</code></pre>
<pre><code class="language-kotlin">// @HiltAndroidApp : Hilt 사용시 반드시 선행 되어야 하는 부분, 모든 의존성 주입의 시작점
@HiltAndroidApp
class MemoApp : Application()

// @AndroidEntryPoint : 안드로이드 클래스에 추가,
// Activity 안에 선언 된 @Inject 어노테이션에 대해 의존성 주입 수행
@AndroidEntryPoint
class MemeActivity : AppCompatActivity() {

    @Inject lateinit var repository: MemoRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        repository.load(&quot;YHLQMDLG&quot;)
    }
}</code></pre>
<pre><code class="language-kotlin">// @InstallIn : 해당 컴포넌트의 모듈이 설치 되게 한다.
@InstallIn(ApplicationComponent::class)
@Module
object DataModule {

        // @Provides : 모듈 클래스 내에 데이터베이스 객체 생성
    @Provides
    fun provideMemoDB(@ApplicationContext context: Context) =
        Room.databaseBuilder(context, MemoDatabase::class.java, &quot;Memo.db&quot;)
            .build()
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/0828f122-d389-4842-a9d3-62e0fce73177/image.png" alt=""></p>
<ol>
<li><code>@HiltAndroidApp</code> 를 통해 ApplicationComponent가 생성된다.</li>
<li><code>@InstallIn</code> 를 모듈 클래스에 추가하여 해당 컴포넌트의 모듈이 설치 되게 한다.</li>
<li><code>@AndroidEntryPoint</code> 를 Activity에 추가함으로써 ApplicationCompoenet의 하위 컴포넌트인 ActivityComponent가 생성되고 MemoRepository 객체를 주입 받을 수 있게 된다.</li>
</ol>
<br>

<h2 id="hiltandroidapp-없이-컴포넌트-생성하기">@HiltAndroidApp 없이 컴포넌트 생성하기</h2>
<pre><code class="language-kotlin">class MemoApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        val component = DaggerMemoComponent.builder()
            ...
            .build()
        component.inject(this)
    }
}</code></pre>
<p>정의된 Component는 컴파일 타임에 Dagger라는 접두어가 붙은 Component 클래스를 생성하게 되고,
Application의 <code>onCreate()</code> 메소드에서 컴포넌트를 인스턴스화하는 것이 일반적인 형태였다.</p>
<h2 id="하지만-hiltandroidapp를-사용하면">하지만 @HiltAndroidApp를 사용하면?</h2>
<pre><code class="language-kotlin">@HiltAndroidApp // 위의 과정을 생략하고, @HiltAndroidApp만 추가하면 된다.
class MemoApplication : Application() {

    override fun onCreate() {
        super.onCreate() // 의존성 주입은 super.onCreate()에서 이루어짐 (bytecode 변환)
    }
}</code></pre>
<p><strong>@HiltAndroidApp</strong> : Hilt 코드 생성을 시작, 반드시 Application 클래스에 추가
해당 어노테이션 추가만으로 ApplicationComponent 코드를 생성 및 인스턴스화 하는 코드가 만들어진다.</p>
<p>컴포넌트를 인스턴스화 하는 부분은 상위 클래스의 <code>onCreate()</code> 에서 이루어진다.
➡️ 바이크 코드 변환 때문에 이러한 과정이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/81594f37-edf6-4d25-b96a-a67b7e0d1774/image.png" alt=""></p>
<p><strong><code>@HiltAndroidApp</code> 이 붙은 MemoApplication 클래스는 컴파일 타임에 Hilt 접두어가 붙은 Hilt_MemoApplication 클래스를 생성한다.</strong> 생성된 Hilt 클래스는 Base 클래스가 상속한 클래스를 똑같이 상속하게 된다. 예제에서는 Base 클래스가 Application을 상속했기 때문에 생성된 Hilt_MemoApplication 클래스도 Application 클래스를 상속하고 있는 것을 확인할 수 있다. <strong>생성된 Hilt 클래스는 컴포넌트의 인스턴스 및 의존성 주입 관련 코드들을 포함하고 있다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/8ccc92e4-ce58-4bd1-ad9a-4f53cbf188f0/image.png" alt=""></p>
<p>Hilt_MemoApplication을 상속해야할 것 같지만 실제로는 그러지 않아도 된다❗️</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/b71145f4-377d-497a-b273-379828ac2d1d/image.png" alt=""></p>
<p><strong>MemoApplication 소스코드는 컴파일을 거쳐 바이트 코드를 산출하고, 그 이후에 Gradle 플러그인이 개입하여 바이트 코드를 조작하기 때문이다.</strong></p>
<p>즉, MemoApplication 바이트 코드는 Hilt_MemoApplication를 상속하는 코드로 변환이 되는데 이는 개발자의 편의성을 위해 도모되었으며, 만약 바이트 코드 변환을 원하지 않는다면 Gradle 플러그인을 비활성화 시키면 된다.</p>
<pre><code class="language-kotlin">@HiltAndroidApp(Application::class)
class MyApplication : Hilt_MyApplication()</code></pre>
<p>만약 Gradle 플러그인을 사용하지 않는다면 <code>@HiltAndroidApp</code> 에 Application 클래스가 상속할 클래스를 명시하고, 실제로 상속하는 클래스는 생성된 Hilt_MyApplication 클래스가 되어야한다.</p>
<br>

<h2 id="androidentrypoint">@AndroidEntryPoint</h2>
<p>어노테이션이 추가된 안드로이드 클래스에 DI 컨테이너를 추가
<code>@HiltAndroidApp</code> 의 설정 후 사용 가능</p>
<h3 id="dagger2-관점에서-보면">Dagger2 관점에서 보면</h3>
<ul>
<li><code>@HiltAndroidApp</code> → Component 생성</li>
<li><code>@AndroidEntryPoint</code> → Subcomponent 생성</li>
</ul>
<h3 id="androidentrypoint를-지원하는-타입">@AndroidEntryPoint를 지원하는 타입</h3>
<ul>
<li>Activity</li>
<li>Fragment</li>
<li>View</li>
<li>Service</li>
<li>BroadcastReceiver</li>
</ul>
<p><em>(ContentProvider는 까다로운 생명주기 때문에 지원하지 않기 때문에 다른 방법으로 의존성 주입이 가능하다.)</em></p>
<br>

<h2 id="hilt-component의-계층구조">Hilt Component의 계층구조</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/22767236-11c7-42c6-baec-1ad5faac874f/image.png" alt=""></p>
<ul>
<li>Hilt는 이미 정의된 표준화된 컴포넌트 세트를 제공한다.</li>
<li><code>@AndroidEntryPoint</code> 를 사용하여 해당 타입에 맞는 컴포넌트를 추가하게 된다.</li>
<li><strong>하위 컴포넌트는 상위 컴포넌트가 가지고 있는 의존성에 대해 접근할 수 있다.</strong> ➡️ 직계 수직 관계에서만 접근이 가능하다.</li>
</ul>
<h2 id="hilt-component-특징">Hilt Component 특징</h2>
<ul>
<li>Dagger와 다르게 직접적으로 인스턴스화할 필요가 없다. (Hilt는 바이트 코드 변환을 사용하기 때문)</li>
<li>각 생명주기와 기능에 알맞은 표준화된 컴포넌트 세트와 스코프를 제공한다.</li>
<li>컴포넌트들은 계층으로 이루어져 있으며, 하위 컴포넌트는 상위 컴포넌트의 의존성에 접근할 수 있다. (=Subcomponent)</li>
</ul>
<br>

<h2 id="hilt-scope">Hilt Scope</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/c535c11e-1aa1-460a-84ac-b63cd4d90495/image.png" alt=""></p>
<ul>
<li>표준화된 스코프를 제공한다.</li>
<li>모듈 클래스에서 Scope 어노테이션을 사용하여 동일 인스턴스를 공유할 수 있다.</li>
<li>retained가 붙어있는 scope는 Configuration(language, orientation 등)에도 유지된다.</li>
</ul>
<h2 id="scoped-binding">Scoped Binding</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/b72f0989-f6be-4e65-b68c-9f455ebccc33/image.png" alt=""></p>
<pre><code class="language-kotlin">// No scope annotation
class MemoRepository @Inject constructor(
    private val db: MemoDatabase
) {
    fun load(id: String) { ... }
}</code></pre>
<p><strong>위 코드처럼 Scope를 지정해주지 않으면 MemoRepository를 다른 Activity에서 inject 하게 되면 매번 새로 생성하기 때문에 서로 다른 인스턴스가 되어버린다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/351687a3-6fdf-4f40-a7d5-35e03cf3b326/image.png" alt=""></p>
<pre><code class="language-kotlin">@Singleton
class MemoRepository @Inject constructor(
    private val db: MemoDatabase) 
{
    fun load(id: String) { ... }
}</code></pre>
<p>모듈에서 사용되는 Scope 어노테이션은 반드시 InstallIn에 명시된 컴포넌트와 쌍을 이루는 Scope를 사용해야 한다.</p>
<p><strong>두 Activity가 ApplicationComponent에 설치된 모듈로부터 동일한 MemoRepository 인스턴스를 주입받은 것을 확인할 수 있다.
이처럼 Hilt는 자원 공유를 쉽게 할 수 있도록 도와준다.</strong></p>
<br>

<h2 id="기본-컴포넌트-바인딩">기본 컴포넌트 바인딩</h2>
<p>컴포넌트는 기본적으로 아래와 같은 객체를 그래프에 바인딩한다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/60276be9-b943-452a-acd7-ed55c55be63a/image.png" alt=""></p>
<p>Dagger의 <code>@BindsInstance</code> 을 사용하지 않아도 컴포넌트에 따라 Application, Activity, Context 등과 같은 인스턴스를 제공받을 수 있다.
대신 Context를 요청할 때는 <code>@ApplicationContext</code> 또는 <code>@ActivityContext</code> 를 사용하여 요청하는 Context의 종류를 명확히 해주어야 한다.</p>
<br>

<h2 id="hiltmodule">HiltModule</h2>
<h3 id="installin">@InstallIn</h3>
<ul>
<li>Hilt가 생성하는 DI 컨테이너에 어떤 모듈을 사용할지 가리킨다.</li>
<li><strong>해당 모듈이 어떤 컴포넌트에 설치될 것인지 명시해야 하고, Hilt는 이를 보고 컴파일 타임에 관련 코드를 생성한다.
( 중요❗️- 올바르지 않은 컴포넌트 또는 스코프를 사용하면 컴파일 에러를 발생시킨다.)</strong></li>
</ul>
<pre><code class="language-kotlin">@InstallIn(ActivityComponent::class)
@Module
object MyModule {
    ...
}</code></pre>
<p>아래의 경우, ActivityComponent를 명시했기 때문에 당연히 ActivityComponent에 설치된다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/c0cdde44-aca7-4590-a345-33f9f2892ab6/image.png" alt=""></p>
<p>🤔 하지만 만약 AcitivtyComponent와 FragmentComponent 모두 MyModule이 필요하다면?</p>
<blockquote>
<p><strong>하위 컴포넌트는 상위 컴포넌트의 의존성에 Access 할 수 있기 때문에 상위 컴포넌트의 모듈을 설치하는 것을 고려할 수 있다.</strong>
ApplicationComponent의 모듈을 설치하면 모든 컴포넌트들이 의존성에 접근할 수 있다.</p>
</blockquote>
<p>따라서, ApplicationComponent 혹은 AcitivityComponent에 설치하면 된다.</p>
<h3 id="hilt-module의-제약사항">Hilt Module의 제약사항</h3>
<p><code>@Module</code> 클래스에 <code>@InstallIn</code>이 없으면 컴파일 에러를 발생시킨다. (모듈이 어느 컴포넌트에 설치되는지 명확하게 확인하기 위해 )</p>
<pre><code class="language-kotlin">// @InstallIn 검사 비활성화
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += [&quot;dagger.hilt.disableModulesHaveInstallInCheck&quot;:&quot;true&quot;]
            }
        }
    }
}</code></pre>
<p>만약 Dagger를 사용하는 프로젝트를 Hilt로 마이그레이션 해야 하는 경우, build.gradle(Module) 파일에 해당 내용을 추가해야 한다.</p>
<h3 id="entrypoint">@EntryPoint</h3>
<p>Hilt가 지원하지 않는 클래스에서 의존성이 필요한 경우 사용
(ex. ContentProvier, DFM, Dagger를 사용하지 않는 3rd-party 라이브러리 등)</p>
<ul>
<li><code>@EntryPoint</code> 는 인터페이스에서만 사용할 수 있다.</li>
<li><code>@InstallIn</code> 을 사용해서 어떤 컴포넌트에 접근할 것인지 명시해야 한다.</li>
<li>EntryPoints 클래스의 정적 메서드를 통해 해당 컴포넌트 그래프에 접근할 수 있다.</li>
</ul>
<pre><code class="language-kotlin">// EntryPoint 생성하기
@EntryPoint
@InstallIn(ApplicationComponent::class)
intreface FooBarInterface {
    fun getBar: Bar
}

// EntryPoint로 접근하기
val bar = EntryPoints.get(
    application,
    FooBarInterface::class.java
).getBar()</code></pre>
<h3 id="entrypoint-예제-1---contentprovider">@EntryPoint 예제 1 - ContentProvider</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/e0cdc1b4-b595-44f2-a825-d86f2a3c9f2b/image.png" alt=""></p>
<p><code>@AndroidEntryPoint</code> 를 지원하지 않기 때문에 <code>@EntryPoint</code> 를 사용해야 한다.</p>
<pre><code class="language-kotlin">@EntryPoint
@InstallIn(ApplicationComponent::class)
interface *MemoEntryPoint* {
    fun getRepository(): MemoRepository
}</code></pre>
<p>MemoRepository를 제공받기 위해 MemoEntryPoint라는 인터페이스를 작성한다.</p>
<pre><code class="language-kotlin">class MemoProvider: ContentProvider() {
    override fun query() {
        val entryPoint = EntryPointAccessors.fromApplication(context, MemoEntryPoint::class.java)
        val repository = entryPoint.getRepository()
    }
}</code></pre>
<p>ContentProvider에서 ApplicationComponent에 있는 MemoRepository에 접근하기 위해 작성된 코드이다.</p>
<blockquote>
<p><strong>EntryPointAccessors</strong></p>
</blockquote>
<ul>
<li>EntryPoints를 감싼 Util성 클래스</li>
<li>EntryPoint 를 쉽게 가져올 수 있도록 도와준다.</li>
</ul>
<h3 id="entrypoint-예제-2---dfm">@EntryPoint 예제 2 - DFM</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/ef69d13b-ccb9-4a87-aef6-0e62aa5f4097/image.png" alt=""></p>
<p><strong>프로젝트를 DFM(Dynamic Featur Module) 구성하는 경우 기본 App 모듈에 의존하게 된다.</strong></p>
<p>기본 App 모듈은 DFM을 참조할 수 없기 때문에 컴파일 타임에서 SubComponent 구조로 그래프를 생성하는 것이 불가능하다. 하지만 컴포넌트를 상속하는 구조인 Dagger의 Component dependency 기능을 사용하면 DFM에서 ApplicationComponent를 상속하는 컴포넌트를 생성할 수 있게 된다.</p>
<pre><code class="language-kotlin">@Component(dependencies = [MemoEntryPoint::class])
interface MemoEditComponent {
    fun inject(activity: MemoEditActivity)

    @Component.Builder
    interface Builder {
        fun context(@BindInstance context: Context): Builder
        fun dependencies(entryPoint: MemoEntryPoint): Builder
        fun build(): MemoEditComponent
    }
}</code></pre>
<p>먼저 DFM 쪽에 Dagger 컴포넌트 인터페이스를 정의한다. EntryPoint를 의존하는 MemoEditComponent를 만들면 EntryPoint가 제공하는 MemoRepository를 MemoEditComponent가 접근할 수 있게 된다.</p>
<pre><code class="language-kotlin">class MemoEditActivity: AppCompatActivity() {

    @Inject
    lateinit var repository: MemoRepository

    ovrride fun onCreate(savedInstanceState: Bundle?) {
        DaggerMemoEditComponent.builder().context(this)
            .dependencies(EntryPointsAccessors.fromApplication(applicationContext, MemoEntryPoint::class.java))
            .build()
            .inject(this)

    super.onCreate(savedInstanceState)
    }
}</code></pre>
<p>EntryPointsAccessors를 통해 ApplicationContext의 일부인 MemoEntryPoint를 받아 DaggerMemoEditComponent를 인스턴스화하고, 마침내 MemoEditActivity에 MemoRepository를 멤버 주입하는 것을 확인할 수 있다.</p>
<br>

<h1 id="그-외-hilt에-대한-내용들">그 외 Hilt에 대한 내용들</h1>
<h2 id="androidx-extensions"><strong>AndroidX Extensions</strong></h2>
<p>Hilt는 Jetpack 라이브러리와 함께 사용할 수 있도록 확장(extension) 라이브러리를 제공한다.
현재 지원하는 Jetpack 컴포넌트로는 ViewModel과 WorkManager가 있다.</p>
<br>

<h3 id="💉-hilt로-viewmodel-주입하기">💉 Hilt로 ViewModel 주입하기</h3>
<p>기존의 Dagger로 ViewModel을 주입하는 것은 상당히 까다로운 작업이었다. 컴포넌트 인스턴스화 끝난 후에 변경될 수 있는 동적인 매개변수 SavedStateHandle을 Dagger의 그래프에 포함시키는 것은 거의 불가능하다.</p>
<p>하지만 이 부분은 Square 사에서 만든 AssistedInject 라이브러리를 통해 해결이 가능하며 Hilt에도 이 기능이 기본적으로 포함되어 있다. 
<strong>ViewModel을 주입하는 것을 Dagger와 AssistedInject로 직접 구현하려면 매우 복잡하지만 Hilt는 이를 단순화 시켜준다.</strong></p>
<pre><code class="language-kotlin">class MemoViewModel @ViewModelInject constructor(
    private val repository: MemoRepository,
    @Assisted private val savedStateHandle: SavedStateHandle
): ViewModel() { ... }

@AndroidEntryPoint
class MemoActivity: AppCompatActivity() {
    pirvate val viewModel: MemoViewModel by viewModels()
}</code></pre>
<ol>
<li>ViewModel 생성자에 <code>@ViewModelInject</code>을 붙인다.</li>
<li>ViewModel 생성자 매개변수인 savedStateHandle은 <code>@Assisted</code>를 추가적으로 덧붙인다.
➡️ 내부적으로 SavedStateHandle을 가져오는 Factory를 통해 동적인 주입이 가능해진다.</li>
<li>MemoViewModel을 인스턴스화한다.</li>
</ol>
<br>

<h3 id="💉-hilt로-worker-주입하기">💉 Hilt로 Worker 주입하기</h3>
<p>WorkManager의 Worker도 ViewModel과 동일한 방식으로 동작한다.
다만 <code>@WorkerInject</code>을 사용하는 것만 다르다.</p>
<pre><code class="language-kotlin">class ExampleWorker @WorkerInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    workDependency: WorkerDependency
): Worker(appContext, workerParams) { ... }

@HiltAndroidApp
class ExampleApplication: Application(), Configuration.Provider {

    @Inject lateinit var workerFactory: HiltWorkerFactory

    override fun getWorkManagerConfiguration() = 
        Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()

}</code></pre>
<ol>
<li>Worker를 정의한다.</li>
<li>Application 클래스에 Configuration.Provider 인터페이스 구현한다.
(Hilt가 제공하는 HiltWorkerFactory를 주입받아 WorkManager를 설정한다.)</li>
</ol>
<br>

<h2 id="custom-component">Custom Component</h2>
<ul>
<li>표준 Hilt 컴포넌트 이외에 새로운 컴포넌트를 만드는 것</li>
<li>커스텀 컴포넌트를 사용하면 복잡하고 이해하기 어려워지기 때문에 꼭 필요한 경우만 사용한다.</li>
</ul>
<p>Hilt는 표준화된 컴포넌트를 가지고 있다. 하지만 간혹 컴포넌트 객체의 수명이 맞지 않거나 특정 기능을 필요로 하는 상황들이 생기기 마련이다. 이런 경우에는 커스텀 컴포넌트를 정의할 필요가 있다. 커스텀 컴포넌트의 생성은 그래프를 복잡하게 만들기 때문에 생성하기 전에 논리적으로 반드시 필요한 것인지 생각해야 한다.</p>
<p>커스텀 컴포넌트를 정의하는 것은 Dagger의 컴포넌트를 정의하는 것과 크게 다를 것이 없다. 다른 점은 DefineComponent를 사용한다는 것이다.</p>
<h3 id="사용하는-annotation">사용하는 Annotation</h3>
<ul>
<li>@DefineComponent</li>
<li>@DefineComponent.Builder</li>
</ul>
<h3 id="제약조건">제약조건</h3>
<ul>
<li>반드시 ApplicationComponent의 하위 계층의 컴포넌트로 만들어야 한다.</li>
<li>표준 컴포넌트 계층 사이에 추가할 수 없다.</li>
</ul>
<p>ex) ActivityComponent와 FragmentComponent 사이에 추가될 수 없다.</p>
<br>

<h2 id="hilt의-설계-철학">Hilt의 설계 철학</h2>
<p>단일체로 된 컴포넌트
예를 들어, 각 Activity는 분리된 Component 인스턴스를 가지지만 정의된 컴포넌트 클래스는 하나로 공용된다.</p>
<h3 id="monolithic-components-특징">Monolithic components 특징</h3>
<ul>
<li><strong>Single binding key space</strong>
특정 바인딩이 어디로부터 왔는지 추적하기 쉬워지고 코드량도 줄어든다.</li>
<li><strong>간단한 설정</strong>
모듈이 설치될 수 있는 부분을 줄이기 때문에 설정 및 테스트가 간단해진다.</li>
<li><strong>생성되는 코드를 줄인다.</strong>
모듈이 여러 SubComponent에 사용되면서 반복적으로 생기게 된다.
➡️ Activity, Fragment, View 등을 통해 급격하게 늘어날 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Glide 없이 LruCache로 이미지 캐싱하기 (Bitmap Cache)]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Glide-%EC%97%86%EC%9D%B4-LruCache%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%98%EA%B8%B0-Bitmap-Cache</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Glide-%EC%97%86%EC%9D%B4-LruCache%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%98%EA%B8%B0-Bitmap-Cache</guid>
            <pubDate>Wed, 28 Sep 2022 04:41:42 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀 
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 잘못 이해하고 작성한 부분에 대한 피드백을 주신다면 감사히 받겠습니다 🙇🏻‍♀️</p>
</blockquote>
<hr>
<p>이미지 로딩을 위해 동일한 이미지를 매번 다운로드 하는 것은 비효율적이다. 한 번 다운로드 했던 이미지는 임시 저장했다가 재사용할 수 있도록 캐싱을 해주는 것이 일반적❗️ Glide 라이브러리에서는 이러한 캐싱 뿐만 아니라 다양한 기능을 제공해주기 때문에 항상 사용해왔다.</p>
<p>하지만 <em><strong>&quot;Android : org.jetbrains.* , androidx.* 외에 다른 라이브러리는 추가하지 않습니다.&quot;</strong></em> 라는 제한이 걸린다면❓ 실제로 프로그래머스 과제 테스트의 제한 사항이었다. 기본의 중요성을 생각해보며.. Glide 없이 이미지 캐싱을 진행해보자 🙋🏻‍♀️❗️</p>
<h1 id="lrucache">LruCache</h1>
<p>LruCache는 LRU 알고리즘에 대한 구현체로, 안드로이드에서 캐시를 관리하기 위해 제공하는 메모리 캐시 객체다.</p>
<blockquote>
<h3 id="잠깐-lru-알고리즘이란-🤷🏻♀️">잠깐! LRU 알고리즘이란? 🤷🏻‍♀️</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/06d7ec44-12c7-447c-b586-2501aa6d5be4/image.png" alt="">
<strong>LRU (Least Recently Used)</strong>
가장 오랜 시간 사용되지 않은(참조되지 않은) 페이지를 교체하는 기법
메모리를 관리하는 운영체제에서 페이지 부재가 발생하여 새로운 페이지를 할당하기 위해 현재 할당된 페이지 중 어느것과 교체할지 결정하는 방법<br>
<strong>➡️ 최근에 조회된 것을 캐시에서 삭제하는 것을 늦추기 위한 것으로, 오랫동안 접근되지 않은 메모리가 우선적으로 삭제된다.</strong></p>
</blockquote>
<pre><code class="language-java">public class LruCache&lt;K, V&gt; { // 제네릭으로 선언됨, &lt;K : 캐시에 접근하기 위한 키 값, V : 캐시에서 가져올 객체 타입&gt;

    public LruCache(int maxSize) { // 생성자 인자로 maxSize를 int 값으로 받음
        throw new RuntimeException(&quot;Stub!&quot;);
    }

    . . .
}
</code></pre>
<p>필자는 &lt; <code>Key</code> : <strong>이미지 url(String)</strong>, <code>Value</code> : <strong>Bitmap</strong> &gt; 으로 설정하여 LruCache를 익명 객체로 선언하였다. 생성자 인자에 넣어줄 캐시 사이즈는 전체 메모리 사이즈 중 1/8로 설정했다.</p>
<pre><code class="language-kotlin">// 전체 메모리 사이즈 중 1/8을 LruCache 사이즈로 잡는다.
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
val bitmapCache = object : LruCache&lt;String?, Bitmap?&gt;(cacheSize) {
    override fun sizeOf(key: String?, value: Bitmap?): Int {
        return value?.byteCount!! / 1024
    }
}

. . .

// 만약 일반 클래스로 선언하고 싶다면 아래와 같이 선언해주면 된다.
class BitmapLruCache(cacheSize: Int) : LruCache&lt;String?, Bitmap?&gt;(cacheSize) {
    override fun sizeOf(key: String?, value: Bitmap?): Int {
        return value?.byteCount!! / 1024
    }
}</code></pre>
<h3 id="lrucache에-bitmap-저장하기">LruCache에 Bitmap 저장하기</h3>
<pre><code class="language-kotlin">bitmapCache.put(url, bitmap)</code></pre>
<h3 id="lrucache에서-bitmap-가져오기">LruCache에서 Bitmap 가져오기</h3>
<pre><code class="language-kotlin">val bitmap: Bitmap? = bitmapCache.get(url)</code></pre>
<p>만약 <strong>bitmap의 변수가 null 값이라면 캐시에 값이 없다는 것</strong>이므로 이를 이용해 조건문으로 우리가 원하는 처리를 해주면 된다.</p>
<pre><code class="language-kotlin">
object ImageLoader {

    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    val cacheSize = maxMemory / 8
    private val bitmapCache = object : LruCache&lt;String?, Bitmap?&gt;(cacheSize) {
        override fun sizeOf(key: String?, value: Bitmap?): Int {
            return value?.byteCount!! / 1024
        }
    }

    fun loadImage(url: String, completed: (Bitmap?) -&gt; Unit) {
        if (url.isEmpty()) {
            completed(null)
            return
        }

        GlobalScope.launch(Dispatchers.IO) {
            try {
                var bitmap: Bitmap? = bitmapCache.get(url)

                if (bitmap == null) {
                    bitmap = BitmapFactory.decodeStream(URL(url).openStream())
                    bitmapCache.put(url, bitmap)
                }

                withContext(Dispatchers.Main) {
                    completed(bitmap)
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    completed(null)
                }
            }
        }
    }

}
</code></pre>
<p><code>loadImage()</code> 는 url로 이미지를 로딩하기 위한 메소드이다.</p>
<ul>
<li>url을 조회하여 캐시에 해당 Bitmap이 있으면 이를 반환하고</li>
<li>없으면 String(Url) ➡️ Bitmap 변환 처리를 해준 후 캐시에 저장을 해주고 이를 반환해준다.
(변환 처리를 해주는 과정에서 <a href="https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Bitmap-%EC%B5%9C%EC%A0%81%ED%99%94Resize%ED%95%9C-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%9C%EB%B2%84%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-1-%EB%B9%84%ED%8A%B8%EB%A7%B5-%EB%8B%A4%EC%9D%B4%EC%96%B4%ED%8A%B8-%EC%8B%9C%ED%82%A4%EA%B8%B0">Bitmap resize</a>를 해주는 것도 좋다.)</li>
</ul>
<p>( + 이미지 다운로드 자체가 네트워크를 사용하는 IO 작업이기 때문에 UI Thread에서 처리하면 안되는거 아시쥬..?! UI Thread에서 네트워크까지 처리해버리면 응답이 언제 올지 모르고.. 시간은 계속 지체되고.. ANR(앱이 일정 시간 내에 응답하지 못해 발생) 에러를 경험하게 될 것입니다.. 🥶 )</p>
<hr>
<h2 id="깨달음">깨달음</h2>
<p><em><strong>&quot;Android : org.jetbrains.* , androidx.* 외에 다른 라이브러리는 추가하지 않습니다.&quot;</strong></em></p>
<p>문구를 보고 순간 얼어붙었던 나..</p>
<p>결국 이 라이브러리도 기본 기능들을 조합해 만들어진 것인데, 라이브러리에게 맡기는 것을 당연하게 생각하다보니 정작 중요한 것을 놓치고 있었던 것 같다. 이번 기회에 크게 반성했다 🥲</p>
<p>언제나 중요한 것은 기본.. 기본을 열심히 해두자 ✍🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Api key 숨기고 안전하게 관리하기]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Api-key-%EC%88%A8%EA%B8%B0%EA%B3%A0-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Api-key-%EC%88%A8%EA%B8%B0%EA%B3%A0-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 07 Sep 2022 08:26:19 GMT</pubDate>
            <description><![CDATA[<p>오픈 api 를 사용한 프로젝트들을 깃허브 올리다보면 <strong>&#39;근데 이거 api key 노출 해도 괜찮나?&#39;</strong> 하고 의문을 가지게 될 것이다.</p>
<p><strong>사실 알잖아요, 괜찮지 않다는 것 😂😭 !!!!!</strong> 혹시 나의 api key 를 악용하는 사람이 나타난다면...? 우리의 서비스도, 개인정보도, Money 💸 도 공중분해 될 수 있다 ... (´༎ຶД༎ຶ`)</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/06ff247d-550a-4ff2-b434-46bff8c2d7e7/image.png" alt=""></p>
<p>하지만 이제는 아니다. 우리는 <strong>gradle</strong>을 사용해 안전하게 관리할 것이기 때문에 😤 !!</p>
<h2 id="1-localproperties에-key-값을-정의한다">1. local.properties에 key 값을 정의한다.</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/770c25c0-aaf4-4003-bfee-4707578cb802/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/910482c3-4036-480d-a71d-376d1fc94285/image.png" alt=""></p>
<h3 id="🙋🏻♀️-localproperties-이라고-노출을-막을-수-있는거-맞나요">🙋🏻‍♀️ local.properties 이라고 노출을 막을 수 있는거 맞나요?</h3>
<p>➡️ 보통 <strong>gitignore</strong>(git에 올리고 싶지 않은 파일이나 폴더를 정의) 파일에 local.properties 가 기본으로 정의되어 있기 때문에 괜찮지만, 혹시 모르니 한 번 확인하는 것을 권장한다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/030f1ad0-b9df-4f15-82cd-bf2d6680d2f3/image.png" alt=""></p>
<h2 id="2-buildgradlemoduleapp에-사용할-key-변수를-선언한다">2. build.gradle(Module:app)에 사용할 key 변수를 선언한다.</h2>
<pre><code class="language-java">plugins {
    id &#39;com.android.application&#39;
    id &#39;org.jetbrains.kotlin.android&#39;
    ...
}

// 선언 및 key 값 가져오기
Properties properties = new Properties()
properties.load(project.rootProject.file(&#39;local.properties&#39;).newDataInputStream())

android {
    ...

    buildTypes {
        debug {
            resValue &quot;string&quot;, &quot;GOOGLE_MAP_API_KEY&quot;, properties[&quot;GOOGLE_MAP_API_KEY&quot;]
            buildConfigField &quot;String&quot;, &quot;GOOGLE_MAP_API_KEY&quot;, properties[&quot;GOOGLE_MAP_API_KEY&quot;]
        }

        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile(&#39;proguard-android.txt&#39;), &#39;proguard-rules.pro&#39;

            resValue &quot;string&quot;, &quot;GOOGLE_MAP_API_KEY&quot;, properties[&quot;GOOGLE_MAP_API_KEY&quot;]
            buildConfigField &quot;String&quot;, &quot;GOOGLE_MAP_API_KEY&quot;, properties[&quot;GOOGLE_MAP_API_KEY&quot;]
        }
    }
}</code></pre>
<p>debug, release 모드에 따라 각각 설정할 수 있다.</p>
<h3 id="buildconfigfield">buildConfigField</h3>
<ul>
<li><p>선언</p>
<pre><code class="language-java">buildConfigField &quot;타입&quot;, &quot;만들 변수명&quot;, properties[&quot;local.properties에 설정한 변수명&quot;]</code></pre>
</li>
<li><p>호출 방법</p>
<pre><code class="language-kotlin">val key = BuildConfig.GOOGLE_MAP_API_KEY</code></pre>
</li>
</ul>
<p>buildConfigField로 정의한 key는 <strong>BuildConfig</strong> 에 저장되어 프로젝트 내에서 언제든 사용할 수 있다.</p>
<h3 id="resvalue">resValue</h3>
<ul>
<li>선언</li>
</ul>
<pre><code class="language-java">resValue (&quot;타입&quot;, &quot;만들 변수명&quot;, properties[&quot;local.properties에 설정한 변수명&quot;])</code></pre>
<ul>
<li>호출 방법</li>
</ul>
<pre><code class="language-android">&lt;meta-data
    android:name=&quot;...&quot;
    android:value=&quot;@string/GOOGLE_MAP_API_KEY&quot; /&gt;</code></pre>
<p>resValue로 정의한 key는 <strong>xml</strong> 에서 사용할 수 있다.</p>
<p>❗️ 여기서 주의할 점은 타입을 선언할 때 소문자로 해주어야한다는 것 !
<em>예를 들어, string을 String으로 선언하면 resources가 item으로 선언되어버린다.</em></p>
<h2 id="✅-gradle-sync-now-를-해준-후-확인-해야-할-것">✅ gradle &#39;Sync Now&#39; 를 해준 후, 확인 해야 할 것</h2>
<h3 id="buildconfigfield-로-선언한-경우">buildConfigField 로 선언한 경우</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/4c42ecca-7bd8-446e-8013-22b08000fc11/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/31284b4f-ab71-4a93-a6e1-c2bf6ae5b518/image.png" alt=""></p>
<p><code>BuildConfig.java</code> 파일에 해당 key가 잘 선언 됐는지,
프로젝트 코드 내에서 확.실.히 해당 파일이 import 되어 호출 된 것이 맞는지 확인해야 한다.</p>
<h3 id="resvalue-로-선언한-경우">resValue 로 선언한 경우</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/c301593d-056a-46ee-8ecf-1a131cd06efd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/3c025078-d789-4a2b-97d3-73fcdce159ab/image.png" alt=""></p>
<p>마찬가지로 <code>gradleResValues.xml</code> 파일에 해당 key가 잘 선언되어 있는지 확인한다.</p>
<h2 id="🚨-혹시-이런-문제를-겪고-있다면">🚨 혹시 이런 문제를 겪고 있다면?</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/bb9d3cb7-469a-46cc-b792-4ee616559dc3/image.gif" alt=""></p>
<ul>
<li><em>gradle에서 제대로 선언한 것 같은데 <code>BuildConfig.java</code> 파일이 없어요 😭</em></li>
<li><em><code>BuildConfig.java</code> 파일만 있고 생성된 key가 없어요 🙃</em></li>
</ul>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/e3e28427-2daa-4d98-aeb7-9a8f170f4c75/image.png" alt=""></p>
<p><strong>[Build] - [Rebuild Project]</strong> 를 실행 한 뒤 <em>(혹은 [Build] - [Clean Project] 후 재실행)</em>,
다시 해당 파일들을 확인해보자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[세상 쉬운 안드로이드 무선(Wifi) 디버깅]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%84%B8%EC%83%81-%EC%89%AC%EC%9A%B4-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%AC%B4%EC%84%A0Wifi-%EB%94%94%EB%B2%84%EA%B9%85</link>
            <guid>https://velog.io/@dear_jjwim/%EC%84%B8%EC%83%81-%EC%89%AC%EC%9A%B4-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%AC%B4%EC%84%A0Wifi-%EB%94%94%EB%B2%84%EA%B9%85</guid>
            <pubDate>Tue, 06 Sep 2022 15:16:36 GMT</pubDate>
            <description><![CDATA[<p>때는 약 2년 전, 안드로이드 무선 디버깅 방법을 처음 접했다.</p>
<p><em>터미널을 열어서 <code>adb</code> (Android 디버그 브리지) 를 사용해 포트를 열고 연결하고, 연결이 끊어지면 연결 기록 전체 삭제하고, 다시 또 포트를 열고 연결하고...</em></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/f4ec7983-dfda-44f5-b1d0-0bf18171b4e4/image.png" alt=""></p>
<p>반복되는 재연결 과정에 지쳐 그냥 케이블을 연결해 쓰곤 했다 ^_ㅠ,, 이후에는 데스크탑에 유선랜을 사용하다가 현재는 mac에 와이파이를 연결해 쓰게 되면서 다시 무선으로 연결해볼까해서 찾아보니</p>
<p><strong>안드로이드 스튜디오가 업데이트 되면서 Android 11 이상의 기기에서는 QR코드로 세상 쉽게 무선 디버깅이 가능하다는 것을 알게 되었다❗️</strong></p>
<blockquote>
<h3 id="🙋🏻♀️-준비할-환경">🙋🏻‍♀️ 준비할 환경</h3>
</blockquote>
<ul>
<li>Android Studio Bumblebee 버전 이상
<em>필자는 Android studio chipmunk에서 테스트 했습니다 :)</em></li>
<li>Android 11 이상의 기기</li>
</ul>
<h3 id="1-연결할-기기의-개발자-옵션에서-무선-디버깅을-체크해준다">1. 연결할 기기의 개발자 옵션에서 무선 디버깅을 체크해준다.</h3>
<p>[Android 설정] - [디바이스 정보] - [빌드번호] 를 연타해 [개발자 옵션] 메뉴를 열고,
[개발자 옵션] - [무선 디버깅] ON 을 해주고 클릭해 들어가면 페어링된 기기 정보를 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/ba98a08e-ed88-4eba-8c9d-d4992cb91f07/image.png" alt=""></p>
<h3 id="2-pc와-디버깅할-폰의-네트워크를-동일한-wifi로-연결한다">2. PC와 디버깅할 폰의 네트워크를 동일한 Wifi로 연결한다.</h3>
<h3 id="3-연결-기기가-표시되는-부분을-클릭해-pair-devices-using-wi-fi-를-선택한다">3. 연결 기기가 표시되는 부분을 클릭해 [Pair Devices Using Wi-Fi] 를 선택한다.</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/4bee1487-540f-4b70-b060-c0a47100910a/image.png" alt=""></p>
<h3 id="4-qr-코드가-뜨면-디버깅할-폰의-무선디버깅---qr-코드로-기기-페어링-메뉴를-클릭해-스캔해준다">4. QR 코드가 뜨면 디버깅할 폰의 [무선디버깅] - [QR 코드로 기기 페어링] 메뉴를 클릭해 스캔해준다.</h3>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/e9eac763-3cce-4ef2-8487-6017e908364f/image.png" alt=""></p>
<p align="center"><img width="300" src="https://velog.velcdn.com/images/dear_jjwim/post/e53b0093-28a6-47ab-ac9e-facef52fb880/image.png"></p>

<p>이 과정까지 마치면 연결 끝❗️ </p>
<hr>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/1a53f05d-b53c-4117-9511-f261ea4999d9/image.png" alt=""></p>
<p>얼른 이 편리함을 누리세요 (´;ω;｀) <del>~</del> !!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Bitmap 최적화(Resize)한 다중 이미지 서버에 업로드하기 2 - 서버로 보내기 (코루틴(Coroutine), Retrofit2)]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Bitmap-%EC%B5%9C%EC%A0%81%ED%99%94Resize%ED%95%9C-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%9C%EB%B2%84%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-2-%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4Coroutine-Retrofit2</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Bitmap-%EC%B5%9C%EC%A0%81%ED%99%94Resize%ED%95%9C-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%9C%EB%B2%84%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-2-%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4Coroutine-Retrofit2</guid>
            <pubDate>Wed, 31 Aug 2022 08:31:54 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀 
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 잘못 이해하고 작성한 부분에 대한 피드백을 주신다면 감사히 받겠습니다 🙇🏻‍♀️</p>
</blockquote>
<hr>
<p>이제 우리는 지난 시간 열심히 다이어트 시켜준 비트맵을 서버로 떠나 보낼 일만 남았다. 이 과정은 무던히 잘 넘길 줄 알았으나.. <strong>비트맵을 압축하는 과정에서 파일 용량에 따라 속도가 다르다보니 기존(사용자가 선택한 순서)과 다르게 순서가 꼬이는 사태가 발생했다.</strong></p>
<p>순서를 지키기 위해 사진 한 장이 압축될 때까지 기다렸다가 다음 사진을 압축하자니, 운 나쁘게 대용량 사진 10장을 업로그 해야 한다면?! 10초 이상의 시간이 지체될 것이고 사용자는 이를 무한로딩으로 인식하고 앱을 종료해버리고 말 것이다 🥲
<br></p>
<h1 id="🎈-서버로-보낼-여러-개의-이미지file-담기">🎈 서버로 보낼 여러 개의 이미지(file) 담기</h1>
<p><strong>위의 문제를 해결하기 위해 HashMap에 저장한 후, 사용자가 선택한 사진 순서에 맞게 arrayList에 다시 저장하여 반환하는 방법을 생각했다.</strong></p>
<p>❓ List가 아닌 HashMap에 저장하는 이유는 위에 언급한 것처럼 압축이 완료되는 순서가 달라서 IndexOutOfBoundsException이 발생 할 수 있기 때문이다.</p>
<blockquote>
<ol>
<li>HashMap을 이용해 <code>index</code>와 <code>path</code>를 저장한다.
<code>Key</code> ➡️ <code>index</code> (사용자가 선택한 사진의 순서)
<code>Value</code> ➡️ <code>최적화된 bitmap의 임시 저장 경로</code> (지난 시간에  FileUtil에서 return 받은 값)<br></li>
<li>이 때, 코루틴 빌더의 <code>join()</code> 메소드를 통해 압축 작업이 전부 완료될 때까지 기다려준다.<br></li>
<li>arrayList에 HashMap의 Value 값을 <code>FormData</code> 에 담아 순서대로 저장하여 반환한다.</li>
</ol>
</blockquote>
<pre><code class="language-kotlin">object FileUtil {

    ...

    suspend fun bitmapResize(
        context: Context,
        uriList: ArrayList&lt;Uri&gt;
    ): MutableList&lt;MultipartBody.Part&gt;? {

        val pathHashMap = hashMapOf&lt;Int, String?&gt;()

        CoroutineScope(Dispatchers.IO).launch {
            uriList.forEachIndexed { index, uri -&gt;
                launch {
                    // 지난 시간에  FileUtil에서 return 받은 값
                    val path = optimizeBitmap(context, uri)
                    pathHashMap[index] = path
                }
            }
        }.join() // 작업이 끝날 때까지 기다린다.

        val fileList = arrayListOf&lt;MultipartBody.Part&gt;()

        pathHashMap.forEach {
            if (it.value.isNullOrEmpty()) {
                return null
            }

            val filePart = addImageFileToRequestBody(it.value!!, &quot;files&quot;)
            fileList.add(filePart)
        }

        return fileList
    }

    // 이미지 &#39;FormData&#39; 에 담기
    fun addImageFileToRequestBody(path: String, name: String): MultipartBody.Part {
        val imageFile = File(path)

        // MIME 타입을 따르기 위해 image/jpeg로 변환하여 RequestBody 객체 생성
        val fileRequestBody = imageFile.asRequestBody(&quot;image/jpeg&quot;.toMediaTypeOrNull())
        // RequestBody로 Multipart.Part 객체 생성
        return MultipartBody.Part.createFormData(name, imageFile.name, fileRequestBody)
    }

    ...

}
</code></pre>
<p>Retrofit2을 통해 서버로 파일을 전송할 때는 Multipart를 이용해야한다.</p>
<blockquote>
<h3 id="multipart란">Multipart란?</h3>
<p>HTTP를 통해 <code>File</code>을 Server로 전송하기 위해 사용되는 <code>Content-type</code> 이다.<br>
HTTP 프로토콜은 크게 <code>Header</code> 와 <code>Body</code> 로 구분 되며 <code>Body</code> 에 데이터를 담아 전송한다.
이 때, <code>Body</code> 에 들어가는 데이터 타입을 명시해주는 것이 <code>Content-type</code> 이다.<br>
<code>Content-type</code> 필드에 MIME(Multipurpose Internet Mail Extensions) 타입을 지정해줄 수 있다. <code>Multipart(=multipart/form-data)</code>는 MIME 타입 중 하나이다.</p>
</blockquote>
<p>addImageFileToRequestBody() 메소드에서 <code>createFormData()</code>를 통해 <code>form-data</code> 에 file을 담아 객체를 생성하여 반환해준다.</p>
<pre><code class="language-kotlin">createFormData(name: String, filename: String?, body: RequestBody)</code></pre>
<ul>
<li>name : 서버와 약속한 key 값</li>
<li>filename : 파일 이름</li>
<li>body : RequestBody 객체</li>
</ul>
<br>

<h1 id="🎈-retrofit2로-파일-업로드">🎈 Retrofit2로 파일 업로드</h1>
<pre><code class="language-kotlin">object RetrofitClient {

    private val gson = GsonBuilder().setLenient().create()

    private val clientBuilder = OkHttpClient.Builder()
    private val loggingInterceptor = HttpLoggingInterceptor()

    init {
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY;
        clientBuilder.addInterceptor(loggingInterceptor)
    }

    val retrofitClient: Retrofit = Retrofit.Builder()
        .baseUrl(API.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(clientBuilder.build())
        .build()

    val fileApiService: FileService = retrofitClient.create(FileService::class.java)

}</code></pre>
<pre><code class="language-kotlin">interface FileService {

    // 다중 파일 업로드
    @Multipart
    @POST(API.FILE_UPLOAD)
    fun uploadFileList(
        @Part files: List&lt;MultipartBody.Part&gt;
    ): Response&lt;Long&gt;

}</code></pre>
<p><strong>파일 업로드 관련 Retrofit Service를 작성할 때는 반!드!시! <code>@Multipart</code> 어노테이션을 작성해야하는 것을 잊지말자 !!</strong></p>
<p>(필자는 원래 RetrofitClient를 작성할 때, <a href="https://insert-koin.io/docs/quickstart/android">Koin</a>의 <code>single{}</code> 을 사용해 싱글톤 객체를 생성하여 의존성 주입(DI)을 하지만 포스팅하기 위해 수정하여 올린다.)</p>
<pre><code class="language-kotlin">private suspend fun uploadPhoto() = withContext(Dispatchers.IO) {

    val fileList = FileUtil.bitmapResize(context!!, uriList)

    if (fileList.isNullOrEmpty()) {
        // bitmapResize 실패시
        return
    }

    val response = fileApiService.uploadFileList(fileList)

    if (response.isSuccessful) {
        // 파일 업로드 성공
    } else {
        // 파일 업로드 실패
    }
}</code></pre>
<p>최종적으로 필요한 곳에 uploadPhoto() 메소드를 호출해주면 끝이다 !</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/1a546a4d-298d-4dc2-b5dd-397742b51bdc/image.jpeg" alt=""></p>
<p>실제로 필자는 이번 과정을 통해 이미지 업로드만 10초 ~ 15초 정도 걸리던 과정을 약 2초 대로 줄일 수 있었다 🥹 _ (원본 이미지 크기와 서버와 통신 과정에 따라 최종적인 업로드 시간에 개인차가 있을 수 있겠지만 👉🏻👈🏻 )_</p>
<p>이전에 작성한 Bitmap Resize 부분은 몇 달 전에 작성한 코드라 미숙한 부분이 많지만 시간이 된다면 보다 깔끔한 코드로 업데이트 해보도록 노력해야겠다 !</p>
<p>오늘도 읽어주셔서 감사합니다 🙇🏻‍♀️ !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC, MVP, MVVM 패턴 (+ DI)]]></title>
            <link>https://velog.io/@dear_jjwim/MVC-MVP-MVVM-%ED%8C%A8%ED%84%B4-DI</link>
            <guid>https://velog.io/@dear_jjwim/MVC-MVP-MVVM-%ED%8C%A8%ED%84%B4-DI</guid>
            <pubDate>Wed, 24 Aug 2022 12:06:38 GMT</pubDate>
            <description><![CDATA[<h1 id="디자인-패턴이란">디자인 패턴이란?</h1>
<p>과거의 소프트웨어 개발 과정에서 발견된 설계의 노하우를 축적하여 그 방법에 이름을 붙여서 이후에 재사용하기 좋은 형태로 특정 규약을 만들어서 정리한 것.
<br></p>
<h1 id="didependency-injection">DI(Dependency Injection)</h1>
<ul>
<li>컴포넌트 간의 의존 관계를 소스코드 내부가 아닌 외부 설정 파일 등을 통해 정의하게 되는 디자인 패턴 중 하나</li>
<li>객체를 직접 생성하지 않고, <strong>외부에서 주입한 객체를 사용하는 방식</strong></li>
<li>인스턴스 간 디커플링을 만들어준다 ➡️ 유닛테스트 용이성 증대</li>
</ul>
<p>패턴에 대해 공부하다보면 제일 많이 만나는 말이 &#39;의존&#39;인데 처음에 이 말이 왜이리 어렵게 느껴지던지,, 그래서 나는 이렇게 해석하기로 했다.</p>
<blockquote>
<p>Model은 Controller와 View에 <strong>의존</strong>하지 않아야한다.
 <strong>= Model 내부에 Controller와 View에 관련된 코드가 있으면 안된다.</strong></p>
</blockquote>
<br>

<h1 id="mvc-패턴-modelviewcontroller">MVC 패턴 (Model+View+Controller)</h1>
<blockquote>
<p>역할에 따라 구분해서 코드를 나눠보자.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/f8a35969-2d7a-40c8-91c0-3f9edfc19195/image.jpeg" alt=""></p>
<p><strong>Activity나 Fragment가 View와 Model을 함께 관리하는 형태</strong></p>
<p>함께 혼용되다 보니 코드가 지저분해지고 비즈니스 로직과 View의 관점적인 부분에서 분리가 안됨
➡️ 버그를 만들기 쉬운 코드</p>
<h3 id="mvc-패턴을-지키기-위한-규칙들">MVC 패턴을 지키기 위한 규칙들</h3>
<ol>
<li><p>Model은 Controller와 View에 의존하지 않아야 한다.</p>
</li>
<li><p>View는 Model에만 의존해야 하고 Controller에는 의존하면 안 된다.</p>
</li>
<li><p>View가 Model로부터 데이터를 받을 때는 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야 한다.</p>
</li>
</ol>
<p><strong>View = UI(레이아웃) + Model로부터 받은 데이터</strong></p>
<ol start="4">
<li>Controller는 Model과 View에 의존해도 된다.</li>
</ol>
<p><strong>Controller는 Model과 View의 중개자 역할이자 전체 로직을 구성하기 때문에</strong></p>
<ol start="5">
<li>View가 Model로부터 데이터를 받을 때는 반드시 Controller에서 받아야 한다.</li>
</ol>
<br>

<h1 id="mvp-패턴-model--view--presenter">MVP 패턴 (Model + View + Presenter)</h1>
<blockquote>
<p>화면과 로직을 분리하자. (유닛테스트 가능)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/203f1bb1-aabf-4209-ab85-aed5eae71ca1/image.jpeg" alt=""></p>
<ul>
<li><p><strong>View : 사용자에게 보여지는 UI</strong>
인터페이스, Activity나 Fragment ViewController의 뷰 콜백을 받도록 구성한다.</p>
</li>
<li><p><strong>Model : 앱에서 사용할 데이터에 관련된 행위와 데이터를 다룬다.</strong></p>
</li>
<li><p><strong>Presenter : View에서 요청한 정보로 Model을 가공하여 View에 전달한다.</strong>
비즈니스 로직과 View를 제어하고, Model과 View를 함께 관리한다.
인터페이스를 통해 뷰에 넣어줄 데이터를 단순하게 추상화한다.</p>
</li>
</ul>
<p><strong>이 또한 View와 Presenter의 의존성이 높은건 어쩔 수 없다.</strong>
<br></p>
<h1 id="mvvm-패턴-model--view--view-model">MVVM 패턴 (Model + View + View Model)</h1>
<blockquote>
<p>화면에 알아서 그려. 나는 데이터만 바꿀게!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/8461db4c-2ad0-4c17-8808-d078d381f12a/image.jpeg" alt=""></p>
<h3 id="viewmodel">ViewModel</h3>
<p>View를 표현하기 위해 만들어진! View를 위한 Model!
<strong>UI와 관계 있는 데이터를 저장한다.</strong></p>
<p>필요한 데이터를 관리하여 바인딩해주고, 비즈니스 로직을 담당해 데이터를 처리하는 요소.</p>
<p>View는 ViewModel을 지켜보고 있다가 (=구독) 데이터에 변화가 생기면 반영한다.
<strong>➡️ 옵저버 패턴을 통한 콜백으로 뷰 제어한다.</strong></p>
<p>ViewController 보일러 플레이트 코드(별 수정 없이 반복적으로 사용되는 코드)를 쉽게 제거할 수 있고,
<strong>모듈화하여 개발이 가능하기 때문에 의존적이지 않고 독립적이다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 registerForActivityResult() (+권한 (Permission) 요청 및 얻어오기)]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-registerForActivityResult</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-registerForActivityResult</guid>
            <pubDate>Tue, 23 Aug 2022 09:17:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/1a5d3a91-e811-4ac1-920a-64283e410334/image.png" alt="">
activity 에서 데이터를 주고 받을 수 있던 <code>startActivityForResult()</code>가 현재 Deprecated 되어 수정이 불가피해졌다. 더이상 미루지 말고 이를 대체할 수 있는 <code>registerForActivityResult()</code>를 사용해보자 !</p>
<blockquote>
<p>dog 라는 String 값을 GiveActivity에서 TakeActivity로 보낸다고 가정해보자.</p>
</blockquote>
<h2 id="📮-giveactivity--데이터를-담아-보낸다">📮 GiveActivity : 데이터를 담아 보낸다.</h2>
<pre><code class="language-kotlin">// DOG_KEY 라는 key 상수를 선언했다고 가정
setResult(Activity.RESULT_OK, Intent().apply {
    putExtra(DOG_KEY, &quot;dog&quot;)
})

finish()</code></pre>
<p>데이터를 보내는 activity는 익숙할 것이다! <code>putExtra()</code> 메소드에 데이터를 담는다.</p>
<h2 id="💌-takeactivity--데이터를-받는다">💌 TakeActivity : 데이터를 받는다.</h2>
<pre><code class="language-kotlin">private val resultLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -&gt;
        if (result.resultCode == Activity.RESULT_OK) {
            result.data?.getStringExtra(DOG_KEY)?.let { text -&gt;
                Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
            }
        }
    }

override fun onCreate() {
    super.onCreate()
    val intent = Intent(this, GiveActivity::class.java)
    resultLauncher.launch(intent)
}
</code></pre>
<p>먼저 <code>registerForActivityResult()</code> 메소드를 통해 데이터를 받기 위한 런처, <strong>ActivityResultLauncher</strong> 를 정의한다. 이 때, <code>registerForActivityResult()</code>에는 <code>ActivityResultContract</code>와 <code>ActivityResultCallback</code> 을 파라미터로 넘겨주어야 한다.</p>
<h3 id="activityresultcontract">ActivityResultContract</h3>
<p><strong>우리가 결과를 호출하는 데에 필요한 입력 유형과 결과의 출력 유형을 정의한다.</strong></p>
<p>예를 들어, 위의 코드에서는 다른 activity를 통해 값을 넘겨 받는 것이기 때문에 ActivityResultContracts.StartActivityForResult() 를 넘겨주어야한다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/29f2ca75-b1d7-42f0-97bc-f3f64548f4a3/image.png" alt=""></p>
<p>만약 여러 가지 퍼미션 권한을 얻어 와야 하는 경우에는 ActivityResultContracts.RequestMultiplePermissions()을 선택하는 등, <strong>상황에 따라 굉장히 많은 유형이 존재하므로 알맞은 것을 선택해 넘겨주면 된다.</strong></p>
<h3 id="activityresultcallback">ActivityResultCallback</h3>
<p>해당 콜백에서는 ActivityResultContract로 정의한 출력 유형의 객체를 가져와 resultCode를 검증한 뒤, <strong>result.data 를 통해 원하는 데이터를 받아온다.</strong></p>
<p><strong>이렇게 ActivityResultLauncher 정의한 후, launch로 GiveActivity를 실행하면 데이터 주고 받기 끝!</strong></p>
<h2 id="권한-permission-요청-및-얻어오기">권한 (Permission) 요청 및 얻어오기</h2>
<p>작성하는 김에 위에 언급한 권한 얻어오기까지 해보자 🙌🏻</p>
<blockquote>
<p>카메라 권한을 받아온다고 가정해보자.</p>
</blockquote>
<pre><code class="language-kotlin">private val cameraPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -&gt;
        if (isGranted) {
            startCamera(binding.viewFinder)
        } else {
            Toast.makeText(this, &quot;권한을 받아오지 못했습니다.&quot;, Toast.LENGTH_SHORT).show()
            finish()
        }
    }

override fun onCreate() {
    super.onCreate()
    initViews()
}

private fun initViews() = with(binding) {
    if (ContextCompat.checkSelfPermission(
            this@CameraActivity,
            Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
    ) {
        startCamera(viewFinder)
    } else {
        cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
    }
}</code></pre>
<p><strong><code>ContextCompat.checkSelfPermission()</code> 을 통해 권한이 있는지 체크하고 런처를 실행해주면 끝!</strong></p>
<p>여러 개의 퍼미션을 한번에 받아와야 한다면 ActivityResultContracts.RequestMultiplePermissions()을 넘겨주고 arrayList에 받아오고 싶은 권한들을 담아 체크하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/e4462c00-4e99-4c9e-801f-19000147baee/image.png" alt=""></p>
<p>이제 더이상 구구절절 <code>PERMISSION_REQUEST_CODE</code> 와 같은 상수들을 따로 설정해주지 않아도 돼서 매우 편하다,,⭐️</p>
<h3 id="마지막으로-주의할-점-❗️">마지막으로 주의할 점 ❗️</h3>
<p>ActivityResultLauncher 콜백은 activity가 재생성될 때 무조건 다시 등록되어야 하기 때문에 onCreate()나 onStart()에서 선언해 주어야 한다. (launch는 상관없다!)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Github Push failed 오류 해결 - Invocation failed Unexpected end of file from server]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Github-Push-failed-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Invocation-failed-Unexpected-end-of-file-from-server</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Github-Push-failed-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Invocation-failed-Unexpected-end-of-file-from-server</guid>
            <pubDate>Wed, 20 Jul 2022 18:24:09 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-문제">🚨 문제</h1>
<p>어제 잘 되던 push가 오늘 갑자기 안된다..?
대체 무슨 이유로 서버로부터 예기치 못한 문제가 생겼다는걸까..ᕙ( ︡’︡益’︠)ง</p>
<pre><code>Push failed
Invocation failed Unexpected end of file from server
java.lang.RuntimeException: Invocation failed Unexpected end of file from server
...</code></pre><br>
<br>

<h1 id="📝-해결">📝 해결</h1>
<p>아래 사항들을 하나씩 체크해보자.</p>
<h2 id="1-github-토큰이-만료-되지-않았나요">1. Github 토큰이 만료 되지 않았나요?</h2>
<p>Github 토큰 만료일이 지났을 경우 당연히 push가 안될 수 있다. 
<strong>[Settings]-[Developer settings]-[Personal access tokens]</strong> 에서 새로운 토큰을 발급해주자.</p>
<h2 id="2-github-토큰-설정이-잘못-되지-않았나요-select-scopes">2. Github 토큰 설정이 잘못 되지 않았나요? (Select scopes)</h2>
<p>토큰을 갱신했다하더라도 <strong>Select scopes</strong> 설정에서 <strong>repo, admin:org, gist, user</strong> 중 하나라도 누락되면 Git 연동을 실패할 수 있다. 꼭 아래와 같이 체크해주자.
<img src="https://velog.velcdn.com/images/dear_jjwim/post/b40d8edf-5a35-4492-8955-5b071ba72bd8/image.png" alt=""></p>
<h2 id="3-use-credential-helper-가-체크-되었나요">3. Use credential helper 가 체크 되었나요?</h2>
<p><strong>[File]-[Settings]-[Version Control]-[Git]</strong> 에서 <strong>✅ Use credential helper</strong> 를 체크한 후, Apply 해주자.
<img src="https://velog.velcdn.com/images/dear_jjwim/post/2d819899-cedf-4be2-93a5-1082f994be29/image.png" alt=""></p>
<h2 id="4-git이-최신-버전인가요">4. Git이 최신 버전인가요?</h2>
<p>아래 명령어로 Git을 최신 버전으로 업데이트 해주자.</p>
<h3 id="git-버전-확인하기">Git 버전 확인하기</h3>
<blockquote>
<p>git --version</p>
</blockquote>
<h3 id="git-업데이트">Git 업데이트</h3>
<blockquote>
<h3 id="window">Window</h3>
<p>2.14.2 ~ 2.16.1 버전 ➡️ git update
2.16.1 이후 버전 ➡️ git update-git-for-windows</p>
</blockquote>
<h3 id="mac">Mac</h3>
<p>brew upgrade git</p>
<h2 id="5-캐시를-지워봤나요">5. 캐시를 지워봤나요?</h2>
<p>위의 방법을 다 수행하고 재실행 해도 안된다? 그렇다면 최후의 수단 캐시를 비워보자. 이 방법은 현재 문제뿐만 아니라 다른 오류들의 해결법이 되는 경우도 많다.</p>
<p>&#39;다 실행했는데 왜 적용이 안되는 것 같지?&#39; 라는 생각이 들 때는 캐시를 한번 비워봅시다!</p>
<p>**[File]-[Invalidate Caches...] **</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/b6329e4a-deb9-4d77-b69b-642f4462bdce/image.png" alt=""></p>
<p>필자는 위의 방법들을 차례로 수행한 후, 캐시를 비우니 마법처럼 해결되었다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Room 관련 오류 해결 - Execution failed for task ':app:kaptDebugKotlin'.]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Room-%EA%B4%80%EB%A0%A8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Execution-failed-for-task-appkaptDebugKotlin</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Room-%EA%B4%80%EB%A0%A8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-Execution-failed-for-task-appkaptDebugKotlin</guid>
            <pubDate>Sat, 16 Jul 2022 09:27:40 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-문제">🚨 문제</h1>
<p>Room을 사용하다가 만난 문제, Build 자체가 안된다.. 답답쓰.. 그동안 Room을 사용하면서 만나본 적 없던 문제라 매우 당황스러웠다. 처음에는 Room 관련 오류인 줄도 몰랐으나 현재 프로젝트에서 kapt를 사용해 라이브러리를 사용하는건 Room 밖에 없었기 때문에 추측할 수 있었다.</p>
<pre><code>&gt; Task :app:kaptDebugKotlin FAILED
Execution failed for task &#39;:app:kaptDebugKotlin&#39;.
&gt; A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
   &gt; java.lang.reflect.InvocationTargetException (no error message)
</code></pre><br>
<br>

<h1 id="📝-해결">📝 해결</h1>
<p>구글링을 하다보면 수많은 해결법이 나오지만 상황에 따라 다르기 때문에 시도했던 방법들을 소개하려한다.</p>
<p>먼저, 위에서 &#39;kapt&#39;를 언급했다는 이유로 <strong>&#39;kapt&#39;를 &#39;annotationProcessor&#39;로 바꾸면 되지 않나요?</strong> 라고 생각할 수도 있다. 실제로 이런 해결법을 제시하는 글도 많다. 하지만 <strong>Kotlin은 kotlinc로 컴파일되기 때문에 기존 Java로 작성된 Annotation Process로는 Kotlin의 Annotation이 제대로 처리되지 않는다.</strong> annotationProcessor를 써봤자 Build는 성공할지 몰라도 아래와 같은 오류를 만나게 될 것이다.</p>
<pre><code>java.lang.RuntimeException: cannot find implementation for hbs.com.timetablescreen.Utils.AppDataBase. AppDataBase_Impl does not exist</code></pre><p><strong>❗️ kapt로 선언해주는 것은 필수 ❗️</strong></p>
<pre><code class="language-kotlin">dependencies {
    // annotationProcessor -&gt; kapt
    kapt &quot;androidx.room:room-compiler:$roomVersion&quot;
}</code></pre>
<h2 id="맥북-m1-오류-해결">맥북 M1 오류 해결</h2>
<p>모듈 수준의 build.gradle(Module:프로젝트명.app)의 dependencies에 아래 코드를 추가한다.</p>
<pre><code class="language-kotlin">dependencies {
    kapt &quot;org.xerial:sqlite-jdbc:3.34.0&quot;
}</code></pre>
<p>맥북 M1을 사용하는 사람들에게 제일 많이 제시되는 해결책이다. 나 또한, 맥북 M1을 사용하고 있어 제일 먼저 시도한 방법이지만 해결하지 못했다..^_ㅠ</p>
<p>현재 Room 4.2.0(현 시점 안정화 된 최신버전)을 사용중인데, <strong>Room 4.1.0 부터 M1 칩을 지원하도록 버그가 수정되었기 때문에</strong> 4.1.0 이상의 버전을 사용하는 사람들에게 위의 방법은 소용이 없다. (4.1.0 이하를 사용하는 사람들은 해결했다는 글이 많다!)</p>
<h2 id="kotlin-버전-낮추기">Kotlin 버전 낮추기</h2>
<p>프로젝트 수준의 build.gradle(Project:프로젝트명)의 plugins에서 kotlin 버전을 &#39;1.5.31&#39;으로 설정한다.</p>
<pre><code class="language-kotlin">plugins {
    id &#39;org.jetbrains.kotlin.android&#39; version &#39;1.5.31&#39; apply false
}</code></pre>
<p>다른 방법도 많이 시도해봤지만 필자는 버전을 낮추니 마법처럼 해결 되었다...⭐️</p>
<p>Kotlin 버전을 딱히 이유 없이 무조건 최신으로 맞췄던 나.. 이번 오류는 이러한 생각을 반성하는 계기가 되었던 오류였다. 라이브러리든 Kotlin 버전이든 서로 호환이 되는 버전을 확인하여 적용시키는게 중요하다는 것을 깨닫게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 UnitTest에서 LiveData 사용시 오류 - Method getMainLooper in android.os.Looper not mocked.]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-UnitTest%EC%97%90%EC%84%9C-LiveData-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%98%A4%EB%A5%98-Method-getMainLooper-in-android.os.Looper-not-mocked</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-UnitTest%EC%97%90%EC%84%9C-LiveData-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%98%A4%EB%A5%98-Method-getMainLooper-in-android.os.Looper-not-mocked</guid>
            <pubDate>Mon, 27 Jun 2022 10:01:20 GMT</pubDate>
            <description><![CDATA[<h1 id="🚨-문제">🚨 문제</h1>
<p>안드로이드 Test 학습 시작과 동시에 만난 오류..</p>
<pre><code>java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
    at android.os.Looper.getMainLooper(Looper.java)
    at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
    at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
    at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:486)
    at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)</code></pre><pre><code class="language-kotlin">internal abstract class ViewModelTest : KoinTest {

    ...

    protected fun &lt;T&gt; LiveData&lt;T&gt;.test(): LiveDataTestObserver&lt;T&gt; {
        val testObserver = LiveDataTestObserver&lt;T&gt;()
        observeForever(testObserver) // 🚨 최초 문제 발생 지점
        return testObserver
    }

}</code></pre>
<p><strong>LiveData를 사용하는 ViewModel 테스트를 진행할 때 생기는 문제이다.</strong>
(같은 상황이더라도 경우에 따라 NullPointerException이 뜰 때도 있다.)</p>
<p>최초 문제 발생 지점인 <code>observeForever()</code> 메소드를 살펴보자.</p>
<pre><code class="language-kotlin">@MainThread
    public void observeForever(@NonNull Observer&lt;? super T&gt; observer) {
        assertMainThread(&quot;observeForever&quot;);
        AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing instanceof LiveData.LifecycleBoundObserver) {
            throw new IllegalArgumentException(&quot;Cannot add the same observer&quot;
                    + &quot; with different lifecycles&quot;);
        }
        if (existing != null) {
            return;
        }
        wrapper.activeStateChanged(true);
    }

...

static void assertMainThread(String methodName) {
        if (!ArchTaskExecutor.getInstance().isMainThread()) {
            throw new IllegalStateException(&quot;Cannot invoke &quot; + methodName + &quot; on a background&quot;
                    + &quot; thread&quot;);
        }
    }</code></pre>
<p>*<em>가장 먼저 실행되는 <code>assertMainThread()</code> 메소드 내부에서 <code>isMainThread()</code> 함수를 사용하여 현재 스레드가 메인 스레드인지를 확인하는 것을 볼 수 있다.
*</em></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/958aae66-ff22-426f-a613-f3591e8d4c9f/image.png" alt=""></p>
<p>안드로이드 java 아래 총 3가지 항목이 있는데,</p>
<ul>
<li><code>main</code> : 실제 앱을 구성하는 코드</li>
<li><code>androidTest</code> : 안드로이드 디바이스를 이용한 테스트 (UI테스트)</li>
<li><code>test</code> : 로컬 테스트 (오직 개발 장비의 JVM에서 돌아가게 되며, 디바이스가 필요 없는 테스트)</li>
</ul>
<p><strong>우리가 테스트중인 <code>test</code> 환경(로컬 테스트)에서는 real Android 환경이 아니기 때문에 MainThread(=UIThread)를 사용할 수 없어 오류가 발생하는 것이다.</strong> (Android MainThread는 android.os의 Looper를 사용)</p>
<br>
<br>

<h1 id="📝-해결">📝 해결</h1>
<p>위와 같은 문제를 해결해주기 위해 사용하는 클래스가 <code>InstantTaskExecutorRule</code> 이다.</p>
<blockquote>
<p>A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.
You can use this rule for your host side tests that use Architecture Components.<br>
아키텍처 구성 요소에서 사용하는 백그라운드 실행기를 각 작업을 동기적으로 실행하는 다른 실행기로 바꾸는 JUnit 테스트 규칙. 아키텍처 구성 요소를 사용하는 호스트 측 테스트에 이 규칙을 사용할 수 있습니다.</p>
</blockquote>
<p><strong>즉, <code>InstantTaskExecutorRule</code>를 사용하면 안드로이드 구성요소 관련 작업들을 모두 동일한 스레드에서 실행시키기 때문에 동기화로 인한 고민을 할 필요가 없어진다는 것이다.</strong></p>
<p>그렇더라도 MainThread를 사용하는건 아닌데 어떻게 이런 일이 가능할까 🤔 ?</p>
<pre><code class="language-kotlin">public class InstantTaskExecutorRule extends TestWatcher {
    @Override
    protected void starting(Description description) {
        super.starting(description);
        ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
            @Override
            public void executeOnDiskIO(Runnable runnable) {
                runnable.run();
            }

            @Override
            public void postToMainThread(Runnable runnable) {
                runnable.run();
            }

            @Override
            public boolean isMainThread() {
                return true;
            }
        });
    }

    ...
}</code></pre>
<p><strong>내부에 <code>isMainThread()</code>가 <code>true</code>로 하드코딩 되어있기 때문이지 ଘ(∩◉ω◉ )⊃----⭐️ !</strong></p>
<p>사용하는 방법은 아래와 같다.</p>
<ol>
<li>라이브러리 추가<pre><code class="language-kotlin">dependencies {
 testImplementation &#39;androidx.arch.core:core-testing:2.1.0&#39;
}</code></pre>
</li>
<li>Unit Test 코드 내에 InstantTaskExecutorRule Rule 추가<pre><code class="language-kotlin">@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()</code></pre>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/20ee5eb6-454d-4212-8488-ef458cfa95aa/image.png" alt=""></p>
<p>문제 해결 완료! 뿅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Bitmap 최적화(Resize)한 다중 이미지 서버에 업로드하기 1 - 비트맵 다이어트 시키기]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Bitmap-%EC%B5%9C%EC%A0%81%ED%99%94Resize%ED%95%9C-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%9C%EB%B2%84%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-1-%EB%B9%84%ED%8A%B8%EB%A7%B5-%EB%8B%A4%EC%9D%B4%EC%96%B4%ED%8A%B8-%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Bitmap-%EC%B5%9C%EC%A0%81%ED%99%94Resize%ED%95%9C-%EB%8B%A4%EC%A4%91-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%9C%EB%B2%84%EC%97%90-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-1-%EB%B9%84%ED%8A%B8%EB%A7%B5-%EB%8B%A4%EC%9D%B4%EC%96%B4%ED%8A%B8-%EC%8B%9C%ED%82%A4%EA%B8%B0</guid>
            <pubDate>Tue, 03 May 2022 12:16:11 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀 
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 제가 잘못 이해한 부분이 있다면 알려주시면 감사하겠습니다 💌</p>
</blockquote>
<hr>
<h1 id="비트맵-다이어트-시키기">비트맵 다이어트 시키기</h1>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/01222946-6fa3-427f-9186-24c3df3018a9/image.png" alt=""></p>
<p>사진 한 장 올릴 때는 원본을 올려도 문제가 없었지만(사실 이것 또한 정말 큰 사이즈의 사진을 만나면 언젠가는 터질 문제였던 것..) 백엔드 서버에 리뷰 사진을 다량으로 업로드하면서 <strong>413(Request Entity Too Large)</strong> 에러를 계속 만나게 되었다. 직접 촬영한 원본 이미지를 10장씩이나 보냈기 때문에 당연히 파일 크기 허용량을 초과할 수밖에 없었다. 사진을 업로드하는 데에 있어 Bitmap 최적화는 필수라는 것을 몸소 느꼈다 😖</p>
<p>파일 관련 주요 개념이 많아서 필요한 부분을 조각으로 공부하다보니 꼭 한번 정리해보고싶어서 작성해 보았다. 같은 문제를 겪고 있는 분들께 도움이 되기를..✨ 
<br>
<em>👉🏻 이 글은 <a href="https://developer.android.com/topic/performance/graphics/load-bitmap?hl=ko">안드로이드 도큐먼트</a> 기준으로 작성했으며, 코틀린을 사용했습니다 :)</em>
<br></p>
<h1 id="fileutil-클래스-생성">FileUtil 클래스 생성</h1>
<p><code>FileUtil</code> 클래스에서 아래 과정으로 최적화를 진행해 볼 것이다.</p>
<blockquote>
<p>임시 파일 생성 ➡ Bitmap 리사이징 ➡ Bitmap을 JPEG로 압축하여 저장 ➡ 최적화된 임시 파일의 저장 경로 리턴</p>
</blockquote>
<p>안드로이드에서 서버로 이미지를 보낼 때 Multipart를 사용하여 FormData를 담아 보내게 된다.</p>
<pre><code class="language-kotlin">MultipartBody.Part.createFormData(name: String, filename: String?, body: RequestBody)</code></pre>
<ul>
<li><code>name</code> : 서버에서 받는 키 값</li>
<li><code>filename</code> : 파일 이름</li>
<li><code>body</code> : 파일 경로(pathname)를 가지는 RequestBody 객체</li>
</ul>
<p><strong><code>FileUtil</code>은 위의 <code>body</code> 객체를 만들기 위해 파일 경로를 반환해주기 위한 클래스</strong>라고 보면 된다.
<br>
<br></p>
<h2 id="🎈-캐시-파일-생성-및-경로-반환">🎈 캐시 파일 생성 및 경로 반환</h2>
<pre><code class="language-kotlin">fun optimizeBitmap(context: Context, uri: Uri): String? {
    try {
        val storage = context.cacheDir // 임시 파일 경로
        val fileName = String.format(&quot;%s.%s&quot;, UUID.randomUUID(), &quot;jpg&quot;) // 임시 파일 이름

        val tempFile = File(storage, fileName)
        tempFile.createNewFile() // 임시 파일 생성

        // 지정된 이름을 가진 파일에 쓸 파일 출력 스트림을 만든다.
        val fos = FileOutputStream(tempFile) 

        decodeBitmapFromUri(uri)?.apply {
            compress(Bitmap.CompressFormat.JPEG, 100, fos)
            recycle()
        } ?: throw NullPointerException()

        fos.flush()
        fos.close()

        return tempFile.absolutePath // 임시파일 저장경로 리턴

    } catch (e:Exception) {
        Log.e(TAG, &quot;FileUtil - ${e.message}&quot;)
    }

    return null
}</code></pre>
<p>먼저 <strong>내부 저장소에 캐시 파일을 생성</strong>할 것이다. 여러 장의 이미지를 저장할 것이기 때문에 파일 이름 중복 방지를 위해 <code>UUID.randomUUID()</code> 를 통해 이름을 생성해준다.</p>
<blockquote>
<h3 id="내부-저장소">내부 저장소</h3>
<p>구동하는 어플리케이션에서만 접근이 가능한 저장소. 앱 삭제 시 내부 저장소 데이터도 모두 삭제됨.
👉🏻 <U>압축된 이미지를 굳이 우리 폰에 저장할 필요가 없기 때문에 내부 저장소를 이용하는 것!</U></p>
</blockquote>
<h3 id="외부-저장소">외부 저장소</h3>
<p>어떤 어플리케이션에서든 접근 가능한 저장 공간. 앱 삭제시에도 데이터 유지.</p>
<p>파일 관련 내용을 다루면서 <code>try-catch</code>문은 다들 필수인거 아시쥬 ผ(•̀_•́ผ) ? Exception 발생 시 <code>e.printStackTrace()</code>로 찍으면 호출한 부분부터 에러 발생 A-Z까지 보여주는 느낌으로 출력되는데 요런식으로 외부에 노출되면 큰일이기 때문에 현업에서는 사용하지 않는다고 한다.</p>
<br>

<h3 id="fileoutputstream">FileOutputStream</h3>
<p>데이터를 파일에 바이트 스트림으로 저장하기 위해 사용.</p>
<ul>
<li><p><strong>flush()</strong>
더 이상 출력 될 데이터가 없을 때, 마지막 부분에 호출하여 출력 스트림 내부에 작은 버퍼에 남아있는 데이터를 모두 출력 시키고 버퍼를 비운다.</p>
</li>
<li><p><strong>close()</strong>
<code>OutputStream</code>을 더 이상 사용하지 않을 때 호출하여 사용했던 시스템 자원을 풀어준다.</p>
</li>
</ul>
<blockquote>
<h4 id="👩💻-스트림이란">👩‍💻 스트림이란?</h4>
<p>음성, 영상, 데이터 등의 작은 조각들이 하나의 줄기를 이루며 전송되는 열.
데이터, 패킷, 비트 등의 일련의 연속성을 갖는 흐름을 의미한다.
물리 디스크 상의 파일, 장치를 통일된 방식으로 다루기 위한 가상적인 개념이다.</p>
</blockquote>
<br>

<h3 id="bitmap-compress">bitmap compress</h3>
<p>형식을 지정해서 Bitmap을 압축하는 메소드이다.</p>
<pre><code class="language-java">public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {...}</code></pre>
<ul>
<li><p><code>CompressFormat</code>
압축할 파일 타입. 보통 JPEG, PNG를 사용한다.
우리는 투명 값을 가진 이미지를 저장하는 것이 아니므로 압축 속도가 더 빠르고, 압축 시 파일 용량이 더 작은 JPEG 타입을 사용할 것이다.</p>
</li>
<li><p><code>quality</code>
압축 정도. 0~100의 숫자를 넣으면 (quality)%로 압축된다.</p>
</li>
<li><p><code>OutputStream</code>
Bitmap 이미지를 저장하기 위한 output stream 객체를 받는다.</p>
</li>
</ul>
<p>Bitmap은 OS에서 제대로 관리를 해주지 않기 때문에 메모리 누수가 나지 않기 위해서는 별도의 관리를 해주어야 한다. 따라서, 사용 후 <code>recycle()</code> 호출은 필수이다 !
<br>
<br></p>
<h2 id="🎈-bitmap-최적화-resize">🎈 Bitmap 최적화 (Resize)</h2>
<p>위에 구현한 <code>optimizeBitmap()</code> 메소드에서 bitmap compress를 하기 전에 최적화된 Bitmap을 반환하는 메소드이다.</p>
<pre><code class="language-kotlin">// 최적화 bitmap 반환
private fun decodeBitmapFromUri(uri: Uri, context: Context): Bitmap? {

    // 인자값으로 넘어온 입력 스트림을 나중에 사용하기 위해 저장하는 BufferedInputStream 사용
    val input = BufferedInputStream(context.contentResolver.openInputStream(uri))

    input.mark(input.available()) // 입력 스트림의 특정 위치를 기억

    var bitmap: Bitmap?

    BitmapFactory.Options().run {
        // inJustDecodeBounds를 true로 설정한 상태에서 디코딩한 다음 옵션을 전달
        inJustDecodeBounds = true
        bitmap = BitmapFactory.decodeStream(input, null, this)

        input.reset() // 입력 스트림의 마지막 mark 된 위치로 재설정

        // inSampleSize 값과 false로 설정한 inJustDecodeBounds를 사용하여 다시 디코딩
        inSampleSize = calculateInSampleSize(this)
        inJustDecodeBounds = false

        bitmap = BitmapFactory.decodeStream(input, null, this)?.apply {
            // 회전된 이미지 되돌리기에서 다시 언급할게용 :)
            rotateImageIfRequired(this, uri)
        }
    }

    input.close()

    return bitmap

}

// 리샘플링 값 계산 : 타겟 너비와 높이를 기준으로 2의 거듭제곱인 샘플 크기 값을 계산
private fun calculateInSampleSize(options: BitmapFactory.Options): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height &gt; MAX_HEIGHT || width &gt; MAX_WIDTH) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        while (halfHeight / inSampleSize &gt;= MAX_HEIGHT &amp;&amp; halfWidth / inSampleSize &gt;= MAX_WIDTH) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}
</code></pre>
<h3 id="content-provider-content-resolver">Content Provider, Content Resolver</h3>
<blockquote>
<ul>
<li><strong>Content Provider</strong>
앱과 앱 저장소 사이에서 데이터 접근을 쉽게 하도록 관리해주는 클래스<br>
</li>
</ul>
</blockquote>
<ul>
<li><strong>Content Resolver</strong>
Content Provider에 접근해 결과를 반환하는 브릿지 역할</li>
</ul>
<p>Content Resolver를 통해 <code>content://스키마</code>를 가진 Uri를 전달해서 Content Provider가 제공하는 데이터에 접근 가능해지는 것이다.
<br></p>
<h3 id="bitmapfactory">BitmapFactory</h3>
<p>파일, 스트림 및 바이트 배열을 포함한 다양한 소스에서 Bitmap 객체를 만든다. Bitmap을 만들 수 있는 여러 가지 디코딩 메소드들을 제공하며, <code>BitmapFactory.Options</code> 객체로 디코딩 옵션을 지정할 수 있다.</p>
<blockquote>
<ul>
<li><strong>inJustDecodeBounds = true</strong>
메모리 할당을 방지한다. 디코딩할 때 이미지(데이터의 크기, 유형을 읽을 수 있음) 크기만 먼저 불러오기 때문에 OutOfMemory를 일으킬만한 큰 이미지를 불러와도 선처리를 가능하게 해준다.<br>
</li>
</ul>
</blockquote>
<ul>
<li><strong>inSampleSize</strong>
얼만큼 줄여서 디코딩할지 지정하면 이미지를 서브 샘플링하여 더 작은 버전을 메모리에 로드하도록 지시한다.</li>
</ul>
<h4 id="decodestream">decodeStream</h4>
<pre><code class="language-java">@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, @Nullable BitmapFactory.Options opts) {...}</code></pre>
<ul>
<li><p><code>InputStream</code>
비트맵으로 디코딩 할 원시 데이터를 보유하는 입력 스트림.</p>
</li>
<li><p><code>outPadding</code>
null이 아닌 경우, 비트맵에 대한 Padding 사각형 반환.
null인 경우, Padding을 [-1, -1, -1, -1]로 설정.</p>
</li>
<li><p><code>opts</code>
다운 샘플링을 제어하는 옵션과 이미지가 완전히 디코딩 되어야 하는지, 크기만 반환되어야 하는지.</p>
</li>
</ul>
<br>
<br>

<p>여기까지 진행하면 이미지 최적화 완료!인 줄 알았는데 띠용... 몇 몇 이미지가 회전된 채로 서버에 날아갔다..^^..?ㅎㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/aeedc351-ce09-4824-85f5-4c4e7c3302f0/image.png" alt=""></p>
<p>이런 말 없었자나요..(ノ｀Д)ノ 찾아보니 원본 이미지를 가로로 찍었을 경우 이런 현상이 발생할 수 있다고... 결론은 얼마나 회전 됐는지 정보를 받아온 뒤, 그만큼 또 반대로 회전시켜서 저장해야한다...ㅋㅅㅋ
<br>
<br></p>
<h2 id="🎈-회전된-이미지-되돌리기">🎈 회전된 이미지 되돌리기</h2>
<pre><code class="language-kotlin">private fun rotateImageIfRequired(context: Context, bitmap: Bitmap, uri: Uri): Bitmap? {
    val input = context.contentResolver.openInputStream(uri) ?: return null

    val exif = if (Build.VERSION.SDK_INT &gt; 23) {
        ExifInterface(input)
    } else {
        ExifInterface(uri.path!!)
    }

    val orientation =
        exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)

    return when (orientation) {
        ExifInterface.ORIENTATION_ROTATE_90 -&gt; rotateImage(bitmap, 90)
        ExifInterface.ORIENTATION_ROTATE_180 -&gt; rotateImage(bitmap, 180)
        ExifInterface.ORIENTATION_ROTATE_270 -&gt; rotateImage(bitmap, 270)
        else -&gt; bitmap
    }
}

private fun rotateImage(bitmap: Bitmap, degree: Int): Bitmap? {
    val matrix = Matrix()
    matrix.postRotate(degree.toFloat())
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}</code></pre>
<p><strong>이미지가 가지고 있는 정보의 집합 클래스인 ExifInterface</strong>에서 얼마나 회전했는지 정보를 받아와 <code>rotateImage</code> 메소드를 통해 재회전을 시켜 리턴해준다. <code>rotateImageIfRequired</code> 메소드는 <code>optimizeBitmap</code>에서 decodeStream을 진행한 후에 호출해주면 된다.
<br>
<br></p>
<h1 id="fileutil-전체-코드">FileUtil 전체 코드</h1>
<pre><code class="language-kotlin">object FileUtil {

    private const val MAX_WIDTH = 1280
    private const val MAX_HEIGHT = 960

    ...

    fun optimizeBitmap(context: Context, uri: Uri): String? {
        try {
            val storage = context.cacheDir
            val fileName = String.format(&quot;%s.%s&quot;, UUID.randomUUID(), &quot;jpg&quot;)

            val tempFile = File(storage, fileName)
            tempFile.createNewFile()

            val fos = FileOutputStream(tempFile)

            decodeBitmapFromUri(uri)?.apply {
                compress(Bitmap.CompressFormat.JPEG, 100, fos)
                recycle()
            } ?: throw NullPointerException()

            fos.flush()
            fos.close()

            return tempFile.absolutePath

        } catch (e:Exception) {
            Log.e(TAG, &quot;FileUtil - ${e.message}&quot;)
        }

        return null
    }

    private fun decodeBitmapFromUri(uri: Uri, context: Context): Bitmap? {

        val input = BufferedInputStream(context.contentResolver.openInputStream(uri))

        input.mark(input.available())

        var bitmap: Bitmap?

        BitmapFactory.Options().run {
            inJustDecodeBounds = true
            bitmap = BitmapFactory.decodeStream(input, null, this)

            input.reset()

            inSampleSize = calculateInSampleSize(this)
            inJustDecodeBounds = false

            bitmap = BitmapFactory.decodeStream(input, null, this)?.apply {
                rotateImageIfRequired(this, uri)
            }
        }

        input.close()

        return bitmap

    }

    private fun calculateInSampleSize(options: BitmapFactory.Options): Int {
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var inSampleSize = 1

        if (height &gt; MAX_HEIGHT || width &gt; MAX_WIDTH) {

            val halfHeight: Int = height / 2
            val halfWidth: Int = width / 2

            while (halfHeight / inSampleSize &gt;= MAX_HEIGHT &amp;&amp; halfWidth / inSampleSize &gt;= MAX_WIDTH) {
                inSampleSize *= 2
            }
        }

        return inSampleSize
    }

    private fun rotateImageIfRequired(context: Context, bitmap: Bitmap, uri: Uri): Bitmap? {
        val input = context.contentResolver.openInputStream(uri) ?: return null

        val exif = if (Build.VERSION.SDK_INT &gt; 23) {
            ExifInterface(input)
        } else {
            ExifInterface(uri.path!!)
        }

        val orientation =
            exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)

        return when (orientation) {
            ExifInterface.ORIENTATION_ROTATE_90 -&gt; rotateImage(bitmap, 90)
            ExifInterface.ORIENTATION_ROTATE_180 -&gt; rotateImage(bitmap, 180)
            ExifInterface.ORIENTATION_ROTATE_270 -&gt; rotateImage(bitmap, 270)
            else -&gt; bitmap
        }
    }

    private fun rotateImage(bitmap: Bitmap, degree: Int): Bitmap? {
        val matrix = Matrix()
        matrix.postRotate(degree.toFloat())
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    }

}</code></pre>
<p>로그를 찍어서 얼마나 압축됐는지 확인해보면 더 좋겠쥬 ( •̀ ω •́ )y
<br></p>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/939a3a68-57a2-4c98-a540-e21528bc1c72/image.png" alt=""></p>
<p>아직 최적화만 진행한거라 앞으로 더 담을 내용이 많아서 시리즈로 나눌 예정이다 ^_ㅠ
다음 포스팅은 FormData에 담아 retrfit을 통해 서버로 보내는 내용을 담을 것이다 뿅 💨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드와 코틀린이란? 프로젝트 구조 알아보기]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%99%80-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%B4%EB%9E%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%99%80-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%B4%EB%9E%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 19 Apr 2022 04:04:12 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀 
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 제가 잘못 이해한 부분이 있다면 알려주시면 감사하겠습니다 💌</p>
</blockquote>
<hr>
<h2 id="안드로이드란">안드로이드란</h2>
<p>모바일 기기를 제어하는 리눅스 기반의 운영체제</p>
<h2 id="안드로이드를-왜-사용할까">안드로이드를 왜 사용할까</h2>
<p><strong>1. 누구나 참여할 수 있는 오픈소스</strong>
개발자나 단말기를 생산하는 사람들은 기기의 호환성에 맞게 자유롭게 수정 가능</p>
<p><strong>2. 다양한 생태계 존재</strong>
Android TV, Wear OS, Android Auto, Android Things 등</p>
<p><strong>3. 풍부한 리소스와 검증된 라이브러리</strong>
구글 기반 서비스들과 쉽게 연동 가능</p>
<p><strong>4. 특정 운영체제나 장비가 필요 없음</strong></p>
<hr>
<h2 id="코틀린이란">코틀린이란</h2>
<p>자바에서 일어나는 문제 해결을 위해 만들어진, JVM에서 동작하는 정적 타입 프로그래밍 언어</p>
<h2 id="코틀린의-장점">코틀린의 장점</h2>
<p><strong>1. 간결성</strong>
ex. getter와 setter를 작성하는 Java의 class ➡ Kotlin에서 data class로 간결하게 표현
<img src="https://velog.velcdn.com/images/dear_jjwim/post/e9fb26ca-d837-4331-8f9c-103ad26331a0/image.png" alt=""></p>
<p><strong>2. 더 안전한 코드 작성</strong>
ex. Null Safety, 컴파일 시점에 미리 알려주기 때문에 NullPointException 방지</p>
<p><strong>3. 자바와 100% 호환 가능</strong></p>
<hr>
<h2 id="프로젝트-구조">프로젝트 구조</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/13f561d8-fef8-4b8a-8196-b34d60fdfdd5/image.png" alt=""></p>
<ul>
<li><strong>app</strong> : 소스코드</li>
<li><strong>Gradle Scripts</strong> : 빌드에 필요한 정보 (앱을 만드는데 필요한 옵션, 라이브러리 등)<br>

</li>
</ul>
<h2 id="📁-app">📁 app</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/0e097272-a1cc-4b1e-8762-d8aa0d47a716/image.png" alt=""></p>
<h3 id="manifests">manifests</h3>
<p>앱과 관련된 기본적인 설정 - 앱의 4대 구성요소(Activity, Service, Broadcast Receiver, Content Provider)와 권한들을 명시함</p>
<h3 id="java">java</h3>
<p>앱 내 동작을 정의하는 코드들
android test : 안드로이드 프레임워크를 가지고 테스트
test : 유닛 테스트</p>
<h3 id="res">res</h3>
<p>앱 내 사용하는 자원들의 집합 (사진, 아이콘, 색상, 문자 등)</p>
<h2 id="🐘-gradle-scripts">🐘 Gradle Scripts</h2>
<p><img src="https://velog.velcdn.com/images/dear_jjwim/post/591bbac5-de59-4cea-ac28-641d1ddb35a6/image.png" alt=""></p>
<blockquote>
<h3 id="미리-알고-가자-🙋🏻♀️">미리 알고 가자 🙋🏻‍♀️</h3>
</blockquote>
<ul>
<li><strong>Gradle : 빌드 프로세스 자동화 툴</strong>
➡ 코드를 작성하고 컴파일, 테스트, 서명 및 배포, apk, bundle 패키징하는 작업까지 모두 이루어져야 앱을 사용할 수 있는데 이 과정을 자동화한 것!<br></li>
<li><strong>모듈 : 소스 파일 및 빌드 설정으로 구성된 모음</strong>
  프로젝트에는 하나 이상의 모듈이 포함 될 수 있으며, 하나의 모듈이 다른 모듈을 종속 항목으로 사용할 수 있다. 각 모듈은 개별적으로 빌드, 테스트 및 디버그 할 수 있다.<br>
 <strong>📱 안드로이드 스튜디오 대표적인 모듈</strong><ul>
<li>*<em>Application : *</em> 처음으로 프로젝트를 만들고 생성되는 app 모듈 ➡ APK 파일 생성</li>
<li>*<em>Android Library : *</em> 안드로이드 프로젝트에서 지원되는 모든 파일 형식을 포함할 수 있음, Application 모듈의 종속 항목으로 추가 할 수 있음 ➡ AAR 파일 생성</li>
<li>*<em>Java or Kotlin library : *</em> 순수 Java 혹은 Kotlin으로만 이루어진 모듈, 안드로이드 프레임워크로부터 독립적인 기능을 구현할 때 사용 ➡ JAR 파일 생성</li>
</ul>
</li>
</ul>
<h3 id="bulidgradle-project-xxx">bulid.gradle (Project: xxx)</h3>
<p>모든 하위 프로젝트 모듈에 공통적인 구성 옵션을 추가할 수 있는 최상위 빌드 파일</p>
<pre><code class="language-kotlin">// Top-level build file where you can add configuration options common to all sub-projects/modules.
// 모든 하위 프로젝트 모듈에 공통적인 구성 옵션을 추가할 수 있는 최상위 빌드 파일입니다.
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath &#39;com.android.tools.build:gradle:7.1.3&#39;
        classpath &#39;org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20&#39;
    }
}

// 새로 빌드할 때 기존 파일들을 어떻게 할지
task clean(type: Delete) {
    // 모든 파일을 삭제하고 다시 빌드
    delete rootProject.buildDir
}</code></pre>
<h3 id="settingsgradle">settings.gradle</h3>
<p>bulid.gradle (Project: xxx) 파일보다 조금 더 높은 스코프에 있음
bulid.gradle (Project: xxx) 의 플러그인을 어디서 가져올지 설정</p>
<h3 id="bulidgradle-module-xxx">bulid.gradle (Module: xxx)</h3>
<ul>
<li><p><strong>plugins</strong>
반드시 필요한 플러그인들 선언</p>
</li>
<li><p><strong>compileSdk</strong>
어떤 안드로이드 SDK 버전으로 앱을 컴파일 할 것인지 (앱 개발 당시의 최신 API를 컴파일 SDK로 지정할 것을 권장)</p>
</li>
<li><p><strong>defaultConfig</strong>
빌드 타입과는 무관하게 모두 적용되는 속성들</p>
</li>
<li><p><strong>minSdk</strong>
앱을 사용할 수 있는 최소한의 API 레벨</p>
</li>
<li><p><strong>targetSdk</strong>
앱이 기기에서 동작할 때 사용하는 API 레벨 (앱이 개발 당시 테스트 된 API 레벨)
앱 개발 당시의 최신 API를 targetSdk로 지정할 것을 권장</p>
</li>
<li><p><strong>buildTypes</strong>
release 모드(출시용), debug 모드(개발용)이 있다.</p>
</li>
<li><p><strong>dependencies</strong>
앱에 종속시킬 라이브러리들 선언</p>
</li>
</ul>
<pre><code class="language-kotlin">// 반드시 필요한 플러그인들 선언
plugins {
    id &#39;com.android.application&#39;
    id &#39;kotlin-android&#39;
}

android {
    // 어떤 안드로이드 SDK 버전으로 앱을 컴파일 할 것인지
    // 앱 개발 당시의 최신 API를 컴파일 SDK로 지정할 것을 권장
    compileSdk 31

    // 빌드 타입과는 무관하게 모두 적용되는 속성들
    defaultConfig {
        applicationId &quot;com.xxx.xxx&quot;
        minSdk 23 // 앱을 사용할 수 있는 최소한의 API 레벨
        targetSdk 31 // 앱이 기기에서 동작할 때 사용하는 API 레벨 (앱이 개발 당시 테스트 된 API 레벨) 앱 개발 당시의 최신 API를 targetSdk로 지정할 것을 권
        versionCode 1
        versionName &quot;1.0&quot;

        testInstrumentationRunner &quot;androidx.test.runner.AndroidJUnitRunner&quot;
    }

    // release 모드(출시용), debug 모드(개발용)이 있다.
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile(&#39;proguard-android-optimize.txt&#39;), &#39;proguard-rules.pro&#39;
        }
    }

    // 컴파일할 때 필요한 옵션들
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    // jvmTarget이 몇 버전인지
    kotlinOptions {
        jvmTarget = &#39;1.8&#39;
    }
}

// 아래 라이브러리들이 우리의 앱에 종속되어 있다
dependencies {
    implementation &#39;androidx.core:core-ktx:1.7.0&#39;
    implementation &#39;androidx.appcompat:appcompat:1.4.1&#39;
    implementation &#39;com.google.android.material:material:1.5.0&#39;
    implementation &#39;androidx.constraintlayout:constraintlayout:2.1.3&#39;
    implementation &#39;androidx.legacy:legacy-support-v4:1.0.0&#39;
    testImplementation &#39;junit:junit:4.+&#39;
    androidTestImplementation &#39;androidx.test.ext:junit:1.1.3&#39;
    androidTestImplementation &#39;androidx.test.espresso:espresso-core:3.4.0&#39;
    ...
}</code></pre>
<hr>
<br>

<p>📖 참고 
<a href="https://youtu.be/BIubAKowCyw">https://youtu.be/BIubAKowCyw</a>
<a href="https://youtu.be/kQEBMV9swzQ">https://youtu.be/kQEBMV9swzQ</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 RecyclerView Adapter View Binding 적용하기]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView-Adapter-View-Binding-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView-Adapter-View-Binding-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Apr 2022 14:31:08 GMT</pubDate>
            <description><![CDATA[<p>activity는 전부 뷰 바인딩 적용해놓고 RecyclerView 부분은 초기에 파일을 복사+붙여넣기를 반복하다보니 전부 <code>findViewById</code>로 적용해 온 내 자신..（；´д｀）ゞ</p>
<blockquote>
<p><strong>🙋🏻‍♀️ inflate란?</strong>
xml에 표기된 레이아웃들을 메모리에 객체화 시키는 것</p>
</blockquote>
<h2 id="💻-findviewbyid-를-적용한-코드">💻 findViewById 를 적용한 코드</h2>
<pre><code class="language-kotlin">class ReviewPhotoAdapter(val callback: (String) -&gt; Unit) :
    ListAdapter&lt;String, ReviewPhotoAdapter.ViewHolder&gt;(diffUtil) {

    inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        fun bind(item: String) {
            val reviewImageView = view.findViewById&lt;ImageView&gt;(R.id.reviewImageView)

            Glide.with(TestApp.instance)
                .load(item)
                .error(R.drawable.ic_person)
                .into(reviewImageView)

            reviewImageView.setOnClickListener {
                callback(item)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item_review_photo, parent, false)
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        return holder.bind(currentList[position])
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback&lt;String&gt;() {
            override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }
        }
    }
}</code></pre>
<p>위에 가져온 코드는 간단한 코드라 복잡해 보이지 않지만 뷰가 엄청 많은 레이아웃을 연결하면 <code>findViewById</code>로 도배가 되겠지요 핫 😅 그래서 결국 RecyclerView 부분도 전부 뷰 바인딩 (View Binding)을 적용해주기로 결정했다.
<br></p>
<h2 id="💻-view-binding-을-적용한-코드">💻 View Binding 을 적용한 코드</h2>
<pre><code class="language-kotlin">class ReviewPhotoAdapter(val callback: (String) -&gt; Unit) :
    ListAdapter&lt;String, ReviewPhotoAdapter.ViewHolder&gt;(diffUtil) {

    inner class ViewHolder(private val binding: ItemReviewPhotoBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(item: String) {
            Glide.with(TestApp.instance)
                .load(item)
                .error(R.drawable.ic_person)
                .into(binding.reviewImageView)

            binding.reviewImageView.setOnClickListener {
                callback(item)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            // ItemReviewPhotoBinding 클래스로 만들어지는 binding 인스턴스를 사용하도록 변경
            ItemReviewPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        return holder.bind(currentList[position])
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback&lt;String&gt;() {
            override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }
        }
    }
}</code></pre>
<h3 id="view-binding-을-쓰면-어떤-점이-좋을까-">View Binding 을 쓰면 어떤 점이 좋을까 ?</h3>
<p><strong>코드가 간결화</strong> 되는 장점도 있지만 <code>findViewById</code>는 사람이 직접 작성해 연결을 하다보니 남발하다보면 실수를 할 수 있기 마련인데..! <code>findViewById&lt;ⓐ&gt;(R.id.reviewImageView)</code> ⬅ ⓐ 부분에 <strong>뷰를 잘못 연결하는 실수를 할 리도 없고</strong> (<code>ImageView</code>를 넣어주어야 하는데 <code>TextView</code>를 입력하는 등), 간혹 안드로이드 4.1 이전 버전에서 Kotlin Extension 기능으로 <code>findViewById</code> 마저 생략하는 경우도 있는데 만약 다른 xml 파일과 id가 같을 경우 해당 뷰를 못 찾아 앱이 다운되어버리는 일도 발생한다. 결국 바인딩 된 xml 파일에서만 해당 id를 찾기 때문에 뷰 바인딩 (View Binding) 최고다 이거에용,,👍🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 커스텀 다이얼로그(Custom Dialog) 만들기, ClickListener로 클릭 이벤트 정의까지]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%8B%A4%EC%9D%B4%EC%96%BC%EB%A1%9C%EA%B7%B8Custom-Dialog-%EB%A7%8C%EB%93%A4%EA%B8%B0-ClickListener%EB%A1%9C-%ED%81%B4%EB%A6%AD-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%A0%95%EC%9D%98%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%8B%A4%EC%9D%B4%EC%96%BC%EB%A1%9C%EA%B7%B8Custom-Dialog-%EB%A7%8C%EB%93%A4%EA%B8%B0-ClickListener%EB%A1%9C-%ED%81%B4%EB%A6%AD-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%A0%95%EC%9D%98%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Fri, 01 Apr 2022 08:45:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://media.vlpt.us/images/dear_jjwim/post/c3d2872c-5293-46c8-a727-6e3ca9b7b190/image.png" alt="">
나는 개인적으로 안드로이드 기본 다이얼로그 디자인을 선호하지 않는다..💦 저 상태에서 기본 색상을 변경하더라도 알림창 하나로 APP 전체적인 디자인 컨셉을 흩트릴 수도 있다는 생각에 기본 알림창 다이얼로그를 전부 커스텀하기로 결정했다.</p>
<p><img src="https://media.vlpt.us/images/dear_jjwim/post/293025b4-30be-46c3-97d7-5eff7d9ce9e7/image.png" alt=""></p>
<p>만들어 볼 커스텀 다이얼로그이다. 여러 페이지(activity, fragment)에 쓰일 수 있도록 TextView는 고정 문구가 아니라 파라미터로 전달 받은 문구를 넣어줄 예정이다 ❗
<br></p>
<h2 id="🎈-디자인하기---xml-작성">🎈 디자인하기 - xml 작성</h2>
<p>먼저 <code>dialog_confirm.xml</code> 파일을 작성한다. <code>Button</code> 스타일은 필자가 따로 정의해놓은거라서 원하는 스타일로 디자인해주면 된다.</p>
<pre><code class="language-xml">dialog_confirm.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:background=&quot;@drawable/view_round_white_6&quot;
    android:gravity=&quot;center&quot;
    android:minWidth=&quot;300dp&quot;
    android:orientation=&quot;vertical&quot;
    android:padding=&quot;20dp&quot;&gt;

    &lt;TextView
        android:id=&quot;@+id/confirmTextView&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:gravity=&quot;center&quot;
        android:minHeight=&quot;50dp&quot;
        android:textSize=&quot;14sp&quot;
        tools:text=&quot;확인창입니다.&quot; /&gt;

    &lt;LinearLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;20dp&quot;
        android:orientation=&quot;horizontal&quot;&gt;

        &lt;Button
            android:id=&quot;@+id/noButton&quot;
            style=&quot;@style/MainOutlineButton&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;40dp&quot;
            android:layout_marginEnd=&quot;7dp&quot;
            android:layout_weight=&quot;1&quot;
            android:text=&quot;취소&quot;
            android:textSize=&quot;14sp&quot; /&gt;

        &lt;Button
            android:id=&quot;@+id/yesButton&quot;
            style=&quot;@style/MainFillButton&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;40dp&quot;
            android:layout_weight=&quot;1&quot;
            android:text=&quot;확인&quot;
            android:textSize=&quot;14sp&quot; /&gt;

    &lt;/LinearLayout&gt;

&lt;/LinearLayout&gt;</code></pre>
<p>만약 테두리를 둥글게 하고 싶다면 drawable에 아래 파일을 작성해 <code>dialog_confirm.xml</code>의 최상위 레이아웃 <code>background</code>로 적용시켜주면 된다.</p>
<pre><code class="language-xml">view_round_white_6.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;shape xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;solid android:color=&quot;@color/white&quot; /&gt;
    &lt;corners android:radius=&quot;6dp&quot; /&gt;
&lt;/shape&gt;</code></pre>
<br>

<h2 id="🎈-custom-dialog-class-작성">🎈 Custom Dialog class 작성</h2>
<p>필자는 삭제할 id 값을 넘겨주어야 했기 때문에 파라미터로 id 값을 넣어주었지만, 간단히 activity 종료를 한다거나 dismiss() 정도만 원한다면 파라미터를 받을 필요 없다.</p>
<p>단순히 <strong>다이얼로그 종료</strong> <code>dismiss()</code>만 원한다면 아래 Dialog 클래스 내의 정의로 끝낼 수 있지만, <strong>activity 종료</strong> <code>finish()</code>를 원한다면 인터페이스를 통해 해당 activity에 액션을 정의해주어야한다.</p>
<pre><code class="language-kotlin">class ConfirmDialog(
    confirmDialogInterface: ConfirmDialogInterface,
    text: String, id: Int
) : DialogFragment() {

    // 뷰 바인딩 정의
    private var _binding: DialogPackageDeleteBinding? = null
    private val binding get() = _binding!!

    private var confirmDialogInterface: ConfirmDialogInterface? = null

    private var text: String? = null
    private var id: Int? = null

    init {
        this.text = text
        this.id = id
        this.confirmDialogInterface = confirmDialogInterface
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = DialogPackageDeleteBinding.inflate(inflater, container, false)
        val view = binding.root

        // 레이아웃 배경을 투명하게 해줌, 필수 아님
        dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

        binding.confirmTextView.text = text

        // 취소 버튼 클릭
        binding.noButton.setOnClickListener {
            dismiss()
        }

        // 확인 버튼 클릭
        binding.yesButton.setOnClickListener {
            this.confirmDialogInterface?.onYesButtonClick(id!!)
            dismiss()
        }

        return view
    }

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

interface ConfirmDialogInterface {
    fun onYesButtonClick(id: Int)
}</code></pre>
<br>

<h2 id="🎈-사용할-activity-또는-fragment에서-호출하기">🎈 사용할 activity 또는 fragment에서 호출하기</h2>
<p>원하는 activity 또는 fragment에서 커스텀한 다이얼로그 및 인터페이스를 정의해주면 끝!</p>
<blockquote>
<h3 id="여기서-하나-짚고-넘어갈-것-🙋🏻♀️">여기서 하나 짚고 넘어갈 것 🙋🏻‍♀️</h3>
<p><em>dialog.show(manager: FragmentManager, tag: String)</em> 호출 시, manager로 넘겨줄 값 !</p>
</blockquote>
<ul>
<li>activity ➡ <code>this.supportFragmentManager</code></li>
<li>fragment ➡ <code>activity?.supportFragmentManager!!</code></li>
</ul>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity(), ConfirmDialogInterface {

    private lateinit var binding: ActivityMainBinding

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        clickViewEvents()
    }

    // 뷰 클릭 이벤트 정의
    private fun clickViewEvents() {
        // 삭제 버튼 클릭
        binding.deleteButton.setOnClickListener {
            val dialog = ConfirmDialog(this, &quot;패키지를 삭제하시겠습니까?&quot;, pkgId)
            // 알림창이 띄워져있는 동안 배경 클릭 막기
            dialog.isCancelable = false
            dialog.show(this.supportFragmentManager, &quot;ConfirmDialog&quot;)
        }
    }

    override fun onYesButtonClick(id: Int) {
        // 액티비티 종료를 원한다면 finish()를 호출해주면 되겠죵 :)
        deletePackageApiCall(id)
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 EditText 영문, 숫자, 특수문자만 입력 - 한글 입력 제한하기, 한글 포함 여부 확인하기 (feat. 특정 문자만 입력 허용하기)]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-EditText-%ED%95%9C%EA%B8%80-%EC%9E%85%EB%A0%A5-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0-%ED%95%9C%EA%B8%80-%ED%8F%AC%ED%95%A8-%EC%97%AC%EB%B6%80-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0-feat.-%EC%9B%90%ED%95%98%EB%8A%94-%EB%AC%B8%EC%9E%90%EB%A7%8C-%EC%9E%85%EB%A0%A5-%ED%97%88%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-EditText-%ED%95%9C%EA%B8%80-%EC%9E%85%EB%A0%A5-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0-%ED%95%9C%EA%B8%80-%ED%8F%AC%ED%95%A8-%EC%97%AC%EB%B6%80-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0-feat.-%EC%9B%90%ED%95%98%EB%8A%94-%EB%AC%B8%EC%9E%90%EB%A7%8C-%EC%9E%85%EB%A0%A5-%ED%97%88%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 31 Mar 2022 06:20:05 GMT</pubDate>
            <description><![CDATA[<p>일반적으로 id에는 한글을 포함할 수 없는 법.. 어떤 방법으로 제한할지 고민하다가 보통 APP들의 경우 포스팅 제목과 같이 두 가지 경우를 적용시키고 있었다. 그래서 일단은 두 가지 경우 다 적용시켜보기로 했다 🙋🏻‍♀️ !!
<br></p>
<h3 id="🎈-한글-포함-여부-확인하기">🎈 한글 포함 여부 확인하기</h3>
<p>입력하는 동안은 한글이 허용되지만 형식이 잘못되었다는 알림을 띄우거나, 다음 버튼을 비활성화하는 등의 예외처리를 한다. 이 방법은 정규식을 이용하면 된다. 아래 코드는 텍스트 변경을 감지하다가 한글을 포함하면 다음 버튼을 비활성화하는 예시이다.</p>
<p>&#39;onMyTextChanged&#39; 라는 EditText 확장 함수를 선언해 텍스트 변경 감지한다.</p>
<pre><code class="language-kotlin">Extensions.kt

fun EditText.onMyTextChanged(completion: (Editable?) -&gt; Unit) {
    this.addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}

        override fun afterTextChanged(editable: Editable?) {
            completion(editable)
        }
    })
}</code></pre>
<blockquote>
<h3 id="👩🏻🏫-미리-알고-가자-">👩🏻‍🏫 미리 알고 가자 !</h3>
<p><strong>Pattern</strong> : 정규 표현식이 컴파일 된 클래스. 정규 표현식에 대상 문자열을 검증하거나 활용하기 위해 사용되는 클래스다.
<strong>Matcher</strong> : Pattern 클래스를 받아 대상 문자열과 패턴이 일치하는 부분을 찾거나 전체 일치 여부 등을 판별하기 위해 사용된다.</p>
</blockquote>
<ol>
<li><strong>Pattern.compile()</strong> : 주어진 정규식을 갖는 패턴을 생성한다.</li>
<li><strong>matcher()</strong> : 패턴에 매칭할 문자열(onMyTextChanged로 반환받은 문자)을 입력해 Matcher를 생성한다.</li>
<li><strong>find()</strong> : 패턴이 일치하는 문자열을 찾아 Boolean값을 반환한다. (있으면 true, 없으면 fasle 반환)</li>
</ol>
<pre><code class="language-kotlin">JoinIdFragment.kt

private fun initView() {
    binding.idEditText.onMyTextChanged {
        val pattern = Pattern.compile(HANGUL_PATTERN)
        val matcher = pattern.matcher(it.toString())
        // if문을 통해 예외처리를 해도 되겠지용 :)
        binding.nextButton.isEnabled = !(matcher.find())
    }
}

companion object {
    const val HANGUL_PATTERN = &quot;.*[ㄱ-ㅎㅏ-ㅣ가-힣]+.*&quot;
}
</code></pre>
<br>

<h3 id="🎈-한글-입력-제한하기">🎈 한글 입력 제한하기</h3>
<p>처음부터 영어, 숫자만 입력을 허용한다. 이 방법은 민망할 정도로 간단하다..! 코틀린 코드까지 갈 필요도 없이 <code>xml</code>에서 <code>EditText</code> 속성 설정으로 끝내버린다..⭐</p>
<p><strong>digits는 입력 가능한 문자 타입을 제한하는 속성이다.</strong> 휴대폰번호에 숫자와 하이픈만 받고 싶다면 <code>android:digits=&quot;0123456789-&quot;</code> 이런식으로 추가하면 된다. 나의 경우 언더바도 추가하면 좋을 것 같아서 아래와 같이 적용시켰다.</p>
<pre><code class="language-xml">fragment_join_id.xml

&lt;EditText
    android:id=&quot;@+id/idEditText&quot;
    android:maxLines=&quot;1&quot;
    android:digits=&quot;0123456789_qwertzuiopasdfghjklyxcvbnm&quot;/&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 Firebase Authentication - GitHub 로그인]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Firebase-Authentication-GitHub-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Firebase-Authentication-GitHub-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Thu, 23 Dec 2021 09:00:09 GMT</pubDate>
            <description><![CDATA[<h2 id="firebase-콘솔-설정">Firebase 콘솔 설정</h2>
<p><a href="https://firebase.google.com/docs/android/setup?hl=ko">안드로이드 프로젝트에 Firebase 추가</a>는 필수입니다. 그 다음 Firebase 콘솔의 좌측바에 있는 <strong>Authentication 메뉴 선택 → Sign-in method → 로그인 제공업체에서 GitHub 선택</strong>을 하시고 나면 아래와 같은 화면이 뜰 거에요.</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/78f7d3bc-b5cd-4469-9881-5f16e85b3832/%ED%8C%8C%EB%B2%A01.png" alt=""></p>
<p>클라이언트 ID와 보안 비밀번호를 필수 입력하고, GitHub 앱 구성에 승인 콜백 URL을 문구가 뜹니다. 이 조건을 충족하려면 GitHub 홈페이지에 로그인한 후, Settings → Developer settings 페이지로 이동합니다.</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/980b057e-807a-44f8-9620-ef1138cb7141/%ED%8C%8C%EB%B2%A02.png" alt=""></p>
<p>OAuth Apps 메뉴로 들어가 New OAuth App을 클릭해 앱등록을 합니다.</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/a5b1cce5-6546-40bd-a63f-ca0a02a03392/%ED%8C%8C%EB%B2%A03.png" alt=""></p>
<p>테스트용이라 나머지는 대충 입력해도 상관없지만, 위에 Firebase 콘솔 깃허브 설정 페이지 맨 하단에 있었던 <strong>콜백 URL을 붙여넣기</strong>는 꼭 지켜주셔야 합니다 !! Register application 버튼을 누르고 나면 클라이언트 ID와 보안 비밀번호를 확인할 수 있습니다.</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/3f089080-d6a4-4a81-aa41-69908bb55148/%ED%8C%8C%EB%B2%A04.png" alt=""></p>
<p>만약 Client secrets가 비어있다면 [Generate a new alient secret] 버튼을 눌러 새로 생성해주고 복사해주세요. 그리고 다시 Firebase 콘솔로 돌아와 클라이언트 ID와 보안 비밀번호에 각각 붙여넣기 해주세요.</p>
<h2 id="sha-지문-찾기">SHA 지문 찾기</h2>
<p>설정 마지막 단계로 SHA 인증서 지문을 등록해야 합니다. (저는 이 부분을 못보고 지나쳤다가 시간을 많이 버렸습니다😂 모두들 시간 아끼세요,, 흑흑) Firebase 콘솔에서 현재 우리가 작업하고 있는 프로젝트 설정으로 들어가게 되면 내 앱에 [SHA 인증서 지문 - 디지털 지문 추가]가 있어요!</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/8a13cff7-f568-4e56-91e8-bff807f897d8/%ED%8C%8C%EB%B2%A05.png" alt=""></p>
<p>그런데 이 SHA 지문을 어떻게 얻느냐 💁🏻‍♀️!!</p>
<p>cmd를 관리자 권한으로 실행해주시고 아래 내용을 붙여넣기 해줍니다. 그런데 만약 <strong>[&#39;keytool&#39;은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.]</strong> 문구가 뜬다면 당황하지 말고 <strong>JDK 환경변수 설정을 확인</strong>해보세요! 제대로 설정되어 있지 않거나 java가 설치되지 않았을 경우 저 문구가 뜹니다.</p>
<pre><code>keytool -list -v \
-alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore</code></pre><p>위 내용을 입력하고 나면 <strong>[키 저장소 비밀번호 입력]</strong>이 뜰 것입니다. 디버그 키의 비밀번호는 <strong>android</strong>입니다. 철자 한 자 한 자 입력해야해요! 비밀번호를 입력하고 나면 인증서 지문이 뜨는데 SHA1, SHA256을 복사해서 Firebase [SHA 인증서 지문 - 디지털 지문 추가]에 붙여넣기해주고 저장합니다.</p>
<p><img src="https://images.velog.io/images/dear_jjwim/post/b7a5ac86-0630-4646-a54c-93631a1f8395/%ED%8C%8C%EB%B2%A06.png" alt=""></p>
<h2 id="안드로이드-코드-작성">안드로이드 코드 작성</h2>
<p>이제 코드를 작성할 차례입니다. 먼저 <code translate="no" dir="ltr">app/build.gradle</code> 파일에 Firebase 인증 Android 라이브러리의 종속 항목을 선언합니다.</p>
<pre><code class="language-kotlin">dependencies {
    // Import the BoM for the Firebase platform
    implementation platform(&#39;com.google.firebase:firebase-bom:28.4.1&#39;)

    // Declare the dependency for the Firebase Authentication library
    // When using the BoM, you don&#39;t specify versions in Firebase library dependencies
    implementation &#39;com.google.firebase:firebase-auth-ktx&#39;
}</code></pre>
<p>GitHub 로그인이 필요한 Activity의 코드입니다. 자세한 설명은 주석으로 달아놓았어요 😀 !!</p>
<pre><code class="language-kotlin">class LoginActivity : AppCompatActivity() {
    private lateinit var binding: ActivityLoginBinding

    private lateinit var auth: FirebaseAuth

    // 빌더로 OAuthProvider의 인스턴스를 생성
    private val provider = OAuthProvider.newBuilder(&quot;github.com&quot;)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)

        auth = Firebase.auth

        initGitHubLoginButton()
    }

    private fun initGitHubLoginButton() {
        binding.loginGithubButton.setOnClickListener {
            if (TextUtils.isEmpty(binding.emailEditText.text.toString())) {
                Toast.makeText(this, &quot;깃허브 아이디를 입력해주세요.&quot;, Toast.LENGTH_LONG).show()
            } else {
                // 선택사항: OAuth 요청과 함께 전송하고자 하는 커스텀 OAuth 매개변수를 추가로 지정합니다.
                provider.addCustomParameter(&quot;login&quot;, getInputEmail())

                auth.startActivityForSignInWithProvider( /* activity= */this, provider.build())
                    .addOnSuccessListener(
                        OnSuccessListener&lt;AuthResult?&gt; { authResult -&gt;
                            auth.signInWithCredential(authResult.credential!!)
                                .addOnCompleteListener(this@LoginActivity) { task -&gt;
                                    if (task.isSuccessful) {
                                        finish()
                                    } else {
                                        Toast.makeText(this, &quot;깃허브 로그인 실패&quot;, Toast.LENGTH_LONG).show()
                                    }
                                }
                        })
                    .addOnFailureListener(
                        OnFailureListener {
                            Toast.makeText(this, &quot;Error : $it&quot;, Toast.LENGTH_LONG).show()
                        })
            }
        }
    }
}</code></pre>
<p><img src="https://images.velog.io/images/dear_jjwim/post/b3dc51b6-34af-4c53-8fef-860e94ad849c/image.png" alt="">
코드까지 작성하고 실행하면 이제 안드로이드 어플에서 GitHub 로그인이 가능해집니다 🙋🏻‍♀️ 
더 궁금하신 점이 있으시다면 아래 문서들도 참고해주세요 -!</p>
<p><a href="https://firebase.google.com/docs/auth/android/github-auth?hl=ko#kotlin+ktx">https://firebase.google.com/docs/auth/android/github-auth?hl=ko#kotlin+ktx</a>
<a href="https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps">https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 코드상에서 TextView 문자의 범위를 지정해 색상, 크기, 스타일을 변경하는 SpannableStringBuilder, view binding]]></title>
            <link>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EA%B3%B5%EB%B6%80-%EC%9D%BC%EC%A7%80-6</link>
            <guid>https://velog.io/@dear_jjwim/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EA%B3%B5%EB%B6%80-%EC%9D%BC%EC%A7%80-6</guid>
            <pubDate>Mon, 20 Dec 2021 08:50:02 GMT</pubDate>
            <description><![CDATA[<h3 id="시작">시작</h3>
<blockquote>
<p>혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀 
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 제가 잘못 이해한 부분이 있다면 알려주시면 감사하겠습니다 💌</p>
</blockquote>
<hr>
<h3 id="🔹-spannablestringbuilder">🔹 SpannableStringBuilder</h3>
<p>코드상에서 TextView 문자의 일부의 색, 크기, 스타일을 변경할 수 있다.</p>
<pre><code class="language-kotlin">    val ssb = SpannableStringBuilder(binding.testTextView.text)
    ssb.setSpan(
        // 적용하고자 하는 효과가 무엇인지 지정
        ForegroundColorSpan(getColor(R.color.green)),
        // 효과를 주고자 하는 위치의 시작점
        binding.testTextView.text.length - 1,
        // 효과를 주고자 하는 위치의 끝점
        binding.testTextView.text.length,
        // Span의 포인트 또는 마크에 대한 제어를 위한 플래그를 지정
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )

    binding.testTextView.text = ssb</code></pre>
<blockquote>
<p><em><strong>플래그 속성 : INCLUSIVE(확장), EXCLUSIVE(단절)</strong></em>
🔽 예시 🔽
SPAN_INCLUSIVE_EXCLUSIVE : 앞부분에 확장성을 열어두고, 뒷부분에는 확장을 사용하지 않겠다
SPAN_EXCLUSIVE_INCLUSIVE : 앞부분은 확장을 하지 않고, 뒷부분에 확장성을 열겠다</p>
</blockquote>
<hr>
<h3 id="🔹-view-binding">🔹 view binding</h3>
<p>지금까지 나는 액티비티에서 뷰의 값을 변경하려면 findViewId를 통해 매번 연결 작업을 해주었다. 뷰가 많을수록 해당 코드가 지저분해져서 속상했었다😭 하지만 이제는 view binding을 통해 findViewId를 대체해보려 한다.</p>
<p>먼저 <code translate="no" dir="ltr">build.gradle</code> 파일에 view binding을 사용한다고 선언해주어야한다.</p>
<pre><code class="language-kotlin">android {
        ...
        viewBinding {
            enabled = true
        }
    }</code></pre>
<p>위의 설정을 마치면 각 XML 레이아웃 파일의 결합 클래스가 생성된다. 결합 클래스의 이름은 XML 파일의 이름을 카멜 표기법으로 변환하고 끝에 &#39;Binding&#39;을 추가하여 생성된다. 그리고 이를 적용할 activity의 <code translate="no" dir="ltr">onCreate()</code>에 선언해주면 된다.</p>
<blockquote>
<p>🙋🏻‍♀️ <strong>예시.</strong>
<code translate="no" dir="ltr">activity_main</code> &gt; ActivityMainBinding
<code translate="no" dir="ltr">activity_detail</code> &gt; ActivityDetailBinding</p>
</blockquote>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        // 메인 액티비티 -&gt; 액티비티 메인 바인딩
        // 자동으로 완성된 액티비티 메인 바인딩 클래스 인스턴스를 가져왔다.
        binding = ActivityMainBinding.inflate(layoutInflater)
        // 뷰 바인딩과 연결
        // 모든 결합 클래스에는 상응하는 레이아웃 파일의 루트 뷰에 관한 직접 참조
        setContentView(binding.root)

        binding.nameTextView.text = &quot;토끼&quot;
   }
}
</code></pre>
]]></description>
        </item>
    </channel>
</rss>