<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>aaaram__.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 02 Aug 2024 00:25:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>aaaram__.log</title>
            <url>https://images.velog.io/images/aaaram__/profile/cb4be1ca-e380-4f0d-85a3-2813774e595c/ic_launcher_round.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. aaaram__.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/aaaram__" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JWT decoding]]></title>
            <link>https://velog.io/@aaaram__/JWT-decoding</link>
            <guid>https://velog.io/@aaaram__/JWT-decoding</guid>
            <pubDate>Fri, 02 Aug 2024 00:25:27 GMT</pubDate>
            <description><![CDATA[<h3 id="json-web-token을-디코딩하기">Json web token을 디코딩하기</h3>
<p>회원 아이디 정보를 토큰에 숨겨놓았다고 하여 디코딩을 해보았다.
header, payload, signature 세 가지로 분리가 가능하며, 필요한 정보는 payload에 숨겨져 있다.</p>
<pre><code class="language-kotlin">    /**
     * [응답 예시]
     * header: {&quot;alg&quot;:&quot;HS256&quot;}
     * payload: {&quot;sub&quot;:&quot;65&quot;,&quot;iat&quot;:1722328823,&quot;exp&quot;:1724920823}
     * signature: �p�]��4�Ć�U�(��X�|��i�D�vH
     */

    fun getTokenPayload(token: String): String {
        val tokenParts = token.split(&quot;.&quot;)

        val decoder = Base64.getUrlDecoder()

        val header = String(decoder.decode(tokenParts[0]))
        val payload = String(decoder.decode(tokenParts[1]))
        val signature = String(decoder.decode(tokenParts[2]))

        LogUtil.d_dev(&quot;[JwtDecodingUtil] header: $header&quot;)
        LogUtil.d_dev(&quot;[JwtDecodingUtil] payload: $payload&quot;)
        LogUtil.d_dev(&quot;[JwtDecodingUtil] signature: $signature&quot;)

        return payload
    }</code></pre>
<h3 id="json-파싱하기">json 파싱하기</h3>
<p>payload에 숨겨진 멤버 정보를 data class로 파싱하여 뽑아쓴다.
나는 멤버 아이디를 정수로 변환하는 코드를 짰다.</p>
<pre><code class="language-kotlin">object GetMemberIdExtension {

    fun String.getMemberId(): Int? {
        return try {
            val memberId = Gson().fromJson(this, MemberId::class.java)
            memberId.sub.toInt()
        } catch (e: JsonSyntaxException) {
            LogUtil.e_dev(&quot;JsonSyntaxException: ${e.localizedMessage}&quot;)
            null
        } catch (e: NumberFormatException) {
            LogUtil.e_dev(&quot;NumberFormatException: ${e.localizedMessage}&quot;)
            null
        }
    }

    data class MemberId(
        val sub: String,
        val iat: Long,
        val exp: Long
    )
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : Tab Layout + View Pager (2)]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-Tab-Layout-View-Pager-2</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-Tab-Layout-View-Pager-2</guid>
            <pubDate>Wed, 31 Jul 2024 04:52:02 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며..</h3>
<p>오랜만에 컴포즈로 탭 레이아웃을 짤 일이 있어서 구글링해서 자료를 보는데, 너무나 익숙한 코드여서 계속 보다보니 내가 3년 전에 쓴 글이었다ㅋㅋ</p>
<p>그 사이 컴포즈 버전도 많이 올라오고머테리얼3 버전을 쓰다보니 업데이트를 할 필요가 있어보여서 새로운 글을 쓴다.</p>
<h1 id="컴포즈-tabrow--horizontalpager">컴포즈 TabRow + HorizontalPager</h1>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/36ab2483-2858-4fcd-b5da-2368577e07a8/image.png" alt=""></p>
<p>우선 <code>TabRow</code> 에는 두 종류가 있는데 아이콘이 있는 것과 없는 것.
그리고 세부적으로 좌우 스크롤이 되는 것과 그렇지 않은 것에 따라 총 다섯가지로 확인된다.</p>
<p>지금 할 것은 픽스된 사이즈 안에서 아이콘 없이 텍스트로만 표현하는 탭 로우 (<code>SecondaryTabRow</code>), 좌우 페이저(<code>HorizontalPager</code>)를 결합하여 만들 것이다.</p>
<h2 id="라이브러리">라이브러리</h2>
<h4 id="libsversionstoml">libs.versions.toml</h4>
<pre><code class="language-kotlin">[versions]
composeBom = &quot;2024.04.01&quot;
material = &quot;1.12.0&quot;

[libraries]
androidx-compose-bom = { group = &quot;androidx.compose&quot;, name = &quot;compose-bom&quot;, version.ref = &quot;composeBom&quot; }
androidx-ui = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui&quot; }
androidx-ui-graphics = { group = &quot;androidx.compose.ui&quot;, name = &quot;ui-graphics&quot; }
androidx-material3 = { group = &quot;androidx.compose.material3&quot;, name = &quot;material3&quot; }</code></pre>
<h4 id="buildgradlekts">build.gradle.kts</h4>
<pre><code class="language-kotlin">     implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.material3)</code></pre>
<h2 id="코딩">코딩</h2>
<ol>
<li><p><code>rememberPagerState</code> 를 통해 초기화 값을 세팅 해주고 <code>seletedTabIndex</code> 를 설정해주면 탭 UI가 만들어진다.</p>
</li>
<li><p>탭을 클릭했을 때 화면을 이동하기 위해 Tab의 <code>onClick</code> 메소드 안에 <code>[animate]scrollToPage(index)</code> 를 넣어준다. (코루틴 안에서 동작하기 떄문에 코루틴 스코프를 넣어 주었다)</p>
</li>
<li><p>HorizontalPager에 PagerState를 넣어주면 탭뷰와 결합이 되어 선택된 탭에 따라 화면이 좌우로 스크롤 된다. 만약 스와이프를 막고 싶다면 <code>userScrollEnabled = false</code> 로 설정하면 된다. (기본값은 true)
<code>content()</code> 라고 쓰여있는 부분은 선택된 인덱스에 다라 컴포저블을 넣어주면 된다.</p>
</li>
<li><p>그 외 컬러값이나 디자인은 알아서..</p>
</li>
</ol>
<pre><code class="language-kotlin">    val coroutineScope = rememberCoroutineScope()

    val tabs = listOf(&quot;대시보드&quot;, &quot;고장이력&quot;, &quot;게시판&quot;)
    val pagerState = rememberPagerState(
        pageCount = { tabs.size },
        initialPageOffsetFraction = 0f,
        initialPage = 0,
    )
    val tabIndex = pagerState.currentPage

    SecondaryTabRow(
        selectedTabIndex = tabIndex
    ) {
        tabs.forEachIndexed { index, value -&gt;
            Tab(selected = tabIndex == index,
                onClick = {
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(index)
                    }
                }
            ) {
                Text(text = &quot;value&quot;)
            }
        }
    }

    HorizontalPager(state = pagerState, userScrollEnabled = true) {
        content()
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git error] cannot lock ref 'branch': 'branch' exists; cannot create 'branch']]></title>
            <link>https://velog.io/@aaaram__/Git-error-cannot-lock-ref-branch-branch-exists-cannot-create-branch</link>
            <guid>https://velog.io/@aaaram__/Git-error-cannot-lock-ref-branch-branch-exists-cannot-create-branch</guid>
            <pubDate>Fri, 03 Nov 2023 04:44:54 GMT</pubDate>
            <description><![CDATA[<p>잘못 push    했던 브랜치가 자꾸 문제를 일으켜서 확인해보니 <code>git fetch --prune</code> 명령으로 오래된 브랜치와 충돌을 해결하라는 메세지가 나왔다.</p>
<p>그런데도 계속 해당 브랜치 때문에 에러가 나서 다른 사람들이 pull 을 받을 수 없는 상황이었다</p>
<p><code>git branch -r</code> 명령으로 조회해도 분명 없는 브랜치인데 왜 그러지.. 했으나 <code>git remote show origin</code> 로 조회하니 나오더라.. 무사히 해당 브랜치를 삭제하고 해피엔딩^ㅡ^</p>
<h3 id="error-log">error log</h3>
<blockquote>
</blockquote>
<pre><code class="language-bash">git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks fetch --tags origin
error: cannot lock ref &#39;refs/remotes/origin/develops/drivingReport&#39;: &#39;refs/remotes/origin/develops&#39; exists; cannot create &#39;refs/remotes/origin/develops/drivingReport&#39;
From https://git-codecommit.ap-northeast-2.amazonaws.com/
 ! [new branch]        develops/drivingReport -&gt; origin/develops/drivingReport  (unable to update local ref)
error: some local refs could not be updated; try running
 &#39;git remote prune origin&#39; to remove any old, conflicting branches</code></pre>
<h3 id="1-remote-branch-확인">1. remote branch 확인</h3>
<pre><code class="language-bash">git remote show origin</code></pre>
<blockquote>
<p>조회 시 나타나는 목록</p>
</blockquote>
<pre><code class="language-bash">* remote origin
  Fetch URL: https://
  Push  URL: https://
  HEAD branch: main
  Remote branches:
    autocard_garage_devlops                          tracked
    develops                                         tracked
    develops/drivingReport                           new (next fetch will store in remotes/origin)
    main                                             tracked
    service_multi_file                               tracked
  Local branches configured for &#39;git pull&#39;:
...</code></pre>
<h3 id="3-branch-삭제">3. branch 삭제</h3>
<pre><code class="language-bash">git push origin --delete develops/drivingReport</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flow 플로우]]></title>
            <link>https://velog.io/@aaaram__/Flow-%ED%94%8C%EB%A1%9C%EC%9A%B0</link>
            <guid>https://velog.io/@aaaram__/Flow-%ED%94%8C%EB%A1%9C%EC%9A%B0</guid>
            <pubDate>Tue, 21 Mar 2023 11:57:26 GMT</pubDate>
            <description><![CDATA[<h2 id="flow">Flow</h2>
<p>Flow는 코루틴을 기반으로 빌드되며 비동기로 계산되는 데이터 스트림이다. 데이터베이스에서 실시간 업데이트를 수신할 수 있다.</p>
<h3 id="리액티브-프로그래밍과-데이터-스트림">리액티브 프로그래밍과 데이터 스트림</h3>
<p><strong>리액티브 프로그래밍</strong>이란 데이터가 변경될 때 이벤트를 발생시켜 데이터를 계속해서 전달하도록 하는 프로그래밍 방식이다. 리액티브 프로그래밍에는 하나의 데이터를 발행하는 생산자가 있고 그 생산자가 데이터 소비자에게 지속적으로 데이터를 전달하는데, 이 것이 <strong>데이터 스트림</strong>의 요소이다.</p>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/c40e8d8a-3fe4-4a2f-92e4-81316c34da00/image.png" alt=""></p>
<ul>
<li>생산자는 스트림에 추가되는 데이터를 생산합니다. 코루틴 덕분에 흐름에서 비동기적으로 데이터가 생산될 수도 있다.</li>
<li>(선택사항) 중개자는 스트림에 내보내는 각각의 값이나 스트림 자체를 수정할 수 있다.</li>
<li>소비자는 스트림의 값을 사용한다.</li>
</ul>
<h3 id="예시">예시</h3>
<p>flow 블럭 내에서 <code>emit()</code> 함수를 사용하여 수동으로 데이터 스트림에 값을 내보낸다. (생산자 역할)
생산자는 suspend 함수인 <code>fetchLatestNews()</code> 의 네트워크 요청이 완료될 때까지 정지상태로 유지된다.</p>
<pre><code class="language-kotlin">class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow&lt;List&lt;ArticleHeadline&gt;&gt; = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
    suspend fun fetchLatestNews(): List&lt;ArticleHeadline&gt;
}</code></pre>
<p>데이터 스트림의 모든 값을 가져올 때 <code>collect</code> 를 사용한다.(소비자 역할)
코루틴 내에서 실행하야 하며 collect를 호출하는 코루틴은 데이터 스트림이 종료될 때까지 정지될 수 있다.</p>
<pre><code class="language-kotlin">class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            // Trigger the flow and consume its elements using collect
            newsRepository.favoriteLatestNews.collect { favoriteNews -&gt;
                // Update View with the latest favorite news
            }
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Coroutine 코루틴]]></title>
            <link>https://velog.io/@aaaram__/Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4</link>
            <guid>https://velog.io/@aaaram__/Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4</guid>
            <pubDate>Sat, 18 Mar 2023 11:09:04 GMT</pubDate>
            <description><![CDATA[<h2 id="coroutine">Coroutine</h2>
<p>비동기 프로그래밍을 하기 위해 코틀린에서 지원하는 라이브러리. 단일 스레드에서 많은 작업이 가능하여 메모리를 절약하고 스레드를 더 잘게 쪼개 사용하기 위한 개념이다.</p>
<h3 id="스레드와-다른-점">스레드와 다른 점</h3>
<p>코루틴은 작업 하나하나에 Thread 를 할당하는 것이 아닌 &#39;Object&#39; 를 할당해주고, 이 Object 를 자유롭게 스위칭함으로써 메모리와 Context Switching 비용 낭비를 대폭 줄일 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/0377ee23-c41f-4a1d-88e3-68a53beecbeb/image.png" alt=""></p>
<p>예를 들어 Thread A 가 Thread B 의 결과가 나오기까지 기다려야 한다면, Thread A 은 블로킹되어 Thread B 로 Context Switching이 일어나고 결과가 나올 때 까지 해당 자원을 쓰지 못한다.</p>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/0b29e790-58c9-4a7d-b135-318dda1d1720/image.png" alt=""></p>
<p>반면 코루틴에서 Object 1 이 Object 2 의 결과가 나오기까지 기다려야 한다면, Object 1 은 Suspend 되지만, Object 1 을 수행하던 Thread 는 그대로 유효하기 때문에 Object 2 도 Object 1 과 동일한 Thread 에서 실행될 수 있다.</p>
<h3 id="suspend">suspend</h3>
<p>함수 앞에 suspend 키워드를 붙이면 코루틴 내에서만 사용이 가능하다. 코루틴 내에서 suspend 함수가 호출된 경우 부모 루틴에서 코드를 잠시 멈추어 상태를 저장했다가, suspend 함수의 처리가 완료 되었을 때 부모 루틴이 다시 복원되어 그 다음 코드가 실행된다.</p>
<h3 id="코루틴의-빌더">코루틴의 빌더</h3>
<ul>
<li><code>launch</code> - 결과를 반환하지 않는 실행 후 망각 코루틴</li>
<li><code>async</code> - <code>await()</code>을 통해 코루틴 결과를 기다릴 수 있다.</li>
<li><code>withContext</code> - 부모 코루틴과 다른 컨텍스트(스레드)에서 코루틴을 실행 시킬 수 있다.</li>
<li><code>runBlocking</code> - 코루틴을 실행하고 완료될 때까지 현재 스레드를 중단시킨다.</li>
</ul>
<h3 id="dispatcher">Dispatcher</h3>
<ul>
<li><code>Main</code> - UI와 상호작용하는 경우</li>
<li><code>IO</code> - 디스크 또는 네트워크 작업</li>
<li><code>Default</code> - CPU를 많이 사용하는 경우</li>
</ul>
<h3 id="예시">예시</h3>
<pre><code class="language-kotlin">class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result&lt;LoginResponse&gt; {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = &quot;{ username: \&quot;$username\&quot;, token: \&quot;$token\&quot;}&quot;

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success&lt;LoginResponse&gt; -&gt; // Happy path
                else -&gt; // Show error in UI
            }
        }
    }
}</code></pre>
<p><strong>로그인 함수가 실행되는 과정</strong></p>
<ul>
<li>앱이 기본 스레드의 <code>View</code> 레이어에서 <code>login()</code> 함수를 호출합니다.</li>
<li><code>launch</code>가 기본 스레드에 새 코루틴을 만들고 코루틴이 실행을 시작합니다.</li>
<li>코루틴 내에서 이제 <code>loginRepository.makeLoginRequest()</code> 호출은 <code>makeLoginRequest()</code>의 <code>withContext</code> 블록 실행이 끝날 때까지 코루틴의 추가 실행을 정지합니다.</li>
<li><code>withContext</code> 블록이 완료되면 <code>login()</code>의 코루틴이 네트워크 요청의 결과와 함께 기본 스레드에서 실행을 재개합니다.</li>
</ul>
<blockquote>
<p>참고 : <a href="https://developer.android.com/kotlin/coroutines?hl=ko">https://developer.android.com/kotlin/coroutines?hl=ko</a>
<a href="https://velog.io/@haero_kim/Thread-vs-Coroutine-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EA%B8%B0">https://velog.io/@haero_kim/Thread-vs-Coroutine-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EA%B8%B0</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thread/Handler 스레드와 핸들러]]></title>
            <link>https://velog.io/@aaaram__/ThreadHandler-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-%ED%95%B8%EB%93%A4%EB%9F%AC</link>
            <guid>https://velog.io/@aaaram__/ThreadHandler-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%99%80-%ED%95%B8%EB%93%A4%EB%9F%AC</guid>
            <pubDate>Fri, 17 Mar 2023 10:50:34 GMT</pubDate>
            <description><![CDATA[<h2 id="thread와-handler">Thread와 Handler</h2>
<p><strong>Thread</strong> : 동시 작업을 위한 하나의 실행 단위
<strong>Handler</strong> : Thread 간의 통신을 하기 위한 클래스</p>
<p>안드로이드의 UI는 Main Thread 라는 하나의 스레드에서 동작한다.
메인 스레드에서 네트워크나 DB 작업 등 무거운 작업을 하게되면 작업이 완료되기 전까지 멈춘 화면이 사용자에게 나타나고, ANR이 나타날 수 있기 때문에 여분의 스레드에서 백그라운드로 긴 작업을 실행한다. 그 과정에서 UI를 변경하려 한다면 Main Thread로 작업을 전달해야 하는데 이때 사용하는 것이 Handler이다.</p>
<h3 id="handler가-필요한-이유">Handler가 필요한 이유</h3>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/eb558a97-b596-4ac3-821c-0affe262eef3/image.png" alt=""></p>
<p>만약 병렬로 돌아가고 있는 Main Thread와 Sub Thread에서 동일한 TextView에 값을 저장하려 한다면 데드락이 발생하게 된다. 시스템에서 어떤 스레드의 작업을 먼저 처리해야 하는지 모르기 때문에 멈추게 되는 것이다. 이런 동기화 문제를 해결하기 위해 Handler를 통해 Sub Thread에서 Main Thread로 UI 작업을 전달하던지, Main Thread 내에서 자체적으로 처리하도록 해야한다.</p>
<h3 id="handler의-동작-과정">Handler의 동작 과정</h3>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/5168dc83-1d76-4983-8a1d-075950395311/image.png" alt=""></p>
<ol>
<li>Handler의 sendMessage() 를 통해 다른 Thread의 MessageQueue에 메시지를 전달한다.</li>
<li>Looper에서 MessageQueue의 메세지를 핸들러로 전달한다.</li>
<li>Handler는 handleMessage()를 통해 메시지를 처리한다.</li>
</ol>
<p>여기서 Looper는 하나의 스레드만을 담당하며 스레드 또한 하나의 Looper를 갖는다.</p>
<h3 id="예시">예시</h3>
<p>Thread 내에서 실행하기 위해 Runnable이라는 인터페이스를 사용한다.</p>
<pre><code class="language-kotlin">// 메인 스레드에서 실행하기 위한 Looper 추가
val handler = Handler(Looper.getMainLooper())

val runnable = Runnable {
    // TODO
    runOnUiThread {
        // 메인스레드에서 실행하고 싶을 때
    }
}

editText.addTextChangedListener {
    handler.removeCallbacks(runnuable) // 실행했던 함수 지우기
    handler.postDelayed(runnable, 500)
}</code></pre>
<blockquote>
<p>이미지 출처 : <a href="https://itmining.tistory.com/">https://itmining.tistory.com/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RTL 언어 목록]]></title>
            <link>https://velog.io/@aaaram__/RTL-%EC%96%B8%EC%96%B4-%EB%AA%A9%EB%A1%9D</link>
            <guid>https://velog.io/@aaaram__/RTL-%EC%96%B8%EC%96%B4-%EB%AA%A9%EB%A1%9D</guid>
            <pubDate>Tue, 01 Nov 2022 06:45:12 GMT</pubDate>
            <description><![CDATA[<h2 id="overview">Overview</h2>
<p>홈페이지 번역 관련 작업을 하면서 RTL 언어에 대해 파악할 필요가 있었다.
아래 목록에 있는 12 개의 언어는 RTL을 적용해야 한다.</p>
<h3 id="right-to-left-languages">Right-To-Left Languages</h3>
<table>
<thead>
<tr>
<th>ISO Language Code</th>
<th>Language Name</th>
</tr>
</thead>
<tbody><tr>
<td>ar</td>
<td>Arabic</td>
</tr>
<tr>
<td>arc</td>
<td>Aramaic</td>
</tr>
<tr>
<td>dv</td>
<td>Divehi</td>
</tr>
<tr>
<td>fa</td>
<td>Persian</td>
</tr>
<tr>
<td>ha</td>
<td>Hausa</td>
</tr>
<tr>
<td>he</td>
<td>Hebrew</td>
</tr>
<tr>
<td>khw</td>
<td>Khowar</td>
</tr>
<tr>
<td>ks</td>
<td>Kashmiri</td>
</tr>
<tr>
<td>ku</td>
<td>Kurdish</td>
</tr>
<tr>
<td>ps</td>
<td>Pashto</td>
</tr>
<tr>
<td>ur</td>
<td>Urdu</td>
</tr>
<tr>
<td>yi</td>
<td>Yiddish</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[앱센터에서 Azure Application Insights로 내보내기 & 깃허브에 자동으로 이슈 등록하기]]></title>
            <link>https://velog.io/@aaaram__/%EC%95%B1%EC%84%BC%ED%84%B0%EC%97%90%EC%84%9C-Azure-Application-Insights%EB%A1%9C-%EB%82%B4%EB%B3%B4%EB%82%B4%EA%B8%B0-%EA%B9%83%ED%97%88%EB%B8%8C%EC%97%90-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%9D%B4%EC%8A%88-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@aaaram__/%EC%95%B1%EC%84%BC%ED%84%B0%EC%97%90%EC%84%9C-Azure-Application-Insights%EB%A1%9C-%EB%82%B4%EB%B3%B4%EB%82%B4%EA%B8%B0-%EA%B9%83%ED%97%88%EB%B8%8C%EC%97%90-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%9D%B4%EC%8A%88-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 Sep 2022 04:14:30 GMT</pubDate>
            <description><![CDATA[<h1 id="appcenter-export-to-application-insights">AppCenter export to Application insights</h1>
<h2 id="1-azure에서-서비스-생성하기">1. Azure에서 서비스 생성하기</h2>
<ul>
<li>Azure에 <em>ResourceGroup</em>, <em>Workspace</em> 를 먼저 생성한다</li>
<li>Name을 입력하고 <em>ResourceGroup</em>, <em>Workspace</em> 을 선택하여 Application Insights 를 생성한다</li>
<li>생성이 완료되면 Overview 화면 상단에 있는 <strong>Instrumentation Key</strong> 를 복사한다</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/26706369/190309145-2e2f5dc7-c36d-4eb1-894e-a02009d31035.png" alt="image"></p>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/92afc4fe-e2a9-4be8-a9b6-2b844f22ccb4/image.png" alt=""></p>
<h2 id="2-appcenter에서-export-설정하기">2. AppCenter에서 Export 설정하기</h2>
<ul>
<li>AppCenter의 <em>[Settings - Export - New Export]</em> 메뉴를 통해 Application Insight를 <strong>custom</strong> 으로 등록한다.</li>
<li>위에서 복사한 Instrumentation 키를 붙여넣어 Create를 완료한다.</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/26706369/190308585-f642cb02-34ba-4125-ad21-6d24e649c75e.png" alt="image"></p>
<p><img src="https://user-images.githubusercontent.com/26706369/190309662-44ccbc29-7f4a-44bc-922d-13b9e96befd5.png" alt="image"></p>
<h1 id="application-insights-work-item-integration-with-github">Application insights Work item integration with GitHub</h1>
<h2 id="1-application-insights-work-items-에서-template-만들기">1. Application Insights Work Items 에서 Template 만들기</h2>
<ul>
<li>GitHub를 tracking system으로 등록한다.</li>
<li>Advanced setting을 통해 Assignee, Project, Template, Milestone 등을 지정할 수 있다</li>
<li>template을 적용 할때는 템플릿의 이름이 아닌 (Bug Report) 실제 파일명을 (bug_report.md) 적어야 한다.</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/26706369/190310111-714eb1b5-8258-4629-85c5-46548755726b.png" alt="image"></p>
<p><img src="https://velog.velcdn.com/images/aaaram__/post/6f57cb3a-2309-44e6-83d9-c6922a8ec679/image.png" alt=""></p>
<h2 id="2-결과-화면">2. 결과 화면</h2>
<ul>
<li>Template, Assignee, Labels가 잘 적용되었다.</li>
<li>그러나 Project 설정은 여러가지 서치와 시도를 해보았으나 아직 적용을 못하였다.</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/26706369/190310583-2ef3e9ec-e4d5-41a1-a4cd-8a487851a9cf.png" alt="image"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[minifyEnabled true 일때 proguard 작성]]></title>
            <link>https://velog.io/@aaaram__/minifyEnabled-true-%EC%9D%BC%EB%95%8C-proguard-%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@aaaram__/minifyEnabled-true-%EC%9D%BC%EB%95%8C-proguard-%EC%9E%91%EC%84%B1</guid>
            <pubDate>Mon, 05 Sep 2022 09:25:46 GMT</pubDate>
            <description><![CDATA[<p>앱 축소와 난독화를 위해 <strong>shrinkResources true</strong> 와 <strong>minifyEnabled true</strong> 를 추가한다.</p>
<p><code>app.gradle</code></p>
<pre><code class="language-kotlin">android {
..
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile(&#39;proguard-android-optimize.txt&#39;), &#39;proguard-rules.pro&#39;
        }
    }
..
}</code></pre>
<p>이대로 앱을 release 모드에서 테스트 할 경우 몇 라이브러리에서 난독화로 인한 충돌이 생긴다.</p>
<p>때문에 해당 라이브러리에 대한 예외처리를 proguard-rules.pro 에 작성해 주어야 한다.</p>
<p><a href="https://developer.android.com/studio/build/shrink-code#keep-code">&gt; 공식문서 보기</a></p>
<p>각 라이브러리마다 구글링을 통해 proguard 작성법을 알 수 있다.</p>
<ul>
<li><a href="https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro">Retrofit</a></li>
<li><a href="https://github.com/Azure/Communication/issues/69#issuecomment-712821078">Azure Communnication</a></li>
</ul>
<p>retrofit의 경우 MyApi의 class와 interface, 그리고 enum을 각각 예외 처리한 코드를 추가했다.</p>
<p>그리고 소셜 로그인 시 access token 을 identityToken에 잘 넣어주지 않아 model, utils 패키지를 모두 예외처리 하였다.</p>
<p><code>proguard-rules-pro</code></p>
<pre><code class="language-bash">
# When minifyEnabled true
# retrofit
-keep class MyApi.models.* { *; }
-keep interface MyApi.apis.* { *; }
-keepclassmembers enum MyApi.models.* { *; }
# model, utils
-keep class com.myapp.model.** { *; }
-keep class com.myapp.utils.** { *; }
-keepclassmembers class com.myapp.model.** { *; }
-keepclassmembers class com.myapp.utils.** { *; }
-keepclassmembers enum * { *; }
# azure call
-keep class com.skype.rt.** {*;}
-keep class com.azure.** {*;}
-keep class com.skype.android.** {*;}
-keep class com.microsoft.media.** {*;}
-keep class com.microsoft.dl.** {*;}
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod

# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations

# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault

# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* &lt;methods&gt;;
}

# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**

# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit

# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions.*
#-dontwarn retrofit2.KotlinExtensions$*

# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* &lt;methods&gt;; }
-keep,allowobfuscation interface &lt;1&gt;

# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response

# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 플레이 콘솔에서 프로덕션 배포 취소하기]]></title>
            <link>https://velog.io/@aaaram__/%EA%B5%AC%EA%B8%80-%ED%94%8C%EB%A0%88%EC%9D%B4-%EC%BD%98%EC%86%94%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EB%8D%95%EC%85%98-%EB%B0%B0%ED%8F%AC-%EC%B7%A8%EC%86%8C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@aaaram__/%EA%B5%AC%EA%B8%80-%ED%94%8C%EB%A0%88%EC%9D%B4-%EC%BD%98%EC%86%94%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EB%8D%95%EC%85%98-%EB%B0%B0%ED%8F%AC-%EC%B7%A8%EC%86%8C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 02 Sep 2022 08:10:01 GMT</pubDate>
            <description><![CDATA[<h2 id="앱-배포-취소하기">앱 배포 취소하기</h2>
<ol>
<li>다운로드 카운트가 <strong>0</strong> 이어야 한다.</li>
<li><code>ready</code> 상태가 아니어야 한다.</li>
<li>테스터를 모두 삭제한다.</li>
<li>국가를 모두 삭제한다.</li>
<li><code>출시 개요</code> 화면에서 (Publish Overview) <code>변경사항 검토 요청</code>을 클릭한다. (Send for review to submit your changes)</li>
<li><code>출시 &gt; 설정 &gt; 고급 설정 &gt; 앱 이용 가능 여부</code> 항목에서 (Release &gt; Setup &gt; Advanced settings &gt; App Availability) <code>출시 안됨</code> (Unpublish) 을 선택한다</li>
</ol>
<p><img src="https://user-images.githubusercontent.com/26706369/187838133-10f95e76-2085-4543-bdbd-b5b48ac209bc.png" alt="image"></p>
<p><a href="https://support.google.com/googleplay/android-developer/answer/9859654?hl=en">https://support.google.com/googleplay/android-developer/answer/9859654?hl=en</a></p>
<p><a href="https://support.google.com/googleplay/android-developer/answer/9859350?hl=en#unpublish">https://support.google.com/googleplay/android-developer/answer/9859350?hl=en#unpublish</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스마트폰 PC로 미러링하기]]></title>
            <link>https://velog.io/@aaaram__/%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%B0-PC%EB%A1%9C-%EB%AF%B8%EB%9F%AC%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@aaaram__/%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%B0-PC%EB%A1%9C-%EB%AF%B8%EB%9F%AC%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 02 Sep 2022 07:52:46 GMT</pubDate>
            <description><![CDATA[<h1 id="vysor">Vysor</h1>
<h2 id="1-장단점">1. 장단점</h2>
<ul>
<li>설치하고 사용하기 쉽다.</li>
<li>화질이 다소 떨어진다.</li>
</ul>
<h2 id="2-사용법">2. 사용법</h2>
<h3 id="vysor-설치">Vysor 설치</h3>
<p>당신의 OS에 맞는 프로그램을 설치한다.</p>
<blockquote>
<p><a href="https://www.vysor.io/download/">https://www.vysor.io/download/</a></p>
</blockquote>
<h3 id="실행">실행</h3>
<p>프로그램을 실행하고 폰 연결하면 끝!</p>
<h1 id="scrcpy">Scrcpy</h1>
<h2 id="1-장단점-1">1. 장단점</h2>
<ul>
<li>맥 유저라면 <code>brew</code>를 먼저 설치해야 한다.<blockquote>
<p><a href="https://brew.sh/index_ko">https://brew.sh/index_ko</a></p>
</blockquote>
</li>
<li>고화질의 화면이 구현된다.</li>
</ul>
<h2 id="2-사용법-1">2. 사용법</h2>
<h3 id="scrcpy-설치">Scrcpy 설치</h3>
<pre><code class="language-bash">brew install scrcpy</code></pre>
<h3 id="실행-1">실행</h3>
<p>폰을 연결하고 터미널에 명령어를 입력하면 끝!</p>
<pre><code class="language-bash">scrcpy</code></pre>
<p>참조 링크</p>
<blockquote>
<p><a href="https://github.com/Genymobile/scrcpy">https://github.com/Genymobile/scrcpy</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Azure search service REST API (2) - Filtering]]></title>
            <link>https://velog.io/@aaaram__/Azure-search-service-REST-API-2-Filtering</link>
            <guid>https://velog.io/@aaaram__/Azure-search-service-REST-API-2-Filtering</guid>
            <pubDate>Thu, 11 Aug 2022 08:50:37 GMT</pubDate>
            <description><![CDATA[<p>인덱스 별로 필터 조건이 다른 경우</p>
<pre><code class="language-kotlin">fun searchAutoComplete(
        searchTerm: String,
        index: String,
        marketingType: MarketingType?,
        consultationEnabled: Boolean?,
        distance: Float?,
        longitude: String?,
        latitude: String?
    ){
        val actionName = &quot;search&quot;
        Log.d(&quot;debug&quot;, &quot;$actionName $index started.&quot;)

        var hospitalFilter = &quot;&quot;
        var doctorFilter = &quot;&quot;
        var dealFilter = &quot;&quot;
        var departmentFilter = &quot;&quot;
        var specialtyFilter = &quot;&quot;

        val locationFilterString = &quot;geo.distance(Location/Point, geography&#39;POINT(${longitude} ${latitude})&#39;) le ${distance?.roundToInt()}&quot;

        marketingType?.let {
            if (it.name.equals(&quot;both&quot;)) {
                consultationEnabled?.let {
                    if(it) {
                        hospitalFilter = &quot;ConsultationEnabled eq $it&quot;
                        doctorFilter = &quot;ConsultationEnabled eq $it&quot;

                        if (distance != null &amp;&amp; distance &gt; 0f) {
                            hospitalFilter = hospitalFilter.plus(&quot;and $locationFilterString&quot;)
                        }
                    } else {
                        if (distance != null &amp;&amp; distance &gt; 0f) {
                            hospitalFilter = locationFilterString
                        }
                    }
                }
            } else {
                hospitalFilter = &quot;MarketingType eq &#39;${marketingType.value.replaceFirstChar { it.uppercase() }}&#39;&quot;
                doctorFilter = &quot;Hospital/MarketingType eq &#39;${marketingType.value.replaceFirstChar { it.uppercase() }}&#39;&quot;
                dealFilter = &quot;MarketingType eq &#39;${marketingType.value.replaceFirstChar { it.uppercase() }}&#39;&quot;
                departmentFilter = &quot;MarketingType eq &#39;${marketingType.value.replaceFirstChar { it.uppercase() }}&#39;&quot;
                specialtyFilter = &quot;Department/MarketingType eq &#39;${marketingType.value.replaceFirstChar { it.uppercase() }}&#39;&quot;

                if (distance != null &amp;&amp; distance &gt; 0f) {
                    hospitalFilter = hospitalFilter.plus(&quot;and $locationFilterString&quot;)
                }

                consultationEnabled?.let {
                    if(it) {
                        hospitalFilter = hospitalFilter.plus(&quot;and ConsultationEnabled eq $it&quot;)
                        doctorFilter = doctorFilter.plus(&quot;and ConsultationEnabled eq $it&quot;)

                        if (distance != null &amp;&amp; distance &gt; 0f) {
                            hospitalFilter = hospitalFilter.plus(&quot;and $locationFilterString&quot;)
                        }
                    } else {
                        if (distance != null &amp;&amp; distance &gt; 0f) {
                            hospitalFilter = locationFilterString
                        }
                    }
                }
            }
        }

        val searchOption = SearchOption(
            search = &quot;Translations/Name: ${searchTerm.trim()}*&quot;,
            select = &quot;Id, Translations&quot;,
            searchFields = &quot;Translations/Name&quot;,
            queryType = &quot;full&quot;,
            top = 3)

        when (index) {
            &quot;idx-hospitals-int&quot; -&gt; {
                searchOption.filter = hospitalFilter
                searchHospitals(searchOption, index)
            }
            &quot;idx-doctors-int&quot; -&gt; {
                searchOption.filter = doctorFilter
                searchDoctors(searchOption, index)
            }
            &quot;idx-deals-int&quot; -&gt; {
                searchOption.filter = dealFilter
                searchDeals(searchOption, index)
            }
            &quot;idx-departments-int&quot; -&gt; {
                searchOption.filter = departmentFilter
                searchDepartments(searchOption, index)
            }
            &quot;idx-specialties-int&quot; -&gt; {
                searchOption.filter = specialtyFilter
                searchSpecialties(searchOption, index)
            }
        }
    }</code></pre>
<img src="https://velog.velcdn.com/images/aaaram__/post/3c720cd4-60e0-4574-92aa-030169ed92a9/image.gif" width=600>
]]></description>
        </item>
        <item>
            <title><![CDATA[Azure search service REST API (1)]]></title>
            <link>https://velog.io/@aaaram__/Azure-search-service-REST-API-1</link>
            <guid>https://velog.io/@aaaram__/Azure-search-service-REST-API-1</guid>
            <pubDate>Thu, 28 Jul 2022 09:21:55 GMT</pubDate>
            <description><![CDATA[<p><a href="https://docs.microsoft.com/en-us/rest/api/searchservice/search-documents">Azure Search Document 링크</a></p>
<h1 id="1-search-client">1. Search Client</h1>
<pre><code class="language-kotlin">object SearchClient {
    private val okHttpClientBuilder = OkHttpClient().newBuilder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .addInterceptor { chain -&gt;
            val request = chain.request().newBuilder()
                .addHeader(&quot;Content-Type&quot; ,&quot;application/json&quot;)
                .addHeader(&quot;api-key&quot;, &quot;YOUR_KEY&quot;)
            chain.proceed(request.build())
        }
        .build()

    private val moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .add(OffsetDateTimeAdapter())
        .build()

    var retrofit = Retrofit.Builder()
        .baseUrl(&quot;https://YOUR_SERVICE_NAME.search.windows.net&quot;)
        .client(okHttpClientBuilder)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()
}</code></pre>
<h1 id="2-search-api">2. Search Api</h1>
<pre><code class="language-kotlin">interface SearchApi {
    @POST(&quot;indexes/{index}/docs/search?api-version=2021-04-30-Preview&quot;)
    suspend fun searchHospitals(@Body body: SearchOption, @Path(&quot;index&quot;) index: String): Response&lt;SearchHospitalResult&gt;

    @POST(&quot;indexes/{index}/docs/search?api-version=2021-04-30-Preview&quot;)
    suspend fun searchDoctors(@Body body: SearchOption, @Path(&quot;index&quot;) index: String): Response&lt;SearchDoctorResult&gt;

    @POST(&quot;indexes/{index}/docs/search?api-version=2021-04-30-Preview&quot;)
    suspend fun searchDeals(@Body body: SearchOption, @Path(&quot;index&quot;) index: String): Response&lt;SearchDealResult&gt;

    @POST(&quot;indexes/{index}/docs/search?api-version=2021-04-30-Preview&quot;)
    suspend fun searchDepartments(@Body body: SearchOption, @Path(&quot;index&quot;) index: String): Response&lt;SearchDepartmentResult&gt;

    @POST(&quot;indexes/{index}/docs/search?api-version=2021-04-30-Preview&quot;)
    suspend fun searchSpecialties(@Body body: SearchOption, @Path(&quot;index&quot;) index: String): Response&lt;SearchSpecialtyResult&gt;
}</code></pre>
<h1 id="3-search-viewmodel">3. Search ViewModel</h1>
<pre><code class="language-kotlin">    private val searchApi = SearchClient.retrofit.create(SearchApi::class.java)

    fun searchAutoComplete(searchTerm: String, index: String, page: Int = 1, limit: Int = 20){
        val searchOption = SearchOption(
            search = &quot;$searchTerm*&quot;,
            queryType = &quot;full&quot;,
            searchFields = &quot;Translations/Name&quot;,
            skip = limit.times(page.minus(1)), // pagination
            top = 3) // limit

        when (index) {
            &quot;idx-hospitals-int&quot; -&gt; {
                searchHospitals(searchOption, index, page)
            }
            &quot;idx-doctors-int&quot; -&gt; {
                searchDoctors(searchOption, index, page)
            }
            &quot;idx-deals-int&quot; -&gt; {
                searchDeals(searchOption, index, page)
            }
            &quot;idx-departments-int&quot; -&gt; {
                searchDepartments(searchOption, index, page)
            }
            &quot;idx-specialties-int&quot; -&gt; {
                searchSpecialties(searchOption, index, page)
            }
        }
    }

    private fun searchHospitals(searchOption: SearchOption, index: String, page: Int = 1) {
        val actionName = &quot;searchHospitals&quot;

        viewModelScope.launch(Dispatchers.Main) {
            val result = searchApi.searchHospitals(searchOption, index)
            try {
                if (result.isSuccessful) {
                    if (result.code() == 200) {
                        result.body()?.let { data -&gt;
                            _hospitals.postValue(data.value)
                        }
                    }
                } else {
                    Log.d(&quot;debug&quot;, &quot;$actionName failed&quot;)
                }
            } catch (e: Exception) {
                Log.d(&quot;debug&quot;, &quot;$actionName failed: ${e.message}&quot;)
            }
        }
    }</code></pre>
<h1 id="4-result">4. Result</h1>
<img src="https://velog.velcdn.com/images/aaaram__/post/7ee5fa57-d649-4f28-9b40-5fb8610f9b65/image.gif?" width=600>
]]></description>
        </item>
        <item>
            <title><![CDATA[ActivityResult]]></title>
            <link>https://velog.io/@aaaram__/ActivityResult</link>
            <guid>https://velog.io/@aaaram__/ActivityResult</guid>
            <pubDate>Thu, 07 Jul 2022 09:27:18 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요">1. 개요</h2>
<p>appCompat 최신 버전에서 <code>onActivityResult</code> 가 deprecate 되어
Compose 에서 <code>rememberLauncherForActivityResult()</code> 라는 메소드를 사용하게 되었다.</p>
<h2 id="2-사용법">2. 사용법</h2>
<p>Compose 에서 <code>rememberLauncherForActivityResult()</code>
Kotlin 에서 <code>registerForActivityResult()</code>
Composable과 Activity에서 각각 다른 메소드를 쓸 뿐, 역할은 같다.
MainActivity 에서 SigninActivity 의 intent 값을 가져와 resultCode가 &quot;OK&quot; 일 때 로직을 실행하는 예시이다.</p>
<h3 id="mainactivitykt">MainActivity.kt</h3>
<pre><code class="language-kotlin">override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            CloudhospitalTheme {
                MainContent(authorizationViewModel) {
                    signIn()
                }
            }
        }
}

fun signIn() {
    signInResult.launch(Intent(this, SigninActivity::class.java))
}


private val signInResult = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result -&gt;
        if (result.resultCode == RESULT_OK) {
            result.data.let { data -&gt;
                if (data != null) {

                    // TODO
                }
            }
        }
    }</code></pre>
<h3 id="mypage">Mypage()</h3>
<pre><code class="language-kotlin">@Composable
fun MyPage(
    signIn: () -&gt; Unit
) {
    Button(onClick = { signIn() }) {
        Text(&quot;Login&quot;)
    }
}</code></pre>
<p>SigninActivity를 MainActivity가 아닌 상속된 Composable에서 launch 하기 위해 작성된 코드이다.
MyPage Composable에서 버튼 클릭시 signInActivity를 launch 한다.</p>
<h3 id="signinactivitykt">SigninActivity.kt</h3>
<pre><code class="language-kotlin"> authenticationViewModel.signedIn.observe(this, { signedIn -&gt;
            if (signedIn == true){
                val returnIntent = Intent()
                returnIntent.putExtra(&quot;signInResult&quot;, signedIn)
                setResult(Activity.RESULT_OK, returnIntent)

                finish()
            }
        })</code></pre>
<h2 id="3-주의-사항">3. 주의 사항</h2>
<p>컴포저블 내에서 요청을 시작하려고 하면 그 시점에 ActivityResultLauncher가 아직 초기화되지 않았기 때문에 런타임 오류가 발생합니다. 구성 이후 실행을 트리거해야 하는 경우 LaunchedEffect 블록 또는 DisposableEffect 블록에서 launch() 메서드를 호출해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Material Date Picker setting min, max date]]></title>
            <link>https://velog.io/@aaaram__/Android-Material-Date-Picker-setting-min-max-date</link>
            <guid>https://velog.io/@aaaram__/Android-Material-Date-Picker-setting-min-max-date</guid>
            <pubDate>Thu, 07 Jul 2022 09:12:08 GMT</pubDate>
            <description><![CDATA[<h2 id="calendarconstraints">CalendarConstraints</h2>
<p>MaterialDatePicker 의 setCalendarConstraints() 메소드</p>
<pre><code class="language-kotlin">MaterialDatePicker.Builder.datePicker().setCalendarConstraints([CalendarConstraints])</code></pre>
<p>CalendarConstraints 의 setValidator() 메소드를 사용한다.</p>
<pre><code class="language-kotlin">CalendarConstrains.Builder().setValidator([DateValidator]).build()</code></pre>
<h2 id="set-max-date">Set max date</h2>
<p><code>DateValidatorPointBackward</code></p>
<pre><code class="language-kotlin">CalendarConstraints.Builder()
    .setValidator(DateValidatorPointBackward.now()).build()</code></pre>
<h2 id="set-min-date">Set min date</h2>
<p><code>DateValidatorPointForward</code></p>
<pre><code class="language-kotlin">CalendarConstraints.Builder()
            .setValidator(DateValidatorPointForward.now()).build()</code></pre>
<h2 id="set-max-month">Set max month</h2>
<p>달력의 최대값을(month 단위) 설정해 달력이 다음 달로 넘어가지 않도록 해준다.
<code>CalendarConstraints.Builder.setEnd()</code></p>
<pre><code class="language-kotlin">val calendar = Calendar.getInstance()
val presentMonth = calendar.timeInMillis

CalendarConstraints.Builder().setEnd(presentMonth)</code></pre>
<h2 id="set-min-month">Set min month</h2>
<p>달력의 최소값을(month 단위) 설정해 달력이 이전 달로 넘어가지 않도록 해준다.
<code>CalendarConstraints.Builder.setStart()</code></p>
<pre><code class="language-kotlin">val calendar = Calendar.getInstance()
val presentMonth = calendar.timeInMillis

CalendarConstraints.Builder().setStart(presentMonth)</code></pre>
<h2 id="최종-코드">최종 코드</h2>
<pre><code class="language-kotlin">private fun showDatePicker(
    activity : AppCompatActivity,
    date: LocalDateTime?,
    isBirthDay: Boolean,
    updatedDate: (LocalDateTime) -&gt; Unit)
{
    val selection = date?.atZone(ZoneId.of(&quot;UTC&quot;))
    val selectionMs = selection?.toInstant()?.toEpochMilli()

    val calendar = Calendar.getInstance()
    val presentMonth = calendar.timeInMillis
    val upTo = calendar.timeInMillis.plus(172800000) //min date 설정. 오늘포함 3일 이후로 날짜 선택 가능

    val constraints: CalendarConstraints = if (isBirthDay) {
        // set max month and date
        CalendarConstraints.Builder().setEnd(presentMonth)
            .setValidator(DateValidatorPointBackward.now()).build()
    } else {
        // set min month and date
        CalendarConstraints.Builder().setStart(presentMonth)
            .setValidator(DateValidatorPointForward.from(upTo)).build()
    }

    val picker = MaterialDatePicker.Builder.datePicker()
        .setCalendarConstraints(constraints)
        .setSelection(selectionMs)
        .build()
    picker.show(activity.supportFragmentManager, &quot;picker&quot;)
    picker.addOnPositiveButtonClickListener {
        val date = LocalDateTime.ofInstant(
            Instant.ofEpochMilli(it), ZoneId.of(&quot;UTC&quot;)
        )
        updatedDate(date)
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : Coil Async Image Load Library]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-Coil-Async-Image-Load-Library</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-Coil-Async-Image-Load-Library</guid>
            <pubDate>Sat, 30 Oct 2021 05:50:11 GMT</pubDate>
            <description><![CDATA[<h2 id="implement-library">Implement Library</h2>
<p><code>implementation(&quot;com.google.accompanist:accompanist-coil:0.15.0&quot;)</code></p>
<h2 id="사용하기">사용하기</h2>
<p><code>rememberImagePainter()</code></p>
<p><img src="https://images.velog.io/images/aaaram__/post/999d9760-a1a9-414e-99bb-ac4180070ed0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : Tab Layout + View Pager]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-Tab-Layout-View-Pager</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-Tab-Layout-View-Pager</guid>
            <pubDate>Sat, 30 Oct 2021 04:38:23 GMT</pubDate>
            <description><![CDATA[<h2 id="implement-library">Implement Library</h2>
<p><code>implementation &quot;com.google.accompanist:accompanist-pager:0.12.0&quot;</code></p>
<h2 id="tab-layout--view-pager">Tab Layout + View Pager</h2>
<pre><code class="language-kotlin">@OptIn(ExperimentalPagerApi::class)
@Composable
fun Procedures(slug: String){
    val tabData = listOf(
        Procedure.treatment.value,
        Procedure.diagnostic.value
    )
    val pagerState = rememberPagerState(
        pageCount = tabData.size,
        initialOffscreenLimit = 2,
        infiniteLoop = true,
        initialPage = 0,
    )
    val tabIndex = pagerState.currentPage
    val coroutineScope = rememberCoroutineScope()

    // TAB
    TabRow(selectedTabIndex = tabIndex,
        modifier = Modifier.padding(top = 20.dp)) {
        tabData.forEachIndexed { index, text -&gt;
            Tab(selected = tabIndex == index, onClick = {
                coroutineScope.launch {
                    pagerState.animateScrollToPage(index)
                }
            }, text = {
                Text(text = text)
            })
        }
    }

    // PAGER
    HorizontalPager(
        state = pagerState
    ) { index -&gt;
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            if (tabData[index] == Procedure.treatment.value) {
                Services(Procedure.treatment, slug)
            } else {
                Services(Procedure.diagnostic, slug)
            }
        }
    }
}</code></pre>
<pre><code class="language-kotlin">@Composable
fun Services(procedure: Procedure, slug: String) {
    val viewModel: ProcedureVM = viewModel()
    val services = viewModel.services.observeAsState().value
    val loadingState = viewModel.loading.observeAsState().value

    LaunchedEffect(&quot;GET_HOSPITALS_SERVICES&quot;) {
        viewModel.fetchHospitalService(procedure, slug, 20)
    }

    if (loadingState == true) {
        LoadingBar()
    } else {
        services?.items?.let { services -&gt;
            Column(Modifier.fillMaxWidth()) {
                services.forEach { service -&gt;
                    Row(horizontalArrangement = Arrangement.SpaceBetween,
                        verticalAlignment = Alignment.CenterVertically,
                        modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp)
                    ) {
                        Text(text = service.name!!, modifier = Modifier.width(200.dp), maxLines = 2)
                        Text(text = price(service), color = MaterialTheme.colors.primaryVariant)
                    }
                }
            }
        }
    }
}</code></pre>
<p><img src="https://images.velog.io/images/aaaram__/post/314e171a-dc8d-4910-a33c-fc1a4b25684c/image.png" alt=""></p>
<p><strong>SpaceBetween</strong>
잠깐 팁.
<code>Row()</code>에서 <code>horizontalArrangement</code> 를 적용하기 위해서는 부모의 width 값이  <code>fillMaxWidth()</code> 여야 한다.</p>
<p><strong>참고</strong>
<a href="https://levelup.gitconnected.com/implement-tablayout-with-viewpager-in-android-jetpack-compose-d509fc6e2d8e">https://levelup.gitconnected.com/implement-tablayout-with-viewpager-in-android-jetpack-compose-d509fc6e2d8e</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : remember, mutableStateOf]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-remember-mutableStateOf</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-remember-mutableStateOf</guid>
            <pubDate>Sat, 30 Oct 2021 04:06:28 GMT</pubDate>
            <description><![CDATA[<h2 id="remember">remember</h2>
<p>메모리에 로컬 상태를 저장한다</p>
<h2 id="mutablestateof">mutableStateOf()</h2>
<p>mutableStateOf() 에 전달된 값이 업데이트 될 때마다 이 state를 사용하는 컴포저블과 하위 요소는 재구성(recomposition) 한다.</p>
<h2 id="활용하기">활용하기</h2>
<p>장문의 텍스트를 ellipsis로 숨겼다가 보여주는 토글 버튼 만들기.
boolean 값의 mutableState로 See More / See Less 토글 텍스트 컴포넌트를 만들었다.</p>
<pre><code class="language-kotlin">
@Composable
fun ToggleText(string: String) {
    var seeMore by remember { mutableStateOf(true) }

    Text(
        text = string,
        style = MaterialTheme.typography.body2,
        maxLines = if (seeMore) 5 else Int.MAX_VALUE,
        overflow = TextOverflow.Ellipsis
    )
    val textButton = if (seeMore) {
        stringResource(id = R.string.see_more)
    } else {
        stringResource(id = R.string.see_less)
    }
    Text(
        text = textButton,
        style = MaterialTheme.typography.button,
        textAlign = TextAlign.Center,
        color = MaterialTheme.colors.primaryVariant,
        modifier = Modifier
            .heightIn(20.dp)
            .fillMaxWidth()
            .padding(top = 15.dp)
            .clickable {
                seeMore = !seeMore
            }
    )
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : Navigation]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-Navigation</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-Navigation</guid>
            <pubDate>Sat, 30 Oct 2021 03:58:53 GMT</pubDate>
            <description><![CDATA[<h2 id="library-적용">Library 적용</h2>
<pre><code class="language-kotlin">implementation &#39;androidx.navigation:navigation-compose:2.4.0-alpha08&#39;</code></pre>
<h2 id="사용하기">사용하기</h2>
<h3 id="1-navhost">1. NavHost</h3>
<p><code>composable()</code> 메소드로 시작 경로와 라우트를 정의한다.</p>
<pre><code class="language-kotlin">val navController = rememberNavController()

NavHost(navController = navController, startDestination = &quot;home&quot;) {
    composable(route = &quot;home&quot;) { Home() }
    composable(route = &quot;search&quot;) { Search() }
}
</code></pre>
<h3 id="2-button-link">2. Button Link</h3>
<pre><code class="language-kotlin">@Composable
fun Home(navController: NavController) {
    Button(onClick = { navController.navigate(&quot;search&quot;) }) {
        Text(text = &quot;Click&quot;)
    } 
}</code></pre>
<h3 id="3-with-argument">3. With Argument</h3>
<p>&#39;slug&#39; 라는 string 타입의 argument 를 사용하여 HomeDetail로 이동하는 예제</p>
<pre><code class="language-kotlin">NavHost(startDestination = &quot;home/{slug}&quot;) {
    composable(
        &quot;home/{slug}&quot;,
        arguments = listOf(navArgument(&quot;slug&quot;) { type = NavType.StringType })
    ) { backStackEntry -&gt;
        val slug = requireNotNull(backStackEntry.arguments).getString(&quot;slug&quot;)
        HomeDetail(navController, slug)
    }
}</code></pre>
<h3 id="4-nested-navigation">4. Nested Navigation</h3>
<p><code>BottomBar()</code> 의 네비게이션과 상세 화면으로 이동하기 위한 예제.
네비게이션 규모가 커질수록 <code>NavGraphBuilder()</code> 로 중첩 네비게이션을 쓰는게 좋다.</p>
<p><strong>sealed class 만들기</strong></p>
<pre><code class="language-kotlin">sealed class NavItem(
    @StringRes val title: Int,
    val icon: ImageVector,
    val route: String
){
    object Hospital : NavItem(R.string.nav_hospital, Icons.Filled.LocalHospital, &quot;hospital&quot;)
    object Article : NavItem(R.string.nav_article, Icons.Filled.Article, &quot;article&quot;)
}</code></pre>
<p><strong>addHomeGraph</strong></p>
<pre><code class="language-kotlin">fun NavGraphBuilder.addHomeGraph(
    navController: NavHostController,
    onClickHospital: (String, NavBackStackEntry) -&gt; Unit,
    onClickArticle: (String, NavBackStackEntry) -&gt; Unit
) {
    composable(NavItem.Hospital.route) {
        Hospitals(navController, onClick = { slug -&gt;
            onClickHospital(slug, it)
        })
    }
    composable(NavItem.Article.route) {
        Article(navController, onClick = { slug -&gt;
            onClickArticle(slug, it)
        })
    }
}</code></pre>
<p><strong>mainNavGraph</strong></p>
<pre><code class="language-kotlin">fun NavGraphBuilder.mainNavGraph(
    navController: NavHostController,
    onClickHospital: (String, NavBackStackEntry) -&gt; Unit,
    onClickArticle: (String, NavBackStackEntry) -&gt; Unit
) {
    navigation(
        route = &quot;home&quot;,
        startDestination = NavItem.Hospital.route
    ) {
        addHomeGraph(navController, onClickHospital, onClickDoctor, onClickDeal, onClickArticle)
    }

    composable(
        &quot;hospital_detail/{slug}&quot;,
        arguments = listOf(navArgument(&quot;slug&quot;) { type = NavType.StringType })
    ) { backStackEntry -&gt;
        val slug = requireNotNull(backStackEntry.arguments).getString(&quot;slug&quot;)
        HospitalDetail(navController, slug)
    }

    composable(
        &quot;article_detail/{slug}&quot;,
        arguments = listOf(navArgument(&quot;slug&quot;) { type = NavType.StringType })
    ) { backStackEntry -&gt;
        val slug = requireNotNull(backStackEntry.arguments).getString(&quot;slug&quot;)
        ArticleDetail(navController, slug)
    }
}</code></pre>
<p><strong>View</strong></p>
<pre><code class="language-kotlin">@Composable
fun MainContent() {
    val navController = rememberNavController()
    val navBackStackEntry by appState.navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route?.substringBeforeLast(&quot;/&quot;)

    Scaffold(
        topBar = { TopBar(navController) },
        bottomBar = { BottomBar(navController) },
        floatingActionButton = { FloatingChatButton() }
    ) {
        NavHost(navController = navController, startDestination = &quot;home&quot;) {
            mainNavGraph(
                navController = navController,
                onClickHospital = {navController.navigate(&quot;hospital_detail/$slug&quot;)},
                onClickArticle = {navController.navigate(&quot;article_detail/$slug&quot;)}
            )
        }
    }
}
</code></pre>
<p><strong>Detail Page</strong></p>
<pre><code class="language-kotlin">@Composable
fun HospitalDetail(
    navController: NavHostController, slug: String
) {
    val viewModel: HospitalVM = viewModel()
    val hospital = viewModel.hospital.observeAsState().value
    val loadingState = viewModel.loading.observeAsState().value

    LaunchedEffect(key1 = &quot;GET_HOSPITAL_DETAIL&quot;) {
        slug?.let {
            viewModel.fetchHospitalItem(slug)
        }
    }

    if (loadingState == true) {
        LoadingBar()
    } else {
        hospital?.let {
            HospitalDetailView(hospital)
        }
    }
}</code></pre>
<p><strong>참고</strong>
<a href="https://developer.android.com/jetpack/compose/navigation#bottom-nav">https://developer.android.com/jetpack/compose/navigation#bottom-nav</a> 
<a href="https://github.com/android/compose-samples/tree/main/Jetsnack">https://github.com/android/compose-samples/tree/main/Jetsnack</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jetpack Compose : LiveData, Flow]]></title>
            <link>https://velog.io/@aaaram__/Jetpack-Compose-LiveData</link>
            <guid>https://velog.io/@aaaram__/Jetpack-Compose-LiveData</guid>
            <pubDate>Sat, 30 Oct 2021 03:25:00 GMT</pubDate>
            <description><![CDATA[<h1 id="livedata">LiveData</h1>
<h2 id="library-적용">Library 적용</h2>
<p><code>build.gradle[app]</code> 에 runtime-livedata 추가</p>
<pre><code class="language-gradle">implementation &quot;androidx.compose.runtime:runtime-livedata:$compose_version&quot;</code></pre>
<h2 id="사용하기">사용하기</h2>
<h3 id="viewmodel">ViewModel</h3>
<pre><code class="language-kotlin">private val _myModels = MutableLiveData&lt;MyViewModel&gt;()
val myModels: LiveData&lt;MyViewModel&gt; = _deal</code></pre>
<h3 id="view">View</h3>
<p><code>observableAsState()</code> 로 state 를 관찰하여 가져온다.</p>
<pre><code class="language-kotlin">private val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        ComposableTheme {
            val myModels = viewModel.myModels.obverseAsState().value ?: emptyList()
            val clickedModel = viewModel.clickedModel.observeAsState().value
        }
    }
}</code></pre>
<h1 id="flow">Flow</h1>
<h2 id="사용하기-1">사용하기</h2>
<h3 id="viewmodel-1">ViewModel</h3>
<pre><code class="language-kotlin">val pager: Flow&lt;PagingData&lt;MyDocument&gt;&gt; = Pager(PagingConfig(pageSize = 20)) {
    MySource(MyRepository())
}.flow.cachedIn(viewModelScope)</code></pre>
<blockquote>
<p>스크롤의 상태를 저장하고 싶다면 <code>cachedIn()</code> 을 쓴다.</p>
</blockquote>
<h3 id="view-1">View</h3>
<p>loadState에 따라 화면을 다르게 구현한다.</p>
<pre><code class="language-kotlin">import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(myViewModel: MyViewModel) {
    val pager = myViewModel.pager
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
          items = lazyPagingItems,
          // The key is important so the Lazy list can remember your
          // scroll position when more items are fetched!
          key = { message -&gt; message.id }
        ) { message -&gt;
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }

        pager.apply {
            loadState.refresh is LoadState.Loading -&gt; {
                item { LoadingBar() }
            }
            loadState.append is LoadState.Loading -&gt; {
                item { LoadingItem() }
            }
            loadState.refresh is LoadState.Error -&gt; {
                val e = pager.loadState.refresh as LoadState.Error
                item {
                    ErrorItem(
                        message = e.error.localizedMessage ?: &quot;Error!&quot;,
                        modifier = Modifier.fillParentMaxSize(),
                        onClickRetry = { retry() }
                    )
                }
            }
            loadState.append is LoadState.Error -&gt; {
                val e = pager.loadState.append as LoadState.Error
                item {
                    ErrorItem(
                        message = e.error.localizedMessage ?: &quot;Error!&quot;,
                        onClickRetry = { retry() }
                    )
                }
            }
            loadState.refresh is LoadState.NotLoading &amp;&amp; loadState.append.endOfPaginationReached -&gt; {
                if (this.itemCount == 0) {
                    item {
                        NoLoadData()
                    }
                }
            }
        }
    }
}</code></pre>
<h1 id="마무리">마무리</h1>
<p>여러 아티클을 읽다보면 안드로이드에 종속적인 liveData 보다, 코틀린 순수 라이브러리인 stateFlow가 더 좋다고 말하고 있다. liveData가 장기적으로 봤을 때 deprecated 되는 것이 아니냐는 소문까지 돌고 있다고..</p>
<p>우선 이런 의견이 있구나, 참고만 하자.</p>
]]></description>
        </item>
    </channel>
</rss>