<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.kmshack.kr/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.kmshack.kr/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-07-31T14:34:24+09:00</updated><id>https://blog.kmshack.kr/feed.xml</id><title type="html">꿈꾸는 개발자의 로그</title><subtitle>안드로이드 앱 개발, UI/UX, 생산성과 비즈니스 인사이트를 기록하는 기술 블로그입니다.</subtitle><author><name>kmshack</name></author><entry><title type="html">AI 시대, 당신의 통장 잔고가 그대로인 진짜 이유</title><link href="https://blog.kmshack.kr/ai-leverage/" rel="alternate" type="text/html" title="AI 시대, 당신의 통장 잔고가 그대로인 진짜 이유" /><published>2026-07-31T00:00:00+09:00</published><updated>2026-07-31T00:00:00+09:00</updated><id>https://blog.kmshack.kr/ai-leverage</id><content type="html" xml:base="https://blog.kmshack.kr/ai-leverage/"><![CDATA[<p>AI가 비즈니스의 모든 것을 바꿀 것처럼 이야기됩니다. 자료를 정리하고, 문서를 만들고, 프로그램을 작성하는 속도는 이미 눈에 띄게 빨라졌습니다. 예전에는 하루가 걸리던 일도 이제 몇 시간이면 끝낼 수 있습니다.</p>

<p>그런데 이상한 점이 있습니다. <strong>처리하는 일은 많아졌는데 매출은 기대만큼 늘어나지 않습니다.</strong> 생산성이 높아졌다면 사업의 결과도 함께 좋아져야 할 것 같지만, 현실은 꼭 그렇지 않습니다.</p>

<p>사업가 알렉스 호르모지가 이야기한 레버리지의 관점에서 보면 이유는 단순합니다. AI는 일을 빠르게 만들어 주지만, 어떤 일을 해야 하는지까지 결정해 주지는 않기 때문입니다.</p>

<p><br />
<img src="/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" alt="AI로 빨라진 업무가 병목을 지나 사업 성과로 연결되는 모습" class="center-image" />
<br /></p>

<blockquote>
  <p>AI가 속도를 높여도 사업의 병목이 그대로라면 결과는 달라지지 않습니다.</p>
</blockquote>

<p><br /></p>

<h2 id="핵심-요약">핵심 요약</h2>

<ul>
  <li>AI는 일을 빠르게 처리하지만, 어떤 일을 해야 하는지 결정하지는 못합니다.</li>
  <li>매출을 높이려면 도구보다 먼저 판매와 서비스 제공 구조를 점검해야 합니다.</li>
  <li>가장 강력한 레버리지는 중요하지 않은 일을 멈추고 하나의 병목에 집중하는 의사결정입니다.</li>
</ul>

<p><br /></p>

<h2 id="1-생산성이-높아졌는데-왜-매출은-그대로일까">1. 생산성이 높아졌는데 왜 매출은 그대로일까?</h2>

<p>레버리지란 <strong>적은 노력으로 더 큰 결과를 만들어 내는 힘</strong>을 뜻합니다. 그런 의미에서 AI는 분명 강력한 레버리지입니다. 반복 업무를 줄이고, 자료 조사와 초안 작성에 필요한 시간을 크게 단축해 줍니다.</p>

<p>문제는 남는 시간을 어디에 사용하느냐입니다.</p>

<p>업무 처리 능력이 늘어나면 예전에는 시간과 인력이 부족해 포기했던 일까지 다시 시작하게 됩니다. 매출에 직접적인 영향을 주지 않는 보고서를 더 만들거나, 고객이 요청하지 않은 기능을 추가하고, 중요하지 않은 회의를 더 자주 여는 식입니다.</p>

<p>중요하지 않은 일을 아무리 빠르게 처리해도 사업의 결과는 달라지지 않습니다. 오히려 해야 할 일이 많아졌다는 착각만 만들 수 있습니다.</p>

<p><strong>AI는 속도를 높여 주는 도구이지, 방향을 정해 주는 도구가 아닙니다.</strong> 잘못된 방향으로 달리고 있다면 더 빠른 속도는 목적지에서 더 멀어지게 만들 뿐입니다.</p>

<p><br /></p>

<h2 id="2-기술-없이도-수익을-높이는-구조의-힘">2. 기술 없이도 수익을 높이는 구조의 힘</h2>

<p>사업의 효율을 높이기 위해 반드시 새로운 기술이 필요한 것은 아닙니다. 때로는 판매 구조나 서비스를 제공하는 방식만 바꿔도 훨씬 큰 레버리지가 만들어집니다.</p>

<h3 id="11에서-그룹-방식으로-바꾸기">1:1에서 그룹 방식으로 바꾸기</h3>

<p>한 시간 동안 한 명을 상담하던 사람이 같은 시간에 10명을 상담할 수 있다면, 단순 계산만으로 제공 효율은 10배 가까이 높아질 수 있습니다. 새로운 프로그램을 개발하지 않아도 서비스 구조를 바꾸는 것만으로 시간의 한계를 줄일 수 있습니다.</p>

<h3 id="비동기-방식-도입하기">비동기 방식 도입하기</h3>

<p>모든 상담을 정해진 시간에 실시간으로 진행할 필요는 없습니다. 고객이 질문을 남기고 담당자가 가능한 시간에 답변하는 방식을 도입하면 일정의 제약이 줄어듭니다. 고객은 기다리는 시간을 줄이고, 담당자는 비슷한 질문을 모아 더 효율적으로 처리할 수 있습니다.</p>

<h3 id="영업-전에-충분한-정보-제공하기">영업 전에 충분한 정보 제공하기</h3>

<p>가격, 서비스 방식, 자주 묻는 질문을 상담 전에 충분히 전달하면 영업 과정도 달라집니다. 고객은 자신에게 필요한 서비스인지 미리 판단할 수 있고, 영업 담당자는 구매 의사가 높은 고객과 더 깊은 대화를 나눌 수 있습니다.</p>

<p>이처럼 레버리지는 새로운 도구를 도입하는 것뿐만 아니라 <strong>기존의 일을 어떤 구조로 제공하느냐</strong>에 따라 달라집니다.</p>

<p><br /></p>

<h2 id="3-가장-강력한-레버리지는-의사결정입니다">3. 가장 강력한 레버리지는 의사결정입니다</h2>

<p>자동화보다 더 큰 효과를 만드는 것은 <strong>중요하지 않은 일을 하지 않기로 결정하는 것</strong>입니다.</p>

<p>잘못된 결정을 내린 상태에서 AI를 사용하면 실수도 더 빠르게 확대됩니다. 필요 없는 기능을 더 빨리 만들고, 효과 없는 광고 문구를 더 많이 생산하고, 읽히지 않는 보고서를 더 정교하게 작성하게 됩니다.</p>

<p>사업가에게 필요한 능력은 더 많은 일을 벌이는 것이 아닙니다. 지금 하지 않아도 되는 일을 제거하고, 가장 중요한 문제 하나를 선택하는 능력입니다.</p>

<p>한 번의 올바른 판단은 여러 사람의 업무 방향을 바꾸고 수백 시간의 낭비를 막아 줍니다. 반대로 잘못된 우선순위는 아무리 좋은 도구를 사용해도 만회하기 어렵습니다.</p>

<p><br /></p>

<h2 id="4-우리-사업의-진짜-병목을-찾는-방법">4. 우리 사업의 진짜 병목을 찾는 방법</h2>

<p>사업을 성장시키려면 현재 가장 막혀 있는 지점, 즉 <strong>병목(Bottleneck)</strong>을 먼저 찾아야 합니다. 사업의 흐름은 크게 세 단계로 나눌 수 있습니다.</p>

<ul>
  <li><strong>수요 창출:</strong> 상품을 알리고 잠재 고객을 유입시키는 단계</li>
  <li><strong>전환:</strong> 관심을 보인 고객이 실제 구매를 결정하는 단계</li>
  <li><strong>제공:</strong> 구매한 고객에게 기대한 결과를 전달하는 단계</li>
</ul>

<p>어느 단계가 병목인지에 따라 해결해야 할 문제도 달라집니다.</p>

<ul>
  <li>신규 고객 유입이 부족하다면 메시지와 유입 채널을 점검해야 합니다.</li>
  <li>문의는 많지만 구매로 이어지지 않는다면 제안, 가격, 신뢰 요소를 개선해야 합니다.</li>
  <li>구매는 발생하지만 환불과 불만이 많다면 서비스 품질과 제공 과정을 살펴봐야 합니다.</li>
</ul>

<p>신규 고객 유입이 문제인데 AI로 내부 보고서 시스템만 개선한다면 매출은 늘어나지 않습니다. 문의는 충분한데 구매가 일어나지 않는다면 자동화보다 제안을 먼저 개선해야 합니다.</p>

<p>새로운 AI 도구를 도입하기 전에 다음 질문부터 확인해 보는 것이 좋습니다.</p>

<ol>
  <li>지금 사업의 성장을 가장 크게 막고 있는 숫자는 무엇인가?</li>
  <li>그 숫자를 개선하면 실제 매출이나 이익이 달라지는가?</li>
  <li>최근 데이터와 고객의 반응이 같은 문제를 가리키고 있는가?</li>
  <li>이 문제와 관계없는 업무 중 당장 멈출 수 있는 것은 무엇인가?</li>
</ol>

<p>이 질문에 답할 수 있다면 AI를 어디에 사용해야 할지도 자연스럽게 분명해집니다.</p>

<p><br /></p>

<h2 id="결론-ai는-도구이고-본질은-판단력입니다">결론: AI는 도구이고, 본질은 판단력입니다</h2>

<p>모든 사업의 시간, 사람, 자본은 한정되어 있습니다. 사업가의 실력은 이 자원을 얼마나 많은 일에 나누느냐가 아니라, 가장 큰 결과를 만들 <strong>단 하나의 문제에 얼마나 집중하느냐</strong>에서 드러납니다.</p>

<p>AI는 그 문제를 해결하는 훌륭한 가속 장치가 될 수 있습니다. 하지만 무엇이 문제인지 찾아내는 일까지 대신해 주지는 않습니다.</p>

<p>AI를 더 많이 사용하는 방법을 고민하기 전에 먼저 물어봐야 합니다.</p>

<p><strong>지금 우리 사업의 성장을 막고 있는 단 하나의 병목은 무엇인가?</strong></p>

<p>그 답을 찾는 순간, AI는 유행하는 도구가 아니라 실제 성과를 만드는 레버리지가 됩니다.</p>

<p><br /></p>

<h2 id="자주-묻는-질문">자주 묻는 질문</h2>

<h3 id="ai를-도입하면-매출이-자동으로-늘어나나요">AI를 도입하면 매출이 자동으로 늘어나나요?</h3>

<p>아닙니다. AI는 업무 속도를 높이는 도구일 뿐입니다. 매출을 막고 있는 병목과 우선순위를 먼저 해결해야 AI의 생산성이 실제 성과로 이어집니다.</p>

<h3 id="ai보다-먼저-확인해야-할-것은-무엇인가요">AI보다 먼저 확인해야 할 것은 무엇인가요?</h3>

<p>수요 창출, 전환, 제공 중 어느 단계가 사업 성장을 막고 있는지 확인해야 합니다. 병목이 신규 고객 유입이라면 내부 자동화보다 메시지와 유입 채널을 먼저 개선해야 합니다.</p>

<h3 id="기술-없이도-레버리지를-높일-수-있나요">기술 없이도 레버리지를 높일 수 있나요?</h3>

<p>가능합니다. 그룹 상담, 비동기 응답, 상담 전 정보 제공처럼 서비스 구조를 바꾸는 것만으로도 같은 시간에 더 많은 고객에게 가치를 제공할 수 있습니다.</p>]]></content><author><name>kmshack</name></author><category term="AI" /><category term="비즈니스" /><category term="생산성" /><summary type="html"><![CDATA[AI로 생산성이 높아져도 매출이 늘지 않는 이유와 의사결정, 사업 구조, 병목을 통해 진짜 레버리지를 찾는 방법을 정리합니다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.kmshack.kr/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" /><media:content medium="image" url="https://blog.kmshack.kr/images/2026-07-31-ai-leverage/ai-business-bottleneck.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AppUpdateManager를 이용한 앱 업데이트 처리</title><link href="https://blog.kmshack.kr/AppUpdateManager/" rel="alternate" type="text/html" title="AppUpdateManager를 이용한 앱 업데이트 처리" /><published>2019-06-24T00:00:00+09:00</published><updated>2019-06-24T00:00:00+09:00</updated><id>https://blog.kmshack.kr/AppUpdateManager</id><content type="html" xml:base="https://blog.kmshack.kr/AppUpdateManager/"><![CDATA[<p>어느 정도 규모가 있는 앱에는 사용자에게 최신버전의 업데이트를 알려주는 기능과 강제 업데이트 기능이 대부분 포함되어 있습니다. 강제 업데이트의 경우 중요한 문제로 인해 더이상 해당 버전을 사용하지못하고 최신버전을 받아 사용하기 원하는 경우에 사용됩니다. 대부분 Backend를 통해 현재 앱버전을 체크하여 업데이트해야 하는 상황에 따라 사용자에게 다이얼로그를 노출시키고 플레이스토어로 이동하는 방법을 사용합니다. 플레이스토어로 이동 후 사용자는 업데이트 버튼을 눌러 업데이트가 끝난뒤 다시 앱으로 돌아오게 됩니다.</p>

<p><br /></p>

<h3 id="문제점">문제점</h3>
<p>사용자는 플레이스토어로 이동하여 수동으로 업데이트 버튼을 눌러야하며, 업데이트가 끝날때 까지 기다려야 합니다. 업데이트시간이 오래걸리게 되면 이탈하게 되어 업데이트하고 있다는 사실을 인지하지 못하고 업데이트된 후 앱을 재 실행하지 않게 될 수 도 있습니다. 또한 베타/알파 테스터나 앱 출시를 단계적으로 배포 하게 될경우 해당 사용자는 플레이스토어로 이동해도 최신버전의 앱으로 업데이트 받지 못할 수도 있습니다.</p>

<p><br /></p>

<h2 id="appupdatemanager">AppUpdateManager</h2>
<p>구글은 이런 오래된 업데이트 처리방식에 대해 지원 하기위해 <code class="language-plaintext highlighter-rouge">AppUpdateManager</code>를 라이브러리를 통해 추가하였습니다. AppUpdateManager를 이용하면 실제 업데이트 해야할지 질의할 수 있으며, 플레이스토어로 이동할 필요 없이 자연스러운 앱 업데이트를 처리 할 수 있습니다.</p>

<h3 id="dependency">dependency</h3>
<p>build.gradle에 아래와 같이 종속성을 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>implementation 'com.google.android.play:core:1.6.1’
</code></pre></div></div>

<p><br /></p>

<h2 id="업데이트-체크하기">업데이트 체크하기</h2>
<p>AppUpdateManager를 통해 <code class="language-plaintext highlighter-rouge">appUpdateInfo</code>로 현재 업데이트 상태를 가져올 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">appUpdateManager</span> <span class="o">=</span> <span class="nc">AppUpdateManagerFactory</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="k">this</span><span class="o">)</span>
  
<span class="n">launch</span><span class="o">{</span>
  <span class="n">val</span> <span class="n">appUpdateInfo</span> <span class="o">=</span> <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">appUpdateInfo</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
  
  <span class="n">when</span><span class="o">(</span><span class="n">appUpdateInfo</span><span class="o">.</span><span class="na">updateAvailability</span><span class="o">()){</span>
      <span class="nc">UpdateAvailability</span><span class="o">.</span><span class="na">UPDATE_AVAILABLE</span> <span class="o">-&gt;{</span>
         <span class="c1">//업데이트 가능한 상태</span>
      <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><br /></p>

<p><code class="language-plaintext highlighter-rouge">appUpdateInfo</code>를 가져오기 위해 <code class="language-plaintext highlighter-rouge">addOnCompleteListener</code>를 통한 Callback구조를 suspendCoroutine를 이용해 suspend 함수로 간단하게 만들수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nc">Task</span><span class="o">&lt;</span><span class="nc">AppUpdateInfo</span><span class="o">&gt;.</span><span class="na">await</span><span class="o">():</span> <span class="nc">AppUpdateInfo</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">suspendCoroutine</span> <span class="o">{</span> <span class="n">continuation</span> <span class="o">-&gt;</span>
        <span class="n">addOnCompleteListener</span> <span class="o">{</span> <span class="n">result</span> <span class="o">-&gt;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">isSuccessful</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">continuation</span><span class="o">.</span><span class="na">resume</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">result</span><span class="o">)</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">continuation</span><span class="o">.</span><span class="na">resumeWithException</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">exception</span><span class="o">)</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h3 id="updateavailability-4가지-상태">UpdateAvailability 4가지 상태</h3>
<ul>
  <li>DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS : AppUpdateType.IMMEDIATE 타입을 통해 업데이트를 수행중인 경우</li>
  <li>UNKNOWN: 알수 없음</li>
  <li>UPDATE_AVAILABLE: 현재 최신버전이 아니며 업데이트가 필요한 경우</li>
  <li>UPDATE_NOT_AVAILABLE: 현재 최신버전이며 업데이트가 필요 하지 않은 경우</li>
</ul>

<p><br /></p>

<h2 id="업데이트-수행하기">업데이트 수행하기</h2>
<p>업데이트가 필요한 상황을 인지 했다면, 이제 실제로 앱 업데이트 동작을 수행하게 해야 합니다. 플레이스토어로 이동하는 방법이 아닌 <code class="language-plaintext highlighter-rouge">AppUpdateManager.startUpdateFlowForResult()</code>를 이용하여 2가지 타입의 업데이트 방법을 사용자에게 제공해 줄 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">appUpdateManager</span><span class="o">.</span><span class="na">startUpdateFlowForResult</span><span class="o">(</span>
    <span class="n">appUpdateInfo</span><span class="o">,</span>
    <span class="nc">AppUpdateType</span><span class="o">.</span><span class="na">FLEXIBLE</span> <span class="o">,</span> <span class="c1">// or AppUpdateType.IMMEDIATE</span>
    <span class="n">activity</span><span class="o">,</span>
    <span class="no">REQUEST_CODE_UPDATE</span><span class="o">)</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="즉시-업데이트">즉시 업데이트</h2>
<p><code class="language-plaintext highlighter-rouge">AppUpdateType.IMMEDIATE</code> 타입을 사용하면 별도의 업데이트 UI를 표시하게 되며 사용자가 업데이트전 다른 작업을 하지 못하도록 블락시키게 됩니다. 강제 업데이트 해야 할 경우 적절합니다. 앱이 업데이트 되면 자동으로 어플리케이션이 자동으로 재시작됩니다. 업데이트가 완료 되는 경우 onActivityForResult()를 통해 결과를 확인 할 수 있습니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2019-06-24-AppUpdateManager/immediate.jpg" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>업데이트중에는 항상 업데이트 UI만 보여지게 하기위해서는 UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 상태로 처리하면 됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">override</span> <span class="n">fun</span> <span class="nf">onResume</span><span class="o">()</span> <span class="o">{</span>
    <span class="kd">super</span><span class="o">.</span><span class="na">onResume</span><span class="o">()</span>
    <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">appUpdateInfo</span>
        <span class="o">.</span><span class="na">addOnSuccessListener</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">updateAvailability</span><span class="o">()</span> <span class="o">==</span> <span class="nc">UpdateAvailability</span><span class="o">.</span><span class="na">DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">startUpdateFlowForResult</span><span class="o">(</span>
                    <span class="n">it</span><span class="o">,</span>
                    <span class="nc">AppUpdateType</span><span class="o">.</span><span class="na">IMMEDIATE</span><span class="o">,</span>
                    <span class="n">activity</span><span class="o">,</span>
                    <span class="no">REQUEST_CODE_UPDATE</span><span class="o">)</span>
            <span class="o">}</span>
        <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><br /></p>

<h2 id="자연스러운-업데이트">자연스러운 업데이트</h2>
<p><code class="language-plaintext highlighter-rouge">AppUpdateType.FLEXIBLE</code> 타입을 사용하는 경우 사용자에게 좀 더 자연스럽게 업데이트를 처리 할 수 있습니다. 사용자가 업데이트를 누르면 DownloadManager를 통해 업데이트할 앱을 백그라운드를 통해 다운로드 받으며, 다운로드 완료가 되면 사용자에게 별도의 UI를 통해 알리고 설치 할 수 있도록 처리합니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2019-06-24-AppUpdateManager/flexible.jpg" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p>다운로드 진행사항을 <code class="language-plaintext highlighter-rouge">InstallStateUpdatedListener</code>를 통해 Callback받을 수 있습니다. 다운로드 완료시 사용자에게 방해가 안될 정도의 UI를 표시하여 설치를 유도합니다. 여기서는 Snackbar를 이용하였습니다.   <code class="language-plaintext highlighter-rouge">appUpdateManager.completeUpdate()</code>를 호출하면 설치 UI로 넘어가게 되며 설치 완료시 자동으로 어플리케이션이 재실행됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">listener</span> <span class="o">=</span> <span class="nc">InstallStateUpdatedListener</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">installStatus</span><span class="o">()</span> <span class="o">==</span> <span class="nc">InstallStatus</span><span class="o">.</span><span class="na">DOWNLOADED</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Snackbar</span><span class="o">.</span><span class="na">make</span><span class="o">(</span><span class="n">coordinator_layout</span><span class="o">,</span> <span class="s">"업데이트 버전 다운로드 완료"</span><span class="o">,</span> <span class="nc">Snackbar</span><span class="o">.</span><span class="na">LENGTH_INDEFINITE</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setAction</span><span class="o">(</span><span class="s">"설치/재시작"</span><span class="o">,</span> <span class="nc">View</span><span class="o">.</span><span class="na">OnClickListener</span> <span class="o">{</span>
                    <span class="n">appUpdateManager</span><span class="o">.</span><span class="na">completeUpdate</span><span class="o">()</span>
                <span class="o">}).</span><span class="na">show</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">appUpdateManager</span><span class="o">.</span><span class="na">registerListener</span><span class="o">(</span><span class="n">listener</span><span class="o">)</span>
</code></pre></div></div>

<p><br /></p>

<p>사용가능한 새 버전이 있음에도 불구하고 특정 기기나 계정에서 새로운 버전을 사용할 수 없을 수 있습니다. 해당 업데이트가 실제 구글 플레이앱에서 제공되는지 여부를 확인 하는것이 중요합니다. <code class="language-plaintext highlighter-rouge">appUpdateInfo.isUpdateTypeAllowed()</code>를 이용하면 실제로 업데이트 가능한지 확인 할 수 있습니다.</p>

<p><br /></p>

<p>참고:<br />
<a href="https://developer.android.com/guide/app-bundle/in-app-updates">https://developer.android.com/guide/app-bundle/in-app-updates</a><br />
<a href="https://proandroiddev.com/theres-a-new-update-available-75a2c5bda76e">https://proandroiddev.com/theres-a-new-update-available-75a2c5bda76e</a></p>]]></content><author><name>kmshack</name></author><category term="플레이스토어" /><category term="안드로이드" /><summary type="html"><![CDATA[어느 정도 규모가 있는 앱에는 사용자에게 최신버전의 업데이트를 알려주는 기능과 강제 업데이트 기능이 대부분 포함되어 있습니다. 강제 업데이트의 경우 중요한 문제로 인해 더이상 해당 버전을 사용하지못하고 최신버전을 받아 사용하기 원하는 경우에 사용됩니다. 대부분 Backend를 통해 현재 앱버전을 체크하여 업데이트해야 하는 상황에 따라 사용자에게 다이얼로그를 노출시키고 플레이스토어로 이동하는 방법을 사용합니다. 플레이스토어로 이동 후 사용자는 업데이트 버튼을 눌러 업데이트가 끝난뒤 다시 앱으로 돌아오게 됩니다.]]></summary></entry><entry><title type="html">WorkManager로 정기적인 백그라운드 작업 수행하기</title><link href="https://blog.kmshack.kr/work_manager/" rel="alternate" type="text/html" title="WorkManager로 정기적인 백그라운드 작업 수행하기" /><published>2019-03-18T00:00:00+09:00</published><updated>2019-03-18T00:00:00+09:00</updated><id>https://blog.kmshack.kr/work_manager</id><content type="html" xml:base="https://blog.kmshack.kr/work_manager/"><![CDATA[<p>Android O 부터 긴 작업의 백그라운드 서비스와 브로드캐스트는 재기능을 하지 않습니다. 따라서 백그라운드 작업을 구현하기위해서는 <code class="language-plaintext highlighter-rouge">WorkManager</code>를 선택할 수 밖에 없습니다.</p>

<p><br /></p>

<p>WokrManager는 Android Jetpack의 일부로 1.0.0버전으로 얼마전 공개되었습니다. Google은 이미 <code class="language-plaintext highlighter-rouge">JobScheduler</code>, <code class="language-plaintext highlighter-rouge">Firebase JobDispatcher</code>와 같은 백그라운드 작업을 위한 라이브러리를 수차례 공개하였습니다. 또한 Everonet의 <code class="language-plaintext highlighter-rouge">Android Job</code>이 있습니다. WorkManager는 이미 공개된 라이브러리보다 많은 장점이 있습니다.</p>

<ul>
  <li>이전버전과의 호환성(API14이상 모두 지원)</li>
  <li>GooglePlay Service에 대한 의존성이 없음</li>
  <li>체인기반의 작업 관리</li>
  <li>작업 상태 쿼리가능</li>
</ul>

<p><br /></p>
<h2 id="이제-프로젝트에-적용해봅시다">이제 프로젝트에 적용해봅시다!</h2>

<p>항상 그렇듯 build.gradle에 종속성을 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>allprojects {
    repositories {
        google()
        jcenter()
    }
}
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies {
    def work_version = 1.0.0

    // (Java only)
    implementation "android.arch.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "android.arch.work:work-runtime-ktx:$work_version"

}
</code></pre></div></div>

<p><br />
백그라운드에서 일부 작업을 실행하는 예제를 보도록하겠습니다. 현재 좌표를 하루에 두번씩 서버로 전송하는 예제이며, 몇가지 제약조건이 추가됩니다. 기기가 Wi-Fi에 연결되어 있고 저장 용량이 부족하지 않은 경우에만 작동합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nf">LocationWorker</span><span class="o">(</span><span class="nl">context:</span> <span class="nc">Context</span><span class="o">,</span> <span class="nl">workerParams:</span> <span class="nc">WorkerParameters</span><span class="o">)</span> 
    <span class="o">:</span> <span class="nc">Worker</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">workerParams</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="o">...</span>
    
    <span class="n">override</span> <span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">{</span>
        
        <span class="n">val</span> <span class="n">latitude</span> <span class="o">=</span> <span class="n">inputData</span><span class="o">.</span><span class="na">getDouble</span><span class="o">(</span><span class="no">KEY_LATITUDE</span><span class="o">,</span> <span class="mf">40.1903484</span><span class="o">)</span>
        <span class="n">val</span> <span class="n">longitude</span> <span class="o">=</span> <span class="n">inputData</span><span class="o">.</span><span class="na">getDouble</span><span class="o">(</span><span class="no">KEY_LONGITUDE</span><span class="o">,</span> <span class="mf">44.5148367</span><span class="o">)</span>
        
        <span class="n">val</span> <span class="n">sendDataService</span> <span class="o">=</span> <span class="nc">SendDataService</span><span class="o">.</span><span class="na">getInstance</span><span class="o">()</span>
        <span class="n">sendDataService</span><span class="o">.</span><span class="na">sendLocation</span><span class="o">(</span><span class="n">latitude</span><span class="o">,</span> <span class="n">longitude</span><span class="o">)</span>
            <span class="o">.</span><span class="na">addSuccessCallback</span> <span class="o">{</span>
            
            <span class="o">}</span>
            <span class="o">.</span><span class="na">addFailureCallback</span> <span class="o">{</span>
            
            <span class="o">}</span>

        <span class="k">return</span> <span class="nc">Result</span><span class="o">.</span><span class="na">success</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
Worker가 생성되고, 주기적으로 실행되는 작업 공간의 큐에 추가됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">createConstraints</span><span class="o">()</span> <span class="o">=</span> <span class="nc">Constraints</span><span class="o">.</span><span class="na">Builder</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">setRequiredNetworkType</span><span class="o">(</span><span class="nc">NetworkType</span><span class="o">.</span><span class="na">UNMETERED</span><span class="o">)</span>  <span class="c1">//와이파이 연결된 경우</span>
                        <span class="c1">// 다른값(NOT_REQUIRED, CONNECTED, NOT_ROAMING, METERED)</span>
                        <span class="o">.</span><span class="na">setRequiresBatteryNotLow</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>                 <span class="c1">// 배터리가 부족하지 않는 경우</span>
                        <span class="o">.</span><span class="na">setRequiresStorageNotLow</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>                 <span class="c1">// 저장소가 부족하지 않는 경우</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">()</span>

<span class="n">fun</span> <span class="nf">createWorkRequest</span><span class="o">(</span><span class="nl">data:</span> <span class="nc">Data</span><span class="o">)</span> <span class="o">=</span> <span class="nc">PeriodicWorkRequestBuilder</span><span class="o">&lt;</span><span class="nc">LocationWorker</span><span class="o">&gt;(</span><span class="mi">12</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">HOURS</span><span class="o">)</span><span class="c1">// 12시간 반복</span>
                <span class="o">.</span><span class="na">setInputData</span><span class="o">(</span><span class="n">data</span><span class="o">)</span> <span class="c1">// 입력 데이터                                                  </span>
                <span class="o">.</span><span class="na">setConstraints</span><span class="o">(</span><span class="n">createConstraints</span><span class="o">())</span> <span class="c1">// 작업을 재시도 할경우에 대한 정책</span>
                <span class="o">.</span><span class="na">setBackoffCriteria</span><span class="o">(</span><span class="nc">BackoffPolicy</span><span class="o">.</span><span class="na">LINEAR</span><span class="o">,</span> 
                                <span class="nc">PeriodicWorkRequest</span><span class="o">.</span><span class="na">MIN_BACKOFF_MILLIS</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MILLISECONDS</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">()</span>

<span class="n">fun</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// 입력 데이터를 설정합니다. Bundle와 동일합니다.</span>
    <span class="n">val</span> <span class="n">work</span> <span class="o">=</span> <span class="n">createWorkRequest</span><span class="o">(</span><span class="nc">Data</span><span class="o">.</span><span class="na">EMPTY</span><span class="o">)</span>
    
    <span class="cm">/* 작업을 큐에 넣을때 동일한 작업인 경우에 대한 정책을 지정할 수 있습니다. 
    ExistingPeriodicWorkPolicy.KEEP은 동일한 작업을 큐에 넣게되며, 
    ExistingPeriodicWorkPolicy.REPLACE인 경우 작업이 대체됩니다. */</span>
    <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">().</span><span class="na">enqueueUniquePeriodicWork</span><span class="o">(</span><span class="s">"Smart work"</span><span class="o">,</span> <span class="nc">ExistingPeriodicWorkPolicy</span><span class="o">.</span><span class="na">KEEP</span><span class="o">,</span> <span class="n">work</span><span class="o">)</span>
    
    <span class="c1">// 작업의 상태를 LiveData를 통해 관찰하게 됩니다. </span>
    <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">().</span><span class="na">getWorkInfoByIdLiveData</span><span class="o">(</span><span class="n">work</span><span class="o">.</span><span class="na">id</span><span class="o">)</span>
        <span class="o">.</span><span class="na">observe</span><span class="o">(</span><span class="n">lifecycleOwner</span><span class="o">,</span> <span class="nc">Observer</span> <span class="o">{</span> <span class="n">workInfo</span> <span class="o">-&gt;</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">workInfo</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">workInfo</span><span class="o">.</span><span class="na">state</span> <span class="o">==</span> <span class="nc">WorkInfo</span><span class="o">.</span><span class="na">State</span><span class="o">.</span><span class="na">SUCCEEDED</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// 작업 완료</span>
            <span class="o">}</span>
        <span class="o">})</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
앞서 WorkManager의 장점으로 소개 했던 체인기반의 작업에 대해서도 알아 보겠습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">chainWorks</span><span class="o">(</span><span class="nl">filter1:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">filter2:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">compress:</span> <span class="nc">Work</span><span class="o">,</span> <span class="nl">upload:</span> <span class="nc">Work</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">WorkManager</span><span class="o">.</span><span class="na">getInstance</span><span class="o">()</span>
    <span class="c1">// Worker를 동시에 병렬로 실행합니다.</span>
    <span class="o">.</span><span class="na">beginWith</span><span class="o">(</span><span class="n">listOf</span><span class="o">(</span><span class="n">filter1</span><span class="o">,</span> <span class="n">filter2</span><span class="o">))</span>
    <span class="c1">// 이전 beginWith의 모든 작업이 끝난 경우 실행됩니다.</span>
    <span class="o">.</span><span class="na">then</span><span class="o">(</span><span class="n">compress</span><span class="o">)</span>
    <span class="c1">//compress작업이 완료 된경우 upload가 실행됩니다.</span>
    <span class="o">.</span><span class="na">then</span><span class="o">(</span><span class="n">upload</span><span class="o">)</span>
    <span class="c1">//enqueue()를 호출해야 이 모든 작업이 실행됩니다.</span>
    <span class="o">.</span><span class="na">enqueue</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2019-03-18-work_manager/worker_manager.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br />
<code class="language-plaintext highlighter-rouge">Worker</code>는 우리가 원했던 작업구현 방식입니다. doWork() 메소드에서 필요한 작업을 구현하면 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">WorkRequest</code>는 Worker의 arguments(입력된 데이터)와 constraints(네트워크 연결) 작업을 당담합니다.</p>

<p><code class="language-plaintext highlighter-rouge">WorkManager</code>는 WorkRequest를 큐에 담고 작업을 시작합니다. 이 작업을 스케쥴링 하기위해 <code class="language-plaintext highlighter-rouge">Room 데이터베이스</code>에 저장 하는 가장 좋은 방법을 사용합니다. 이 작업 결과는 LiveData를 통해 전송됩니다.</p>

<p><br /></p>
<h2 id="결론">결론</h2>
<p>WorkManager는 앱이 종료되거나 기기가 재시작되어도 실행되며, 비동기 작업을  구현하는 가장간단하면서 효과적인 솔루션입니다. 또한 제약사항을 통해 앱의 소비전력도 줄일 수 있습니다.</p>

<p><br /></p>

<p>참고:<br />
<a href="https://medium.com/@RobertLevonyan/android-workmanager-manage-periodic-tasks-c13fa7744ebd">https://medium.com/@RobertLevonyan/android-workmanager-manage-periodic-tasks-c13fa7744ebd</a>
<a href="https://developer.android.com/topic/libraries/architecture/workmanager">https://developer.android.com/topic/libraries/architecture/workmanager</a></p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><category term="서비스" /><summary type="html"><![CDATA[Android O 부터 긴 작업의 백그라운드 서비스와 브로드캐스트는 재기능을 하지 않습니다. 따라서 백그라운드 작업을 구현하기위해서는 WorkManager를 선택할 수 밖에 없습니다.]]></summary></entry><entry><title type="html">ViewPager2 맛보기</title><link href="https://blog.kmshack.kr/ViewPager2/" rel="alternate" type="text/html" title="ViewPager2 맛보기" /><published>2019-02-18T00:00:00+09:00</published><updated>2019-02-18T00:00:00+09:00</updated><id>https://blog.kmshack.kr/ViewPager2</id><content type="html" xml:base="https://blog.kmshack.kr/ViewPager2/"><![CDATA[<p><a href="https://android-developers.googleblog.com/2011/08/horizontal-view-swiping-with-viewpager.html">2011년에 출시된 ViewPager</a>가 7년여 만에 <a href="https://developer.android.com/jetpack/androidx/releases/viewpager2#1.0.0-alpha01">ViewPager2</a> 알파버전을 출시 하였습니다. 출시후 많은 개발자들이 ViewPager의 개선을 필요로 하였습니다.</p>

<p>앱에 많이 사용되고있지만 확실히 좋은 위젯은 아닙니다. FragmentPagerAdapter나 FragmentStatePagerAdapter를 사용할때 Fragment없이 ViewPager를 사용하면 안되는지에 대해 적어도 한번 이상은 궁금했었습니다. 
그리고 RTL지원과 수직페이징 처리등 다양한 기능을 지원하도록 요청하였습니다. 오픈소스를 통한 솔루션은 있으나 공식적인 라이브러리 업데이트는 아직 없습니다. 지금까지 였습니다..</p>

<p><br /></p>

<p>이제 모두 지원 합니다.</p>
<h2 id="viewpager2">ViewPager2</h2>

<p>프로젝트는 AndroidX로 구성하고 minSdkVersion 14이상을 지원해야 합니다.
build.gradle에 아래 라이브러리를 추가합니다.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha01'
</code></pre></div></div>

<p><br />
RecyclerView에 익숙하다면 ViewPager2를 구성하는것은 매우 익숙합니다. RecyclerView.Adapter를 상속받아 어댑터를 생성합니다. 물론 ViewHolder도 필요합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nf">AppPagerAdapter</span><span class="o">(</span><span class="kd">private</span> <span class="n">val</span> <span class="nl">apps:</span> <span class="nc">Array</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;)</span> <span class="o">:</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">Adapter</span><span class="o">&lt;</span><span class="nc">AppViewHolder</span><span class="o">&gt;()</span> <span class="o">{</span>
    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onCreateViewHolder</span><span class="o">(</span><span class="nl">parent:</span> <span class="nc">ViewGroup</span><span class="o">,</span> <span class="nl">viewType:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">AppViewHolder</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nf">AppViewHolder</span><span class="o">(</span><span class="nc">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">parent</span><span class="o">.</span><span class="na">context</span><span class="o">).</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">app_pager_item</span><span class="o">,</span> <span class="n">parent</span><span class="o">,</span> <span class="kc">false</span><span class="o">))</span>
    <span class="o">}</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onBindViewHolder</span><span class="o">(</span><span class="nl">holder:</span> <span class="nc">AppViewHolder</span><span class="o">,</span> <span class="nl">position:</span> <span class="nc">Int</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">txtApp</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">apps</span><span class="o">[</span><span class="n">position</span><span class="o">]</span>
    <span class="o">}</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">getItemCount</span><span class="o">()</span> <span class="o">=</span> <span class="n">apps</span><span class="o">.</span><span class="na">size</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nf">AppViewHolder</span><span class="o">(</span><span class="nl">view:</span> <span class="nc">View</span><span class="o">)</span> <span class="o">:</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span><span class="o">(</span><span class="n">view</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">val</span> <span class="nl">txtApp:</span> <span class="nc">TextView</span> <span class="o">=</span> <span class="n">view</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">txt_app</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
마지막으로 RecyclerView와 마찬가지로 ViewPager2의 Adapter를 설정합니다. 여기서 RecyclerView에서 처럼 LayoutManager는 필요없습니다. ORIENTATION_VERTICAL를 통해 스크롤 하도록 성정가능합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ViewPager2</span><span class="o">.</span><span class="na">orientation</span> <span class="o">=</span> <span class="nc">ViewPager2</span><span class="o">.</span><span class="na">RIENTATION_VERTICAL</span>
</code></pre></div></div>

<p><br />
ViewPager2는 아직 알파버전인 단계로 버그나 문제점은 보여집니다. 하지만 ViewPager의 View 재사용성 문제를 RecyclerView의 ViewHolder패턴을 그대로 사용하면서 해결함과 동시에 RecyclerView에 익숙한 개발자들에게 별도의 학습없이 적용할 수 있게 솔류션을 제공하였습니다. 또한 orientation기능 도입으로 가로 스크롤뿐만 세로스크롤도 지원하게 되었습니다. 또한 RTL도 지원합니다!</p>

<p>추가 기능외 전 버전의 notifyDataSetChanged()가 실행되지 않는 문제를 해결하였습니다. getItemPosition에서 POSITION_NONE을 강제로 반환하도록 하여 문제를 회피했던 경험 모두 있으실것 같은데 이제 해결되었습니다.</p>

<p><br /></p>

<h3 id="알려진-버그-100-알파-01기준">알려진 버그 (1.0.0 알파 01기준)</h3>
<p>-ClipToPadding 미지원<br />
-TabLayout 미지원<br />
-pageWidth 미지원(강제 가로/세로 100%)<br />
-currentItem 설정시 이전 페이지 보이는 현상</p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><category term="뷰페이저" /><summary type="html"><![CDATA[2011년에 출시된 ViewPager가 7년여 만에 ViewPager2 알파버전을 출시 하였습니다. 출시후 많은 개발자들이 ViewPager의 개선을 필요로 하였습니다.]]></summary></entry><entry><title type="html">Kotlin Coroutines 사용 패턴</title><link href="https://blog.kmshack.kr/kotlin_coroutine_pattern/" rel="alternate" type="text/html" title="Kotlin Coroutines 사용 패턴" /><published>2018-12-11T00:00:00+09:00</published><updated>2018-12-11T00:00:00+09:00</updated><id>https://blog.kmshack.kr/kotlin_coroutine_pattern</id><content type="html" xml:base="https://blog.kmshack.kr/kotlin_coroutine_pattern/"><![CDATA[<h2 id="async-호출을-감싸서-핸들링-하는-경우-coroutinescope또는-supervisorjob을-사용하자">async 호출을 감싸서 핸들링 하는 경우 coroutineScope또는 SupervisorJob을 사용하자.</h2>

<p>async 블록이 예외를 throw하는 경우 try/catch블록으로 감싸는 것만으로 모든 예외를 처리 할수 있다는 것에 신뢰하면 안됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="nl">job:</span> <span class="nc">Job</span> <span class="o">=</span> <span class="nc">Job</span><span class="o">()</span>
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>   <span class="c1">// (1)</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>                               <span class="c1">// (2)</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예제에서 doWork() 함수는 처리되지 않는 예외를 throw할 수 있는 새로운 코루틴(1)을 시작합니다. try/catch 블록(2)으로 doWork() 함수를 감싸게 되면 충돌이 발생합니다.</p>

<p>이는 자식 Job이 실패 하는 경우 부모도 즉각적으로 실패 해버리기때문에 발생하는 문제입니다.</p>

<p>충돌을 피할 수 있는 한 가지의 방법은 <code class="language-plaintext highlighter-rouge">SupervisorJob</code>을 사용하는 것입니다.</p>

<p>자식의 실패또는 취소로 인해 Job이 실패 되지 않으며 이로 인해 다른 자녀들에게 영향을 주지않게 됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               <span class="c1">// (1)</span>
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>참고: 이 기능은 SupervisorJob으로 코루틴 범위에서 비동기를 명시적으로 실행하는 경우에만 작동합니다. async가 부모 코루틴(1)의 범위에서 시작되었기 때문에 아래 코드는 여전히 크래시가 납니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">async</span> <span class="o">{</span>                                         <span class="c1">// (1)</span>
            <span class="c1">// may throw Exception </span>
        <span class="o">}.</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>크래시를 피하는 또 다른 방법은 <code class="language-plaintext highlighter-rouge">coroutineScope</code>(1)을 사용하여 async를 감싸서 사용하는 방법이 있습니다. 이로 인해 async 내부에서 예외가 발생하면 외부 범위를 건드리지 않고 이 범위에서 작성된 다른 모든 coroutine만 취소 됩니다.(2)</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>                               
<span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
<span class="c1">// may throw Exception</span>
<span class="n">fun</span> <span class="nf">doWork</span><span class="o">():</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="o">=</span> <span class="n">coroutineScope</span> <span class="o">{</span>     <span class="c1">// (1)</span>
    <span class="n">async</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>                       <span class="c1">// (2)</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="n">doWork</span><span class="o">().</span><span class="na">await</span><span class="o">()</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">e:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="루트-코루틴은-main-디스패처를-선호하자">루트 코루틴은 Main 디스패처를 선호하자.</h2>

<p>백그라운드 코루틴에서 백그라운드 작업을 수행하고 UI업데이트를 해야 하는 경우 Main 디스패처에서 실행해야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span>          <span class="c1">// (1)</span>
<span class="n">fun</span> <span class="nf">login</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span> <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span> <span class="o">}</span>  <span class="c1">// (2)  </span>
    <span class="n">networkClient</span><span class="o">.</span><span class="na">login</span><span class="o">(...)</span>
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span> <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span> <span class="o">}</span>  <span class="c1">// (2)</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예에서 기본 디스패처(1)에서 코루틴을 실행합니다. 이 접근 방식을 사용하면 UI를 터치 할때 마다 Main 디스패처로 컨텍스트를 전환해야 합니다.(2)</p>

<p>이런 경우 Main 디스패처로 코루틴을 실행 하는 것이 코드가 훨씬 단순해지며 컨텍스트 전환이 더 명확해집니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span>
<span class="n">fun</span> <span class="nf">login</span><span class="o">()</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>    
    <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span> <span class="n">networkClient</span><span class="o">.</span><span class="na">login</span><span class="o">(...)</span> <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="불필요한-asyncawait-사용을-피하자">불필요한 async/await 사용을 피하자.</h2>

<p>async 함수를 사용하고 즉시 await하는 경우라면 코드를 당장 걷어내야합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">data</span> <span class="o">=</span> <span class="n">async</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* code */</span> <span class="o">}.</span><span class="na">await</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>만약 코루틴 컨텍스트를 바꾸고 즉시 중단하고 싶다면 withContext를 사용하는 것이 훨씬 좋은 방법이 될 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">data</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* code */</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>포퍼먼스 측면으로는 큰 문제는 아니지만( 심지어 async가 작업을 수행하기위해 새로운 코루틴을 생성해도) 의미론적으로 async 하다는 것은 백그라운드에서 여러개의 코루틴을 시작한 뒤 기다리고 있음을 의미하기때문에 적절하지 않습니다.</p>

<p><br /></p>

<h2 id="scope-job을-취소하지-말자">scope Job을 취소하지 말자.</h2>

<p>코루틴을 취소해야 하는 경우 스코프 작업을 취소하면 안됩니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">WorkManager</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">doWork1</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span>
    <span class="o">}</span>
    <span class="n">fun</span> <span class="nf">doWork2</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span>
    <span class="o">}</span>
    <span class="n">fun</span> <span class="nf">cancelAllWork</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">job</span><span class="o">.</span><span class="na">cancel</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">workManager</span> <span class="o">=</span> <span class="nc">WorkManager</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork2</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">cancelAllWork</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span> <span class="c1">// (1)</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 코드의 문제는 작업을 취소할 때 완료 상태로 만들어 버리는 것에 있습니다. 이미 완료된 작업 범위에서 실행된 코루틴은 재 실행되지 않습니다. (1)</p>

<p>특정 범위의 모든 코루틴을 취소하기위해서는 <code class="language-plaintext highlighter-rouge">cancelChildren</code> 함수를 사용할 수 있습니다. 또한 개별 작업 취소기능을 제공합니다.(2)</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">WorkManager</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">scope</span> <span class="o">=</span> <span class="nc">CoroutineScope</span><span class="o">(</span><span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Default</span> <span class="o">+</span> <span class="n">job</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">doWork1</span><span class="o">():</span> <span class="nc">Job</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span> <span class="c1">// (2)</span>
    <span class="n">fun</span> <span class="nf">doWork2</span><span class="o">():</span> <span class="nc">Job</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span> <span class="cm">/* do work */</span> <span class="o">}</span> <span class="c1">// (2)</span>
    <span class="n">fun</span> <span class="nf">cancelAllWork</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">coroutineContext</span><span class="o">.</span><span class="na">cancelChildren</span><span class="o">()</span>         <span class="c1">// (1)                             </span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="n">fun</span> <span class="nf">main</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">workManager</span> <span class="o">=</span> <span class="nc">WorkManager</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork2</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">cancelAllWork</span><span class="o">()</span>
    <span class="n">workManager</span><span class="o">.</span><span class="na">doWork1</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="분명하지-않은-디스패처는-suspend-함수로-작성하지-말자">분명하지 않은 디스패처는 suspend 함수로 작성하지 말자.</h2>

<p>명백한 코루틴 디스패처에 대한 실행인 경우 suspend 함수로 작업하지 않아야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nf">login</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span>  
        <span class="n">someBlockingCall</span><span class="o">()</span> 
    <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위의 예제는 로그인하는 기능으로 Main 디스패처가 아닌 곳에서 실행하면 크래시를 발생하는 suspend 함수 입니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>     <span class="c1">// (1) no crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span>  <span class="c1">// (2) cause crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CalledFromWrongThreadException</code> 생성된 원래 스레드에서만 해당 뷰를 제어해야 합니다.</p>

<p>suspend 함수는 어떠한 코루틴 디스패처에 대해 실행할 수 있도록 디자인되어야합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">suspend</span> <span class="n">fun</span> <span class="nf">login</span><span class="o">():</span> <span class="nc">Result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">view</span><span class="o">.</span><span class="na">showLoading</span><span class="o">()</span>
    <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">withContext</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">IO</span><span class="o">)</span> <span class="o">{</span>  
        <span class="n">someBlockingCall</span><span class="o">()</span> 
    <span class="o">}</span>
    <span class="n">view</span><span class="o">.</span><span class="na">hideLoading</span><span class="o">()</span>
    <span class="k">return</span> <span class="n">result</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 모든 디스패처에서 로그인 기능을 실행할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Main</span><span class="o">)</span> <span class="o">{</span>     <span class="c1">// (1) no crash</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="n">launch</span><span class="o">(</span><span class="nc">Dispatcher</span><span class="o">.</span><span class="na">Default</span><span class="o">)</span> <span class="o">{</span>  <span class="c1">// (2) no crash ether</span>
    <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="n">login</span><span class="o">()</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>

<h2 id="globalscope-사용을-피하자">GlobalScope 사용을 피하자.</h2>

<p>만일 안드로이드 어플리케이션에서 <code class="language-plaintext highlighter-rouge">GlobalScope</code>를 사용하고 있다면 당장 사용을 중단해야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">GlobalScope</span><span class="o">.</span><span class="na">launch</span> <span class="o">{</span>
    <span class="c1">// code</span>
<span class="o">}</span>
</code></pre></div></div>

<p>GlobalScope는 전체 어플리케이션 수명 동안에 작동하고, 취소되지 않는 최상위 수준의 동시 처리를 시작하는데 사용됩니다.</p>

<p>별로도 정의한 <code class="language-plaintext highlighter-rouge">CoroutineScope</code>를 사용해야하며 async를 사용하거나 GlobalScope 인스턴스에서 실행하는 것이 좋습니다.</p>

<p>안드로이드에서 코루틴은 Activity, Fragment, View 또는 ViewModel의 수명주기로 쉽게 범위를 지정할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MainActivity</span> <span class="o">:</span> <span class="nc">AppCompatActivity</span><span class="o">(),</span> <span class="nc">CoroutineScope</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="n">val</span> <span class="n">job</span> <span class="o">=</span> <span class="nc">SupervisorJob</span><span class="o">()</span>

    <span class="n">override</span> <span class="n">val</span> <span class="nl">coroutineContext:</span> <span class="nc">CoroutineContext</span>
        <span class="nf">get</span><span class="o">()</span> <span class="o">=</span> <span class="nc">Dispatchers</span><span class="o">.</span><span class="na">Main</span> <span class="o">+</span> <span class="n">job</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onDestroy</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onDestroy</span><span class="o">()</span>
        <span class="n">coroutineContext</span><span class="o">.</span><span class="na">cancelChildren</span><span class="o">()</span>
    <span class="o">}</span>
    
    <span class="n">fun</span> <span class="nf">loadData</span><span class="o">()</span> <span class="o">=</span> <span class="n">launch</span> <span class="o">{</span>
        <span class="c1">// code</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
참고: <a href="https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e">https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e</a></p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><category term="코틀린" /><category term="코루틴" /><summary type="html"><![CDATA[async 호출을 감싸서 핸들링 하는 경우 coroutineScope또는 SupervisorJob을 사용하자.]]></summary></entry><entry><title type="html">MotionLayout을 통한 코드 한줄 없이 전화효과 주기</title><link href="https://blog.kmshack.kr/motionlayout/" rel="alternate" type="text/html" title="MotionLayout을 통한 코드 한줄 없이 전화효과 주기" /><published>2018-10-12T00:00:00+09:00</published><updated>2018-10-12T00:00:00+09:00</updated><id>https://blog.kmshack.kr/motionlayout</id><content type="html" xml:base="https://blog.kmshack.kr/motionlayout/"><![CDATA[<p>ConstraintLayout을 사용하는데 있어서 ConstraintSet을 이용한 애니메이션 처리는 간결한 코드를 통해 간단히 처리 할 수 있는 방법을 제공해 왔습니다. 두개의 레이아웃을 통해 서로 다른 크기와 위치의 변화를 감지하여 자동으로 애니메이션 처리하는 멋진 아이디어 덕분입니다.</p>

<p>아직 ConstraintSet을 사용해보지 않으셨다면 아래 <a href="https://www.youtube.com/watch?v=OHcfs6rStRo">Sean McQuillan @objcode</a>의 영상을 보시기 권해드립니다.</p>

<iframe width="98%" height="400" src="https://www.youtube.com/embed/OHcfs6rStRo?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe>

<p>위의 영상에서 볼수 있듯 ConstraintLayout과 TransitionManager를 통해 애니메이션을 만드는 쉬운 방법을 제공합니다. 이런 기본적인 아이디어를 바탕으로 MotionLayout은 개념을 훨씬 확장하게 됩니다.</p>

<p><br /></p>
<h1 id="motionlayout">MotionLayout?</h1>

<p>MotionLayout은 레이아웃 전환과 복잡한 모션처리 사이를 컨트롤 쉽게 하기위해 만들어졌습니다. 사용자와 상호 작용해야하는 요소인 버튼, 타이틀 등의 실제 UI요소를 이동하거나 크기 조정또는 애니메이션처리 해야 하는 경우 가장 유용합니다. 단순히 애니메이션을 처리 하기위해서는 기존 안드로이드 프레임워크에서 지원하는 여러가지 방식을 사용하면 됩니다.</p>

<ul>
  <li>Animated Vector Drawable</li>
  <li>Property Animation</li>
  <li>LayoutTransition</li>
  <li>TransitionManager</li>
  <li>CoordinatorLayout</li>
</ul>

<p><br />
MotionLayout은 기존 애니메이션 방식과는 전혀다릅니다. 이름에서도 알 수 있듯 레이아웃이며 이에따라 요소들을 배치할 수 있습니다. 실제로 ConstraintLayout을 상속한 하위클래스로 풍부한 레이아웃 기능을 기반으로합니다.</p>

<h4 id="property-animation--transitionmanager--coordinatorlayout--motionlayout">Property Animation + TransitionManager + CoordinatorLayout = MotionLayout</h4>

<p><br />
두 레이아웃 사이의 전환에 있어 최상위 레이아웃 뿐만 아니라 하위 레이아웃 속성에 애니메이션을 적용 할 수 있습니다. 또한 CoordinatorLayout과 같이 애니메이션의 포지셔닝을 마음대로 조절가능합니다.  터치 핸들링과 키프레임을 지원함으로 자신의 필요에 맞게 쉽게 전환 효과를 정의 할 수 있습니다.</p>

<p>MotionLayout은 전혀 코드가 필요하지 않고 xml로 완벽하게 작동할 수 있게 합니다. 이렇게 코드와 디커플링됨에 따라 Android Studio에서 xml을 보여주는 훌륭한 그래픽 도구를 제공 할 수 있을 것으로 보입니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-12-motionlayout/1.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>마지막으로 MotionLayout을 지원하는 ConstraintLayout 2.0에서는 API 레벨 14(ICS) 부터 서포트 라이브러리를 통해 사용할 수 있습니다. 이는 현재 안드로이드 기기의 99.8%를 지원하는 수준입니다.</p>

<p><br /> 
<strong>MotionLayout 프로젝트에 추가</strong><br />
Gradle file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies {
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha2'
}
</code></pre></div></div>

<p><br />
<strong>MotionLayout 사용</strong><br />
Layout file:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span> <span class="err">...</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<p><br /></p>
<h1 id="motionscene">MotionScene</h1>
<p>MotionLayout은 일반적인 레이아웃과는 달리 <code class="language-plaintext highlighter-rouge">res/xml</code> 디렉토리에 저장되는 별도의 <code class="language-plaintext highlighter-rouge">xml</code>파일인 <code class="language-plaintext highlighter-rouge">MotionScene</code>에 속성을 정의합니다. 여기에는 애니메이션을 지정하는 필요한 Transition 처리, 키 프레임, 터치 처리등의 요소들이 포함됩니다.</p>

<p><br /> <img src="/images/2018-10-12-motionlayout/2.png" alt="" class="center-image" /> <br /></p>

<p><br /></p>
<h2 id="기존-constraintset을-이용한-방식을-motionlayout에-적용">기존 ConstraintSet을 이용한 방식을 MotionLayout에 적용</h2>

<p>ConstraintLayout을 사용하여 첫번째 화면 왼쪽에 위젯 배치, 두번째 화면 오른쪽에 위젯 배치의 2개의 ConstraintSet을 만들어 좌/우 전환하는(TransitionManager를 사용하면 애니메이션 처리) 예를 들어보겠습니다.</p>

<p><br /></p>

<ul>
  <li>화면 왼쪽에 위젯 배치</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.ConstraintLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_marginStart=</span><span class="s">"8dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span>
        <span class="na">app:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintStart_toStartOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.ConstraintLayout&gt;</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-12-motionlayout/4.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br /></p>
<ul>
  <li>화면 오른쪽에 위젯 배치</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.ConstraintLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_marginEnd=</span><span class="s">"8dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span>
        <span class="na">app:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintEnd_toEndOf=</span><span class="s">"parent"</span>
        <span class="na">app:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.ConstraintLayout&gt;</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-12-motionlayout/5.png" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p><br />
ConstraintLayout을 사용하면 두개의 레이아웃을 ConstraintSet을 이용하여 서로 전환할 수 있습니다. TransitionManager를 사용하는 경우 전환 애니메이션으로 표시됩니다. <strong>이 방식의 문제는 전환이 시작되면 중도에 중단할 수 없다는 것에 있습니다. 전환시 특정 지점으로 이동하도록 지원하지 않기때문에 사용자와의 상호작용할 수 없습니다.</strong></p>

<p><br />
MotionLayout은 이런 모든 문제를 해결 할 수 있습니다. 이를 위해 기존의 2개의 레이아웃을 이용하여 MotionLayout을 통해 자동으로 초기화 하는 방식으로 바꿔보겠습니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">android:id=</span><span class="s">"@+id/motionLayout"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span>
    <span class="na">app:layoutDescription=</span><span class="s">"@xml/scene_01"</span>
    <span class="na">tools:showPaths=</span><span class="s">"true"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.motion.MotionLayout&gt;</span>
</code></pre></div></div>
<p><br /></p>

<p>레이아웃 <code class="language-plaintext highlighter-rouge">inflate</code>시 MotionScene은 <code class="language-plaintext highlighter-rouge">@xml/scene_01</code>파일을 참조하게 됩니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;MotionScene</span>
    <span class="na">xmlns:motion=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;Transition</span>
        <span class="na">motion:constraintSetStart=</span><span class="s">"@layout/motion_01_cl_start"</span>
        <span class="na">motion:constraintSetEnd=</span><span class="s">"@layout/motion_01_cl_end"</span>
        <span class="na">motion:duration=</span><span class="s">"1000"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;OnSwipe</span>
            <span class="na">motion:touchAnchorId=</span><span class="s">"@+id/button"</span>
            <span class="na">motion:touchAnchorSide=</span><span class="s">"right"</span>
            <span class="na">motion:dragDirection=</span><span class="s">"dragRight"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/Transition&gt;</span>

<span class="nt">&lt;/MotionScene&gt;</span>
</code></pre></div></div>

<p><br />
<code class="language-plaintext highlighter-rouge">scene_01</code>은 전환에 사용 할 ConstraintSet 시작(<code class="language-plaintext highlighter-rouge">motion_01_cl_start</code>)/종료(<code class="language-plaintext highlighter-rouge">motion_01_cl_end</code>) 레이아웃을 지정할 수 있습니다. 또한 핵심인 <code class="language-plaintext highlighter-rouge">onSwipe</code>는 전환에 있어 핸들러를 지정했음을 주목해야합니다.</p>

<p><br /></p>
<h3 id="onswipe">OnSwipe</h3>
<p><code class="language-plaintext highlighter-rouge">OnSwipe</code> 핸들러는 사용자의 액션이 일치되는 경우 전환을 시작합니다.</p>

<p><strong>주요 속성</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">touchAnchorId</code> 추적해야 할 대상 (<code class="language-plaintext highlighter-rouge">@+id/button</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">touchAnchorSide</code> 손가락을 추적해야하는 물체의 측면 (<code class="language-plaintext highlighter-rouge">right/left/top/bottom</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">dragDirection</code> 추적중인 동작의 방향 (<code class="language-plaintext highlighter-rouge">dragRight/dragLeft/dragUp/dragDown</code>)</li>
</ul>

<p><br /></p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-12-motionlayout/3.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>이제 코드 한줄 없이 사용자의 스와이프 이벤트를 통해 네모박스가 자연스럽게 애니메이션되면서 끝에서 끝으로 이동 합니다.</p>

<p><br /></p>
<h2 id="motionlayout-자체로-motionscene-구현">MotionLayout 자체로 MotionScene 구현</h2>
<p>위의 예제 ConstraintSet을 사용하여 2개의 레이아웃으로 전환하였으나 이번에는 MotionLayout으로만 사용하여 빠르게 레이아웃을 수성하도록 해보겠습니다.</p>

<p>MotionLayout은 <code class="language-plaintext highlighter-rouge">res/xml</code> 디렉토리에 있는 MotionScene 파일에서 ConstraintSet을 직접 설정하는 기능을 지원합니다. 이는 ConstraintSet을 사용하기위한 다양한 레이아웃 생성을 하지 않아도 되는 장점이 있습니다. 이는 코드와 별도로 동작하기때문에 나중에 Adnroid Studio에서 모션레이아웃 뷰어를 지원할 가능성이 높습니다.</p>

<p><br /></p>
<h3 id="motionlayout의-속성">MotionLayout의 속성</h3>
<ul>
  <li>alpha</li>
  <li>visibility</li>
  <li>elevation</li>
  <li>rotation, rotation[X/Y]</li>
  <li>translation[X/ Y/Z]</li>
  <li>scaleX/Y</li>
</ul>

<p><br />
첫번째 예제에서 사용한 ConstraintLayout대신 MotionLayout을 사용하여 레이아웃을 다시 구성하였습니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;android.support.constraint.motion.MotionLayout</span>
    <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">xmlns:app=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span>
    <span class="na">android:id=</span><span class="s">"@+id/motionLayout"</span>
    <span class="na">app:layoutDescription=</span><span class="s">"@xml/scene_02"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"match_parent"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;View</span>
        <span class="na">android:id=</span><span class="s">"@+id/button"</span>
        <span class="na">android:background=</span><span class="s">"@color/colorAccent"</span>
        <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
        <span class="na">android:text=</span><span class="s">"Button"</span> <span class="nt">/&gt;</span>

<span class="nt">&lt;/android.support.constraint.motion.MotionLayout&gt;</span>
</code></pre></div></div>

<p>첫번째 예제에서 constraintSetStart와 constraintSetEnd속성 값으로 사용할 <code class="language-plaintext highlighter-rouge">@layout</code>을 지정해준 반면 정의한 ConstraintSet <code class="language-plaintext highlighter-rouge">@id</code>를 지정해줍니다.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;MotionScene</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:motion=</span><span class="s">"http://schemas.android.com/apk/res-auto"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;Transition</span>
        <span class="na">motion:constraintSetStart=</span><span class="s">"@+id/start"</span>
        <span class="na">motion:constraintSetEnd=</span><span class="s">"@+id/end"</span>
        <span class="na">motion:duration=</span><span class="s">"1000"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;OnSwipe</span>
            <span class="na">motion:touchAnchorId=</span><span class="s">"@+id/button"</span>
            <span class="na">motion:touchAnchorSide=</span><span class="s">"right"</span>
            <span class="na">motion:dragDirection=</span><span class="s">"dragRight"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/Transition&gt;</span>

    <span class="nt">&lt;ConstraintSet</span> <span class="na">android:id=</span><span class="s">"@+id/start"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Constraint</span>
            <span class="na">android:id=</span><span class="s">"@+id/button"</span>
            <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_marginStart=</span><span class="s">"8dp"</span>
            <span class="na">motion:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintStart_toStartOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/ConstraintSet&gt;</span>

    <span class="nt">&lt;ConstraintSet</span> <span class="na">android:id=</span><span class="s">"@+id/end"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;Constraint</span>
            <span class="na">android:id=</span><span class="s">"@+id/button"</span>
            <span class="na">android:layout_width=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_height=</span><span class="s">"64dp"</span>
            <span class="na">android:layout_marginEnd=</span><span class="s">"8dp"</span>
            <span class="na">motion:layout_constraintBottom_toBottomOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintEnd_toEndOf=</span><span class="s">"parent"</span>
            <span class="na">motion:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/ConstraintSet&gt;</span>

<span class="nt">&lt;/MotionScene&gt;</span>
</code></pre></div></div>

<p>기존의 위젯은 Constraint로 모두 대체합니다. 속성 변경을 원하는 위젯만 Constraint를 지정해야 적용가능 합니다. 만약 10개의 위젯이 있는 레이아웃일 경우 2개만 애니메이션 처리하고 싶은 경우 MotionScene의 ConstraintSet은 해당 위젯에 대한 Constraint를 지정하면 됩니다.</p>

<p><br /></p>
<h3 id="motionlayout-속성">MotionLayout 속성</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">app:layoutDescription="reference"</code> 예제에서 본것 처럼 MotionScene xml파일을 지정해야 합니다.</li>
  <li><code class="language-plaintext highlighter-rouge">app:applyMotionScene="boolean"</code> MotionScene을 적용할지 여부를 지정합니다.</li>
  <li><code class="language-plaintext highlighter-rouge">app:showPaths="boolean"</code> 동작 경로를 표시합니다. (기본값= false). 릴리즈 빌드시 끄는것을 잊지마세요.</li>
  <li><code class="language-plaintext highlighter-rouge">app:progress="float"</code> 특정 위치의 진행 장면으로 이동합니다. (0~1)</li>
  <li><code class="language-plaintext highlighter-rouge">app:currentState="reference"</code> 레이아웃 초기화시 특정 ConstraintSet을 강제로 실행합니다.</li>
</ul>

<p><br />
이제 한줄의 코드 없이 사용자 이벤트를 통한 상호작용과 부드러운 애니메이션을 처리를 할 수 있습니다.<br />
위의 모든 예제 코드는 <a href="https://github.com/googlesamples/android-ConstraintLayoutExamples">Github</a>를 통해 보실 수 있습니다.</p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><summary type="html"><![CDATA[ConstraintLayout을 사용하는데 있어서 ConstraintSet을 이용한 애니메이션 처리는 간결한 코드를 통해 간단히 처리 할 수 있는 방법을 제공해 왔습니다. 두개의 레이아웃을 통해 서로 다른 크기와 위치의 변화를 감지하여 자동으로 애니메이션 처리하는 멋진 아이디어 덕분입니다.]]></summary></entry><entry><title type="html">안드로이드에서 자연스러운 키보드 변경 처리하기</title><link href="https://blog.kmshack.kr/smoothkeyboard/" rel="alternate" type="text/html" title="안드로이드에서 자연스러운 키보드 변경 처리하기" /><published>2018-10-09T00:00:00+09:00</published><updated>2018-10-09T00:00:00+09:00</updated><id>https://blog.kmshack.kr/smoothkeyboard</id><content type="html" xml:base="https://blog.kmshack.kr/smoothkeyboard/"><![CDATA[<p><a href="https://play.google.com/apps/testing/me.saket.dank">Dank</a>라는 앱을 사용할때 부드럽게 잘 작동하는것에 많은 사용자들은 만족스러워 할것입니다. 앱은 화면 변경시 적절한 모션처리를 잘하였지만, 좀더 부드럽게 느껴지는 이유는 키보드 변경처리를 잘하였기 때문입니다.</p>

<p>소프트키보드가 표시되면 안드로이드는 콘텐츠의 크기를 즉각 변경합니다. (<code class="language-plaintext highlighter-rouge">windowSoftInputMode</code>를 설정하지 않은경우) 이로 인해 레이아웃에 부자연스러운 변화가 생깁니다. 아쉽게도 플랫폼이나 대다수의 앱이 이를 처리하지 않기때문에 사용자들은 정상적으로 작동한다는것으로 착각하고 있습니다.</p>

<p>역시 글로만 봐서 이해하기 힘들기때문에 차이점을 명확히 알아보기 위해 Google Keep과 Dank 앱을 영상으로 비교 해보겠습니다.</p>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-09-smooth_keyboard/1.gif" alt="" class="center-image" /> Google Keep</td>
    </tr>
  </tbody>
</table>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-09-smooth_keyboard/2.gif" alt="" class="center-image" /> Dank</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p>위의 화면을 통해 비교해 보면 Dank앱이 키보드가 반응하면 콘텐츠를 위쪽으로 부드럽게 크기를 조정하며 스크롤하는것을 볼 수 있습니다. 반면 Google Keep은 키보드의 반응에  변동이 없어 컨텐츠영역에도 아무런 반응이 없어 보입니다.</p>

<p>좀 더 부드러운 화면으로 비교를 원한다면 영상으로 확인해보세요.( <a href="/images/2018-10-09-smooth_keyboard/keep_keyboard_720p.mp4">Google Keep</a>, <a href="/images/2018-10-09-smooth_keyboard/dank_keyboard_720p.mp4">Dank</a>)</p>

<p><br /></p>

<p>눈치 빠른 분들은 Dank앱이 처리하는 작은 트릭을 발견하실 수 있습니다. 이 작은 트릭은 키보드가 표시되면 Activity의 전체 화면의 크기가 조정된다는 것에 있습니다. 이는 Activity를 구성하는 View 계층의 최상단 레이아웃(<code class="language-plaintext highlighter-rouge">DecorView</code>)에 아무런 영향을 받지 않고 처리 했다는 사실을 알 수 있습니다. 실제로 뷰의 크기가 저장되는 레이아웃ID는 콘텐츠 레이아웃(<code class="language-plaintext highlighter-rouge">android.R.id.content</code>)입니다. 이것은 <code class="language-plaintext highlighter-rouge">DecorView</code>내의 컨텐츠 레이아웃을 제어할 수 있다는 것을 의미합니다.</p>

<p>Activity를 구성하는 View Tree는 일반적으로 다음과 같습니다.</p>

<p><strong>DecorView</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">-</span> <span class="nc">LinearLayout</span>
<span class="o">--</span> <span class="nc">FrameLayout</span>  <span class="o">&lt;-</span> <span class="n">android</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">content</span>
<span class="o">---</span> <span class="nc">LinearLayout</span>
<span class="o">----</span> <span class="nc">Activity</span> <span class="n">content</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-09-smooth_keyboard/3.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<p>리사이즈 되는 부분을 제어하기 위해 콘텐츠 레이아웃(android.R.id.content)의 크기가 변경되는 것을 감지하는 유틸 클래스를 만들었습니다. View의 크기가 변경되면 컨텐츠의 전체 높이에서 변경된 높이만큼 변경되도록 애니메이션처리 합니다.</p>

<p><br /></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">val</span> <span class="n">decorView</span> <span class="o">=</span> <span class="n">activity</span><span class="o">.</span><span class="na">window</span><span class="o">.</span><span class="na">decorView</span>

<span class="n">decorView</span><span class="o">.</span><span class="na">viewTreeObserver</span><span class="o">.</span><span class="na">addOnPreDrawListener</span> <span class="o">{</span> 
  <span class="n">val</span> <span class="n">contentHeight</span> <span class="o">=</span> <span class="n">contentViewFrame</span><span class="o">.</span><span class="na">height</span>
  <span class="n">val</span> <span class="n">sizeChanged</span> <span class="o">=</span> <span class="n">contentHeight</span> <span class="o">!=</span> <span class="n">previousHeight</span>

  <span class="nf">if</span> <span class="o">(</span><span class="n">sizeChanged</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">animateSizeChange</span><span class="o">(</span><span class="n">from</span> <span class="o">=</span> <span class="n">previousHeight</span><span class="o">,</span> <span class="n">to</span> <span class="o">=</span> <span class="n">contentHeight</span><span class="o">)</span>
  <span class="o">}</span>

  <span class="n">previousHeight</span> <span class="o">=</span> <span class="n">contentHeight</span>
<span class="o">}</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><br /> <img src="/images/2018-10-09-smooth_keyboard/4.gif" alt="" class="center-image" /> <br /></td>
    </tr>
  </tbody>
</table>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fun</span> <span class="nf">animateSizeChange</span><span class="o">(</span><span class="nl">from:</span> <span class="nc">Int</span><span class="o">,</span> <span class="nl">to:</span> <span class="nc">Int</span><span class="o">)</span> <span class="o">{</span>
  <span class="c1">// Immediately snap back to the original size.</span>
  <span class="n">contentView</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">from</span><span class="o">)</span>
  
  <span class="nc">ObjectAnimator</span><span class="o">.</span><span class="na">ofInt</span><span class="o">(</span><span class="n">from</span><span class="o">,</span> <span class="n">to</span><span class="o">)</span>
    <span class="o">.</span><span class="na">addUpdateListener</span> <span class="o">{</span> 
      <span class="n">val</span> <span class="n">h</span> <span class="o">=</span> <span class="n">it</span><span class="o">.</span><span class="na">animatedValue</span> <span class="n">as</span> <span class="nc">Int</span>
      <span class="n">contentView</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">h</span><span class="o">)</span> 
    <span class="o">}</span>
    <span class="o">.</span><span class="na">start</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이와 관련된 모든 코드는 <a href="https://github.com/saket/FluidKeyboardResize">여기</a>를 통해 확인하세요.</p>

<p><br /></p>

<p>컨텐츠 뷰의 크기에 따라 애니메이션처리를 하여 높이를 변경하는 것은 손이 많이 가는일이지만 앱의 품질을 높이는 큰역할을 합니다. 다르게 구현할 수 있는 방법도 많고, 이것이 최상의 솔루션은 아니지만 여러달 테스트하면서 문제점을 발견하지는 못하였습니다.</p>

<p><br />
<br />
참고: <a href="https://saket.me/smoothly-reacting-to-keyboard/">https://saket.me/smoothly-reacting-to-keyboard/</a></p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><summary type="html"><![CDATA[Dank라는 앱을 사용할때 부드럽게 잘 작동하는것에 많은 사용자들은 만족스러워 할것입니다. 앱은 화면 변경시 적절한 모션처리를 잘하였지만, 좀더 부드럽게 느껴지는 이유는 키보드 변경처리를 잘하였기 때문입니다.]]></summary></entry><entry><title type="html">ProcessLifecycleOwner를 이용한 앱 Background/Foreground이벤트 처리</title><link href="https://blog.kmshack.kr/ProcessLifecycleOwner%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%B1-Background,-Foreground-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC/" rel="alternate" type="text/html" title="ProcessLifecycleOwner를 이용한 앱 Background/Foreground이벤트 처리" /><published>2018-06-13T00:00:00+09:00</published><updated>2018-06-13T00:00:00+09:00</updated><id>https://blog.kmshack.kr/ProcessLifecycleOwner%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%20%EC%95%B1%20Background,%20Foreground%20%EC%9D%B4%EB%B2%A4%ED%8A%B8%20%EC%B2%98%EB%A6%AC</id><content type="html" xml:base="https://blog.kmshack.kr/ProcessLifecycleOwner%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%B1-Background,-Foreground-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC/"><![CDATA[<p>안드로이드 앱을 개발을 하다보면 앱이 Background, Foreground 상태에 대한 이벤트를 탐지를 필요로 합니다. 일반적으로 ActivityManager의 Activity의 갯수와 Lifecycle에 따라 이벤트 탐지를 합니다. 모든 Activity의 Lifecycle을 관리한다는 것은 번거로운 일입니다. 하지만 Android Architecture Components를 이미 사용한다면 간단히 몇줄 만에 해당 이벤트를 탐지 할 수 있습니다.</p>

<p>Architecture Components를 아직 사용하지 않고 있다면 build.gradle에 아래와 같이 라이브러리를 추가 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dependencies{
  kapt "android.arch.lifecycle:compiler:1.1.1"
  implementation "android.arch.lifecycle:extensions:1.1.1"
}
</code></pre></div></div>

<p>Activity(or Fragment)의 Lifecycle 상태를 모니터링할때 사용하는 LifecycleObserver 인터페이스를 동일하게 하나 구현합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AppLifecycleObserver</span> <span class="o">:</span> <span class="nc">LifecycleObserver</span> <span class="o">{</span>

    <span class="nd">@OnLifecycleEvent</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">Event</span><span class="o">.</span><span class="na">ON_START</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">onForeground</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">//foreground</span>
    <span class="o">}</span>

    <span class="nd">@OnLifecycleEvent</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">Event</span><span class="o">.</span><span class="na">ON_STOP</span><span class="o">)</span>
    <span class="n">fun</span> <span class="nf">onBackground</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">//background</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>구현된 인터페이스를 Activity가 아닌 Application의 onCreate()에 ProcessLifecycleOwner를 이용하여 Observer하도록 합니다. ProcessLifecycleOwner는 내부적으로 Activity의 갯수및 Lifecycle 상태를 모니터링 하는 역할을 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AppApplication</span> <span class="o">:</span> <span class="nc">Application</span><span class="o">()</span> <span class="o">{</span>

    <span class="n">override</span> <span class="n">fun</span> <span class="nf">onCreate</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">()</span>

        <span class="nc">ProcessLifecycleOwner</span><span class="o">.</span><span class="na">get</span><span class="o">().</span><span class="na">lifecycle</span>
            <span class="o">.</span><span class="na">addObserver</span><span class="o">(</span><span class="nc">AppLifecycleObserver</span><span class="o">())</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 우리는 ON_START이벤트가 발생하는 경우 앱이 Foreground 상태로 진입을 의미하며, ON_STOP이벤트의 경우 Background로 진입했다는 것을 알 수 있습니다. 이러한 이벤트는 Activity의 Lifecycle를 모니터링 하기때문에 의도치 않게 Activity가 중도에 destory이 되어 새롭게 create되는 경우 이벤트가 지연될 수 있습니다. 이는 밀리세컨드의 단위로 정확성을 요구하는 곳에서 사용이 부적절할 수 있음을 의미합니다.
ProcessLifecycleOwner에 대한 더 많은 정보는 <a href="https://developer.android.com/reference/android/arch/lifecycle/ProcessLifecycleOwner">안드로이드 개발문서</a>를 참고 해주세요.</p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><category term="아키텍쳐" /><summary type="html"><![CDATA[안드로이드 앱을 개발을 하다보면 앱이 Background, Foreground 상태에 대한 이벤트를 탐지를 필요로 합니다. 일반적으로 ActivityManager의 Activity의 갯수와 Lifecycle에 따라 이벤트 탐지를 합니다. 모든 Activity의 Lifecycle을 관리한다는 것은 번거로운 일입니다. 하지만 Android Architecture Components를 이미 사용한다면 간단히 몇줄 만에 해당 이벤트를 탐지 할 수 있습니다.]]></summary></entry><entry><title type="html">Kotlin Coroutines – Retrofit2 + Coroutines 동시처리</title><link href="https://blog.kmshack.kr/Kotlin-Coroutines-Retrofit2-+-Coroutines-%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC/" rel="alternate" type="text/html" title="Kotlin Coroutines – Retrofit2 + Coroutines 동시처리" /><published>2018-05-12T00:00:00+09:00</published><updated>2018-05-12T00:00:00+09:00</updated><id>https://blog.kmshack.kr/Kotlin%20Coroutines%20%E2%80%93%20Retrofit2%20+%20Coroutines%20%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC</id><content type="html" xml:base="https://blog.kmshack.kr/Kotlin-Coroutines-Retrofit2-+-Coroutines-%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC/"><![CDATA[<p>Android의 스레딩 모델은 UI 스레드라고 불리는 싱글 스레드가 사용자의 인터페이스 렌더링, 이벤트 캡쳐 및 기타 여러 측면을 담당하는데 이는 모든 UI 프레임워크와 동일합니다. 네트워크 요청, DB 쿼리, 많은 연산과 같은 긴 작업을 수행하면 UI가 멈추어 ANR 오류가 발생합니다.</p>

<p><br /></p>
<h2 id="우리는-이런-문제를-해결-하기위해서-아래와-같은-몇가지-방식을-사용합니다">우리는 이런 문제를 해결 하기위해서 아래와 같은 몇가지 방식을 사용합니다.</h2>
<ol>
  <li>AsyncTask – 순서대로 짧은 연산 실행, 동시에 수행 할 수 있음</li>
  <li>Executors – 고정된 스레드 풀과 동시에 작업 실행</li>
  <li>Intent Service – 순서대로 긴 연산을 실행, 큐잉 처리함</li>
  <li>RxJava – 가장 인기 있으며, 안드로이드 프레임워크에서 지원하지 않음</li>
  <li>Raw Threads</li>
</ol>

<p>위의 방식은 훌륭하지만 올바르게 사용하기 위해선 미묘한 차이를 보입니다. 또, 디버깅과 취소처리가 간단하지 않습니다.</p>

<p><br /></p>
<h2 id="kotlin-coroutines">Kotlin Coroutines</h2>

<p>코틀린 코루틴는 순차적으로 일어나는 일을 비동기 처리하기위한 방법입니다. 코루틴을 생성하는 것은 스레드를 만드는 것에 비해 비용이 적게듭니다. 그 이유는</p>
<blockquote>
  <p>코루틴은 컴파일 기술을 통해 완벽하게 구현됩니다. (VM 또는 OS측면에서 지원이 필요없습니다). Suspend는 코드 변환을 통해 작동합니다.</p>
</blockquote>

<p>코루틴은 여전히 실험단계에 있으며, 이는 API가 앞으로 변화될 가능성을 의미합니다. 하지만 JetBrains은 이전 버전과 같은 호환성을 제공할것 이라고 약속했습니다. (<a href="https://stackoverflow.com/questions/46240236/can-experimental-kotlin-coroutines-be-used-in-production">여기를 통해 확인 해보세요.</a>)</p>

<p><br /></p>
<h2 id="code">Code</h2>

<p>코루틴을 사용하기위해서 함수를 suspend로 표시해야 모든 정상기능을 사용할 수 있습니다. 이러한 기능을 사용하기위해 실행및 비동기화(Launch &amp; Async)하는 코루틴 빌더가 필요합니다.</p>

<p><br /></p>
<h2 id="launch--async">Launch &amp; Async</h2>

<ul>
  <li>Launch – 현재 스레드를 차단하지 않고 새로운 코루틴을 싱행하고 코루틴을 제거하는데 사용 할 수 있는 작업으로 코루틴에 대한 참조를 반환합니다.</li>
  <li>Async – 새로운 코루틴을 실행하고 지연후 결과를 반환합니다.</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">actual</span> <span class="n">fun</span> <span class="nf">launch</span><span class="o">(</span>
    <span class="nl">context:</span> <span class="nc">CoroutineContext</span> <span class="o">=</span> <span class="nc">DefaultDispatcher</span><span class="o">,</span>
    <span class="nl">start:</span> <span class="nc">CoroutineStart</span> <span class="o">=</span> <span class="nc">CoroutineStart</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">,</span>
    <span class="nl">parent:</span> <span class="nc">Job</span><span class="o">?</span> <span class="o">=</span> <span class="kc">null</span><span class="o">,</span>
    <span class="nl">block:</span> <span class="n">suspend</span> <span class="nc">CoroutineScope</span><span class="o">.()</span> <span class="o">-&gt;</span> <span class="nc">Unit</span>
<span class="o">):</span> <span class="nc">Job</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">newContext</span> <span class="o">=</span> <span class="n">newCoroutineContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">parent</span><span class="o">)</span>
    <span class="n">val</span> <span class="n">coroutine</span> <span class="o">=</span> <span class="k">if</span> <span class="o">(</span><span class="n">start</span><span class="o">.</span><span class="na">isLazy</span><span class="o">)</span>
        <span class="nc">LazyStandaloneCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span> <span class="k">else</span>
        <span class="nc">StandaloneCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">active</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="n">coroutine</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">coroutine</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span>
    <span class="k">return</span> <span class="n">coroutine</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="n">actual</span> <span class="n">fun</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">async</span><span class="o">(</span>
    <span class="nl">context:</span> <span class="nc">CoroutineContext</span> <span class="o">=</span> <span class="nc">DefaultDispatcher</span><span class="o">,</span>
    <span class="nl">start:</span> <span class="nc">CoroutineStart</span> <span class="o">=</span> <span class="nc">CoroutineStart</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">,</span>
    <span class="nl">parent:</span> <span class="nc">Job</span><span class="o">?</span> <span class="o">=</span> <span class="kc">null</span><span class="o">,</span>
    <span class="nl">block:</span> <span class="n">suspend</span> <span class="nc">CoroutineScope</span><span class="o">.()</span> <span class="o">-&gt;</span> <span class="no">T</span>
<span class="o">):</span> <span class="nc">Deferred</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="n">val</span> <span class="n">newContext</span> <span class="o">=</span> <span class="n">newCoroutineContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">parent</span><span class="o">)</span>
    <span class="n">val</span> <span class="n">coroutine</span> <span class="o">=</span> <span class="k">if</span> <span class="o">(</span><span class="n">start</span><span class="o">.</span><span class="na">isLazy</span><span class="o">)</span>
        <span class="nc">LazyDeferredCoroutine</span><span class="o">(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span> <span class="k">else</span>
        <span class="nc">DeferredCoroutine</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;(</span><span class="n">newContext</span><span class="o">,</span> <span class="n">active</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="n">coroutine</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">coroutine</span><span class="o">,</span> <span class="n">block</span><span class="o">)</span>
    <span class="k">return</span> <span class="n">coroutine</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br /></p>
<ul>
  <li>CoroutineContext – 코루틴 빌더가 실행되는 스레드를 정의합니다.</li>
  <li>DefaultDispatcher – CommonPool을 사용합니다. 개발자는 실행해야하는 스레드를 지정할 수있는 모든 권한을가집니다. 다른 옵션은 UI 스레드에서 실행되는 UI입니다.</li>
  <li>CoroutineStart – 시작 방법 정의
    <ul>
      <li>DEFAULT - 즉시 실행을 시작</li>
      <li>LAZY - 코루틴을 느리게 시작</li>
      <li>ATOMIC - 자동으로 최적화된 방법으로 시작</li>
      <li>UNDISPATCHED - 분산 처리방법으로 시작</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<h3 id="예제">예제</h3>

<p><strong>연속적으로 처리하는 작업</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">private</span> <span class="n">lateinit</span> <span class="kt">var</span> <span class="nl">job1:</span> <span class="nc">Job</span>
    <span class="n">suspend</span> <span class="n">fun</span> <span class="nf">getThingsDone</span><span class="o">(</span><span class="nl">myThings:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
        <span class="c1">// Heavy computation going on here :D</span>
        <span class="k">return</span> <span class="n">myThings</span> <span class="o">+</span> <span class="mi">99</span>
    <span class="o">}</span>

    <span class="n">suspend</span> <span class="n">fun</span> <span class="nf">getThingsDoneAgain</span><span class="o">(</span><span class="nl">myThings:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
        <span class="c1">// Heavy computation going on here :D</span>
        <span class="k">return</span> <span class="n">myThings</span> <span class="o">+</span> <span class="mi">50</span>
    <span class="o">}</span>
    
    <span class="n">fun</span> <span class="nf">toDoWorkSerially</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">job1</span> <span class="o">=</span> <span class="n">launch</span><span class="o">(</span><span class="no">UI</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">work1</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDone</span><span class="o">(</span><span class="mi">23</span><span class="o">)</span> <span class="o">}</span>
                <span class="n">val</span> <span class="n">work2</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDoneAgain</span><span class="o">(</span><span class="mi">55</span><span class="o">)</span> <span class="o">}</span>
                
                <span class="n">val</span> <span class="n">result1</span> <span class="o">=</span> <span class="n">work1</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="n">tvResult1</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result1</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
                
                <span class="n">val</span> <span class="n">result2</span> <span class="o">=</span> <span class="n">work2</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="n">tvResult2</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result2</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
            
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">exception</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">()</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>UI (Main Thread)는 Context를 사용하여 새로운  코루틴 Launch를 만듭니다. 이 과정에서 백그라운드에서 무거운 작업을 수행하고 UI 스레드에서 두 개의 다른 코루틴을 실행합니다. Async는 새로운 코루틴을 생성하고 지연된것을 리턴하여 기다릴 수 있게 됩니다.</p>

<p>간단히 말해 코드는 work1과 work2가 실행될 때 순차적으로 실행되고 result1은 work1이 완료 될 때까지 기다린 다음 result2는 work2가 완료 될 때까지 기다립니다. result1과 result2는 기본 스레드인 CommonPool을 Context로 사용했기 때문에 백그라운드 스레드에서 가져옵니다. 모든 예외는 해당 try catch 블록에서 발견됩니다.</p>

<p><br />
<strong>동시에 처리 하는 작업</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="n">lateinit</span> <span class="kt">var</span> <span class="nl">job2:</span> <span class="nc">Job</span>

<span class="n">fun</span> <span class="nf">toDoWorkConcurrent</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">job2</span> <span class="o">=</span> <span class="n">launch</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">val</span> <span class="n">work1</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDone</span><span class="o">(</span><span class="mi">43</span><span class="o">)</span> <span class="o">}</span>
            <span class="n">val</span> <span class="n">work2</span> <span class="o">=</span> <span class="n">async</span> <span class="o">{</span> <span class="n">getThingsDoneAgain</span><span class="o">(</span><span class="mi">123</span><span class="o">)</span> <span class="o">}</span>

            <span class="n">val</span> <span class="n">result</span> <span class="o">=</span> <span class="n">computeResult</span><span class="o">(</span><span class="n">work1</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">work2</span><span class="o">.</span><span class="na">await</span><span class="o">())</span>

            <span class="n">withContext</span><span class="o">(</span><span class="no">UI</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">tvResult1</span><span class="o">.</span><span class="na">text</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span>
            <span class="o">}</span>

        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">exception</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">()</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="n">fun</span> <span class="nf">computeResult</span><span class="o">(</span><span class="nl">await:</span> <span class="nc">Int</span><span class="o">,</span> <span class="nl">await1:</span> <span class="nc">Int</span><span class="o">):</span> <span class="nc">Int</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">await</span> <span class="o">+</span> <span class="n">await1</span>
<span class="o">}</span>
    
</code></pre></div></div>

<p>위의 변수의 결과는 동시에 work1과 work2가 완료 될때만 이용할 수 있습니다.  이를 위해 await() 호출시 지연된 모든 객체가 호출됩니다.</p>

<p>코루틴 취소는 Job에서 cancel()을 통해 간단히 호출할 수 있습니다.</p>

<p><br /></p>
<h2 id="retrofit2--coroutines">Retrofit2 + Coroutines</h2>
<p>코루틴을 Retrofit2와 함께 사용하기 위해서는 Retrofit2 Kotlin Coroutines Adapter 라이브러리를 이용하여 Service 메소드의 반환 유형으로 Deferred 타입을 사용할 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">LoginRepository</span> <span class="nd">@Inject</span> <span class="n">constructor</span><span class="o">(</span><span class="kd">private</span> <span class="n">val</span> <span class="nl">apiService:</span> <span class="nc">APIService</span><span class="o">,</span> <span class="kd">private</span> <span class="n">val</span> <span class="nl">sharedPreferences:</span> <span class="nc">SharedPreferences</span><span class="o">)</span> <span class="o">{</span>

    <span class="n">fun</span> <span class="nf">getOTPForNumber</span><span class="o">(</span><span class="nl">phoneNumber:</span> <span class="nc">String</span><span class="o">):</span> <span class="nc">LiveData</span><span class="o">&lt;</span><span class="nc">OTPResult</span><span class="o">&gt;</span> <span class="o">{</span>
    
        <span class="n">val</span> <span class="n">loginResult</span> <span class="o">=</span> <span class="nc">MutableLiveData</span><span class="o">&lt;</span><span class="nc">OTPResult</span><span class="o">&gt;()</span>
        <span class="n">launch</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">request</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getOTPForNumber</span><span class="o">(</span><span class="n">phoneNumber</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">response</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">await</span><span class="o">()</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">isSuccessful</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Success</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">message</span><span class="o">()))</span>
                <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                    <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Error</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">code</span><span class="o">(),</span> <span class="n">response</span><span class="o">.</span><span class="na">message</span><span class="o">()))</span>
                <span class="o">}</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">loginResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">OTPResult</span><span class="o">.</span><span class="na">Exception</span><span class="o">(</span><span class="n">exception</span><span class="o">))</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">loginResult</span>
    <span class="o">}</span>
    
   <span class="n">fun</span> <span class="nf">getMyThings</span><span class="o">(</span><span class="nl">date:</span> <span class="nc">String</span><span class="o">,</span> <span class="nl">meal:</span> <span class="nc">Triple</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">,</span> <span class="nc">Boolean</span><span class="o">&gt;):</span> <span class="nc">LiveData</span><span class="o">&lt;</span><span class="nc">DispatchesResult</span><span class="o">&gt;</span> <span class="o">{</span>

        <span class="n">val</span> <span class="n">dispatchResult</span> <span class="o">=</span> <span class="nc">MutableLiveData</span><span class="o">&lt;</span><span class="nc">DispatchesResult</span><span class="o">&gt;()</span>
        <span class="n">launch</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">val</span> <span class="n">task1</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getDispatches</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">date</span><span class="o">,</span> <span class="n">meal</span><span class="o">.</span><span class="na">first</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">task2</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">getDispatches</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">date</span><span class="o">,</span> <span class="n">meal</span><span class="o">.</span><span class="na">second</span><span class="o">)</span>
                <span class="n">val</span> <span class="n">task3</span> <span class="o">=</span> <span class="n">apiService</span><span class="o">.</span><span class="na">checkAppDetails</span><span class="o">()</span>
                <span class="n">checkResult</span><span class="o">(</span><span class="n">task1</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">task2</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">task3</span><span class="o">.</span><span class="na">await</span><span class="o">(),</span> <span class="n">dispatchResult</span><span class="o">)</span>

            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nl">exception:</span> <span class="nc">Exception</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">dispatchResult</span><span class="o">.</span><span class="na">postValue</span><span class="o">(</span><span class="nc">DispatchesResult</span><span class="o">.</span><span class="na">Exception</span><span class="o">(</span><span class="n">exception</span><span class="o">))</span>
            <span class="o">}</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">dispatchResult</span>
    <span class="o">}</span>

     <span class="kd">private</span> <span class="n">fun</span> <span class="nf">checkResult</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">task1</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="n">task2</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">task3</span><span class="o">.</span><span class="na">isSuccessful</span> <span class="o">&amp;&amp;</span> <span class="n">task3</span><span class="o">.</span><span class="na">body</span><span class="o">()?.</span><span class="na">code</span> <span class="o">==</span> <span class="mi">1000</span><span class="o">))</span> <span class="o">{</span>
        
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>apiService 인터페이스가 기존 Call대신 Deferred를 반환하기 때문에 이를 통해 이전에 다룬 코루틴을 그대로 사용할 수 있습니다.</p>

<p><br /></p>
<h2 id="결론">결론</h2>
<p>코루틴을 사용하면 코드를 쉽게 읽을 수 있는 비동기 코드 작성이 가능하며, 오류및 유지 보수를 줄 일 수 있습니다. 또한 코드의 경량화는 말할것도 없습니다.</p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><category term="코틀린" /><summary type="html"><![CDATA[Android의 스레딩 모델은 UI 스레드라고 불리는 싱글 스레드가 사용자의 인터페이스 렌더링, 이벤트 캡쳐 및 기타 여러 측면을 담당하는데 이는 모든 UI 프레임워크와 동일합니다. 네트워크 요청, DB 쿼리, 많은 연산과 같은 긴 작업을 수행하면 UI가 멈추어 ANR 오류가 발생합니다.]]></summary></entry><entry><title type="html">Fragment 트랜지션 쉽게 적용하기</title><link href="https://blog.kmshack.kr/Fragment-%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98-%EC%89%BD%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0/" rel="alternate" type="text/html" title="Fragment 트랜지션 쉽게 적용하기" /><published>2018-03-19T00:00:00+09:00</published><updated>2018-03-19T00:00:00+09:00</updated><id>https://blog.kmshack.kr/Fragment%20%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98%20%EC%89%BD%EA%B2%8C%20%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://blog.kmshack.kr/Fragment-%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98-%EC%89%BD%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0/"><![CDATA[<p>이 글은 Fragment에서 멋진 전환 효과를 적용하기 위한 글로 간단하지만 알아두면 쉽게 적용 할 수 있는 부분에 대해 다루려고 합니다. 얼마전 <a href="https://twitter.com/chrisbanes">Chris Banes</a>가 좀 더 멋진 장면 효과를 구현한 <a href="https://github.com/chrisbanes/tivi/commit/8b6951371467744c9507fe1e680f4dd6f2f139ef">ColumnedChangeBounds</a> 코드도 공개하였기 때문에 어떻게 사용할 수 있을지에 대해 알아 보겠습니다.</p>

<iframe width="100%" height="400" src="https://www.youtube.com/embed/ntO_JcaQC6s" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen=""></iframe>

<p>일반적으로 Fragment전환시 아무런 전환 효과를 주지 않습니다. commit과 동시에 컨텐츠 화면이 심심하게 바뀝니다. Fragment도 Activity에서 처럼 전환 효과를 주기위해서는 <code class="language-plaintext highlighter-rouge">sharedElementViews</code>를 이용하여 전환하고 싶은 View를 추가하면 됩니다. 그리고 트랜잭션 최적화 작업을 위해 <code class="language-plaintext highlighter-rouge">etReorderingAllowed()</code>속성을 허용합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">supportFragmentManager</span><span class="o">.</span><span class="na">beginTransaction</span><span class="o">()</span>
  <span class="o">.</span><span class="na">setReorderingAllowed</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
  <span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">container</span><span class="o">,</span> <span class="nc">GridFragment</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">count</span><span class="o">))</span>
  <span class="o">.</span><span class="na">addToBackStack</span><span class="o">(</span><span class="s">"detail"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">apply</span> <span class="o">{</span>
      <span class="n">val</span> <span class="nl">topFragment:</span> <span class="nc">Int</span> <span class="o">=</span> <span class="n">supportFragmentManager</span><span class="o">.</span><span class="na">fragments</span><span class="o">.</span><span class="na">size</span>
      <span class="n">val</span> <span class="nl">fragment:</span> <span class="nc">GridFragment</span> <span class="o">=</span> <span class="n">supportFragmentManager</span><span class="o">.</span><span class="na">fragments</span><span class="o">[</span><span class="n">topFragment</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="n">as</span> <span class="nc">GridFragment</span>

      <span class="n">val</span> <span class="nl">recyclerView:</span> <span class="nc">RecyclerView</span> <span class="o">=</span> <span class="n">fragment</span><span class="o">.</span><span class="na">recyclerview</span>

      <span class="kt">var</span> <span class="n">viewCount</span> <span class="o">=</span> <span class="mi">0</span>
      <span class="k">for</span> <span class="o">(</span><span class="n">itemView</span> <span class="n">in</span> <span class="n">recyclerView</span><span class="o">.</span><span class="na">children</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">addSharedElement</span><span class="o">(</span><span class="n">itemView</span><span class="o">,</span> <span class="n">viewCount</span><span class="o">.</span><span class="na">toString</span><span class="o">())</span>
          <span class="n">viewCount</span><span class="o">++</span>
      <span class="o">}</span>

  <span class="o">}</span>
  <span class="o">.</span><span class="na">commit</span><span class="o">()</span>
</code></pre></div></div>

<p>위의 코드에서 <code class="language-plaintext highlighter-rouge">addSharedElement</code>를 통해 현재 보여지고 있는 View에 각각 전환될 이름을 부여 해줍니다. 그럼 Fragment가 전환된 View에 해당 전환된 이름의 View를 찾아 트랜지션 됩니다.</p>

<p><br /></p>
<h2 id="문제점">문제점</h2>
<p>새로운 Fragment가 생성되면 데이터를 가져오거나 이미지를 불러오고 레이아웃을 구성하고 그리는 시간이 걸리기 때문에 전환 효과는 자연스럽지 않습니다. 이런 구성을 하는 동안 애니메이션이 끝나게 되는 경우 아무런 효과를 보지 못할 수 있습니다. 그렇기 때문에 장면 효과를 데이터나 뷰를 구성하기 전까지 지연할 필요가 있습니다.</p>

<p><code class="language-plaintext highlighter-rouge">postponeEnterTransition()</code>, <code class="language-plaintext highlighter-rouge">startPostponedEnterTransition()</code>을 이용하면 장면 효과를 지연하고 시작할 수 있습니다. 우리는 뷰가 그려진다는 사실을 <code class="language-plaintext highlighter-rouge">OnPreDrawListener</code>를 통해 인지 할 수 있습니다. 최근에 지원된 코틀린 라이브러리 <a href="https://github.com/android/android-ktx">KTX</a>를 이용한다면 <code class="language-plaintext highlighter-rouge">OnPreDrawListener</code>대신 <code class="language-plaintext highlighter-rouge">doOnPreDraw</code>를 통해 쉽게 콜백 받을 수 있습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">postponeEnterTransition</span><span class="o">()</span>
 
<span class="n">recyclerView</span><span class="o">.</span><span class="na">doOnPreDraw</span> <span class="o">{</span>
    <span class="n">startPostponedEnterTransition</span><span class="o">()</span>
</code></pre></div></div>

<p><br />
이제 Fragment 전환 효과를 위한 준비는 모두 끝났습니다. 어떻게 트랜지션이 이루어 질지에 대한 정의만 해주면 되는데 기본적으로 몇가지를 지원해는데 Bounds가 변경되기 때문에 <code class="language-plaintext highlighter-rouge">ChangeBounds()</code> 트랜지션을 설정 하도록 하겠습니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">override</span> <span class="n">fun</span> <span class="nf">onCreate</span><span class="o">(</span><span class="nl">savedInstanceState:</span> <span class="nc">Bundle</span><span class="o">?)</span> <span class="o">{</span>
      <span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">(</span><span class="n">savedInstanceState</span><span class="o">)</span>
      <span class="n">sharedElementEnterTransition</span> <span class="o">=</span> <span class="nc">ChangeBounds</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p><br />
좀 더 멋진 효과를 보기위해서는 Chris Banes과 같이 <code class="language-plaintext highlighter-rouge">ColumnedChangeBounds</code>를 직접 구현하면 됩니다. 이 글에서 구현된 모든 코드는 <a href="https://github.com/kmshack/FragmentTransitions">https://github.com/kmshack/FragmentTransitions</a>에 공개됩니다.</p>]]></content><author><name>kmshack</name></author><category term="안드로이드" /><summary type="html"><![CDATA[이 글은 Fragment에서 멋진 전환 효과를 적용하기 위한 글로 간단하지만 알아두면 쉽게 적용 할 수 있는 부분에 대해 다루려고 합니다. 얼마전 Chris Banes가 좀 더 멋진 장면 효과를 구현한 ColumnedChangeBounds 코드도 공개하였기 때문에 어떻게 사용할 수 있을지에 대해 알아 보겠습니다.]]></summary></entry></feed>