<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>공학도의 잡다한 시선</title>
    <link>https://whyprogrammer.tistory.com/</link>
    <description>뷰티 영화 프로그램 IT 도서 책 게임 등 후기 리뷰 / 프로그래밍</description>
    <language>ko</language>
    <pubDate>Mon, 1 Jun 2026 02:21:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>상추님</managingEditor>
    <image>
      <title>공학도의 잡다한 시선</title>
      <url>https://t1.daumcdn.net/cfile/tistory/234A754F58C56F9004</url>
      <link>https://whyprogrammer.tistory.com</link>
    </image>
    <item>
      <title>R8 Full Mode 적용 시 유의 사항 (default compat mode 와 차이점)</title>
      <link>https://whyprogrammer.tistory.com/656</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;R8 FAQ&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;R8 은 ProGuard 와 동일한 문법을 사용&lt;/li&gt;
&lt;li&gt;즉 ProGuard 와 호환 되도록 최대화&lt;/li&gt;
&lt;li&gt;하지만 다른 부분도 있음. R8 은 최적화 모드가 다름 (2가지 제공)&lt;/li&gt;
&lt;li&gt;Compatibility 모드 와 Full 모드를 제공&lt;/li&gt;
&lt;li&gt;Compatibility 모드가 default&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R8 uses the same configuration specification language as ProGuard, and tries to be compatible with ProGuard. However as R8 has different optimizations it can be necessary to change the configuration when switching to R8. R8 provides two modes, R8 compatibility mode and R8 full mode. R8 compatibility mode is default in Android Studio and is meant to make the transition to R8 from ProGuard easier by limiting the optimizations performed by R8.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;R8 full mode&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비-호환 모드 라고 일컬으며, full 모드 라고도 한다&lt;/li&gt;
&lt;li&gt;좀 더 공격적인 최적화를 진행한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가적인 ProGuard rules 설정이 필요할 수도 있다는 것. (비호환)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;android.enableR8.fullMode=true 를 &lt;a href=&quot;http://gradle.properties&quot;&gt;gradle.properties&lt;/a&gt; 에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 차이점:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스가 유지되더라도 기본 생성자(&amp;lt;init&amp;gt;() ) 는 암묵적으로 유지되지 않습니다.&lt;/li&gt;
&lt;li&gt;ldc , instanceof , checkcast 와 함께 사용 되는 타입에 대해 기본 생성자(&amp;lt;init&amp;gt;()) 는 암묵적으로 유지 되지 않습니다. 1️⃣&lt;/li&gt;
&lt;li&gt;-keepclassmembers 규칙은 특정 클래스의 필드나 메서드를 보호 하기는 하지만, 그 클래스 자체가 인스턴스화 되지는 않을 수 있음. 만약 클래스가 리플랙션(reflection)을 통해서만 인스턴스화된다면, 그 클래스는 -keep 규칙을 사용해 명시적으로 보호해야 함. 그렇지 않으면 R8 이 해당 클래스를 제거할 수 있음 2️⃣&lt;/li&gt;
&lt;li&gt;인터페이스 에서 기본적으로 구현된 메서드는 Java8 이후로 도입되었으며, 인터페이스를 구현하는 클래스가 이 메서드를 반드시 구현하지 않아도 되도록 합니다. 하지만 R8 은 추상 메서드와 기본메서드를 다르게 취급하여 기본 메서드는 추상 메서드와 동일하게 취급되지 않기 떄문에 명시적으로 보호하지 않으면 제거될 수 있음 (&amp;rdquo;default method&amp;rdquo; 가 제거되면 이를 사용하는 클래스가 정상적으로 동작하지 않을 수 있음) 3️⃣&lt;/li&gt;
&lt;li&gt;Signature 같은 속성(attrivute) 이나 Annotation 은 해당 클래스, 메서드, 필드가 -keep 규칙에 의해 보호되고 있을 때만 유지된다. 단순히 -keepattributes 규칙을 사용한다고 해서 모든 속성(attribute) 이나 Annotation 이 자동으로 보호 되지는 않는다. 4️⃣
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;attribute 와 annotation 을 보호하기 위한 가장 최소한의 규칙은 -keep[classmembers], allowshrinking, allowoptimization , allowobfuscation, allowaccessmodification class-specification 이며 해당 규칙은 shrinking, optimization, obfuscation, access modification 을 허용합니다.&lt;/li&gt;
&lt;li&gt;full 모드 에서는 클래스 간의 관계를 설명하는 속성 ex) InnerClass 와 EnclosingMethod 와 같은 속성은 양쪽 클래스가 모두 보호되어야 함. 그렇지 않으면 해당 속성들이 제거될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최적화와 최소화 에서 SourceFile 속성은 항상 파일 이름을 다시 작성 (파일 이름을 모두 SourceFile 로) 합니다. 즉 원본 소스파일 이름을 알 수 없게 처리됩니다. -renamesourcefileattribute 옵션을 사용하면 SourceFile 속성에 사용자 정의 이름을 설정가능하고 이 옵션을 통해 원본 소스 파일 이름을 대신할 값을 지정할 수 있음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 항상 mapping 파일이 생성되고 이 파일에는 원본소스 파일 이름과 난독화된 파일 이름 간의 매핑 정보가 포함됩니다. 5️⃣&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣&amp;nbsp;타입 검사에 사용되는 클래스의 기본 생성자 보호&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시 코드 에서 Example 클래스는 타입 검사 에만 활용 되므로 기본 생성자를 생성할 필요가 없습니다. 이 경우 R8 공격적 최적화가 진행되어 기본 생성자가 제거 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Example

fun main() {
    // 'Example' 클래스는 실제로 인스턴스가 생성되지 않고 타입 체크에만 사용됨
    val obj: Any = &quot;Test String&quot;

    if (obj is Example) {
        println(&quot;obj는 Example 타입입니다.&quot;)
    } else {
        println(&quot;obj는 Example 타입이 아닙니다.&quot;)
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣&amp;nbsp;Reflection 으로만 생성되는 클래스 보호&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시 에서 MyClass 는 리플렉션을 통해서만 인스턴스화하고 있음. 일반적으로 코드에서 직접 인스턴스화 하지 않았기 떄문에, R8 은 이 클래스를 불필요하다고 판단하고 제거할 수 있다. 즉 -keep 옵션을 지정해야함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 ClassNotFoundException 이 발생 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MyClass {
    val myField: String = &quot;Hello&quot;

    fun myMethod() {
        println(&quot;This is myMethod.&quot;)
    }
}

fun main() {
    val clazz = Class.forName(&quot;MyClass&quot;)
    val instance = clazz.getDeclaredConstructor().newInstance()

    // 리플렉션을 통해 메서드 호출
    val method = clazz.getDeclaredMethod(&quot;myMethod&quot;)
    method.invoke(instance)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 아래 코드 만으로는 클래스가 유지되지 않을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-keepclassmembers class MyClass {
    &amp;lt;fields&amp;gt;;
    &amp;lt;methods&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 추가 규칙을 정의하면 안전할 것이다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;-keep class MyClass

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣&amp;nbsp;interface 의 default 메서드 보호&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드에서 MyInterface 는 두가지 메서드를 포함하는데, 하나는 추상, 하나는 default 메서드 이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface MyInterface {
    fun abstractMethod()

    fun defaultMethod() {
        println(&quot;This is the default method.&quot;)
    }
}

class MyClass : MyInterface {
    override fun abstractMethod() {
        println(&quot;Implementing the abstract method.&quot;)
    }
}

fun main() {
    val myClass = MyClass()
    myClass.abstractMethod()  // 이 메서드는 구현되어 있음
    myClass.defaultMethod()   // 이 메서드는 인터페이스에서 기본적으로 구현됨
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R8 최적화 단계에서 defaultMethod() 가 불필요 하다고 판단되어 제거될 가능성이 있음. 추상 메서드는 기본적으로 제거 대상이 아니라 안전하지만 기본 메서드는 명시적으로 보호하지 않으면 위험할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-keep interface MyInterface {
    void defaultMethod();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 규칙을 추가하여 방어할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣&amp;nbsp;Inner 클래스와 Outer 클래스 관계 보호&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class OuterClass {
    inner class InnerClass {
        fun innerMethod() {
            println(&quot;This is an inner method.&quot;)
        }
    }

    fun outerMethod() {
        println(&quot;This is an outer method.&quot;)
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 에서 InnerClass 는 OuterClass 의 내부 클래스로 해당 정보는 EnclosingMethod 와 InnerClass 속성으로 바이트 코드에 기록된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-keepclassmembers class OuterClass$InnerClass {
    &amp;lt;init&amp;gt;();
    void innerMethod();
}
-keepattributes InnerClasses,EnclosingMethod

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 규칙으로 인해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;keepclassmembers 규칙은 OuterClass$InnerClass 클래스의 생성자와 innerMethod 메서드를 유지합니다.&lt;/li&gt;
&lt;li&gt;keepattributes InnerClasses,EnclosingMethod는 InnerClasses와 EnclosingMethod 속성을 보호하여 InnerClass와 OuterClass 사이의 관계를 유지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 OuterClass가 제거된다면, InnerClass와의 관계를 나타내는 EnclosingMethod 속성도 무의미해지므로 제거될 수 있습니다. 비호환 모드(non-compat mode)에서는 이러한 관계를 유지하기 위해 양쪽 클래스가 모두 보호되어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5️⃣&amp;nbsp;파일명 변수 사용&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// MySource.kt
class MyClass {
    fun greet() {
        println(&quot;Hello, World!&quot;)
    }
}

fun main() {
    val myClass = MyClass()
    myClass.greet()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드가 컴파일 되면 바이트 코드에는 해당 파일이 MySource.kt 라는 이름에서 유래되었다는 정보가 SourceFile 속성에 포함되게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;-renamesourcefileattribute MyCustomSource

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 옵션을 사용하면 SourceFile 명칭을 MyCustomSource 으로 변경할 수 있다. 사용하는 이유는 디버깅 시 일관된 파일명을 가지도록 하기 위함이다. 실제 디버깅 시에는 매핑 파일을 사용 하므로 문제는 없다.&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;-keepattributes SourceFile, SourceDir

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 해당 옵션을 사용하려면 SourceFile 이 존재 해야 하므로 위 옵션을 추가하여 함께 보존해야 해당 옵션이 효과를 발휘할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In non-compat mode, also called &amp;ldquo;full mode&amp;rdquo;, R8 performs more aggressive optimizations, meaning additional ProGuard configuration rules may be required. Full mode can be enabled by adding&amp;nbsp;android.enableR8.fullMode=true&amp;nbsp;in the&amp;nbsp;gradle.properties&amp;nbsp;file. The main differences compared to R8 compatibility mode are:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;The default constructor (&amp;lt;init&amp;gt;()) is not implicitly kept when a class is kept.&lt;/li&gt;
&lt;li&gt;The default constructor (&amp;lt;init&amp;gt;()) is not implicitly kept for types which are only used with&amp;nbsp;ldc,&amp;nbsp;instanceof&amp;nbsp;or&amp;nbsp;checkcast.&lt;/li&gt;
&lt;li&gt;The enclosing classes of fields or methods that are matched by a&amp;nbsp;keepclassmembers&amp;nbsp;rule are not implicitly considered to be instantiated. Classes that are only instantiated using reflection should be kept explicitly with a&amp;nbsp;keep&amp;nbsp;rule.&lt;/li&gt;
&lt;li&gt;Default methods are not implicitly kept as abstract methods.&lt;/li&gt;
&lt;li&gt;Attributes (such as&amp;nbsp;Signature) and annotations are only kept for classes, methods and fields which are matched by keep rules even when&amp;nbsp;keepattributes&amp;nbsp;is specified. The weakest rule that will keep annotations and attributes is&amp;nbsp;keep[classmembers],allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class-specification&amp;nbsp;Additionally, for attributes describing a relationship such as&amp;nbsp;InnerClass&amp;nbsp;and&amp;nbsp;EnclosingMethod, non-compat mode requires both endpoints being kept.&lt;/li&gt;
&lt;li&gt;When optimizing or minifying the&amp;nbsp;SourceFile&amp;nbsp;attribute will always be rewritten to&amp;nbsp;SourceFile&amp;nbsp;unless&amp;nbsp;renamesourcefileattribute&amp;nbsp;is used in which case the provided value is used. The original source file name is in the mapping file and when optimizing or minifying a mapping file is always produced.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md&quot;&gt;https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md&lt;/a&gt;&lt;/p&gt;</description>
      <category>java</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/656</guid>
      <comments>https://whyprogrammer.tistory.com/656#entry656comment</comments>
      <pubDate>Mon, 12 Aug 2024 11:10:50 +0900</pubDate>
    </item>
    <item>
      <title>ChatGPT 로 Commit Message 생성하기</title>
      <link>https://whyprogrammer.tistory.com/655</link>
      <description>&lt;h1&gt;ChatGPT 로 Commit Message 생성하기&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유용한 커밋 메세지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit 메세지를&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발, 리팩토링 진행 시 작업을 세분화하고 이력을 명확히 남기고
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 작업은 보통 한개의 jira 와 관련이 있지만. QA 이슈 지라가 발행되는 경우 jira 번호 링크하여 추적에 도움&lt;/li&gt;
&lt;li&gt;개발, 버그 수정, 새기능 추가, 리팩토링 을 한번에 하는 것이 아닌 나눠 수행하도록 상기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코드 리뷰 시, 팀원들의 이해를 돕고 (불가피하게 많은 량의 코드를 pr로 올리는 경우 commit msg가 도움)&lt;/li&gt;
&lt;li&gt;문제 추적을 용이하게 하고(필요시 각 단위작업 rollback 용이성 확보)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하기 위해 작성을 하곤 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수명, 함수명 못지않게 고민이 되는 것이 commit 메세지 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글 에서는 Commit Message 작성 시 ChatGPT 의 도움을 받아 업무 만족도와 효율성을 높이는 방법에 대해 공유합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려 해야 하는 사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;사내 코드 or 사내 프로젝트에 머지될 본인이 작성한 코드&amp;rdquo; 는 외부로 유출 되어서는 안된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 내용 위주로 사전 정보를 GPT 에게 제공하도록 한다. (불가피한 경우 코드는 보안 위험이 없는 정도로 재작성하여 일부만 넘길 수도 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;본인이 방출 시킨 코드에 대한 의도가 무엇인지 명확히 파악 하고 있어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 의도로 무슨 코드를 작성한지 본인도 명확히 글로 풀어낼 수 없으면 프롬프트 작성 자체가 불가능 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프롬프트 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 작성은 사실 정해진 정답은 없지만, 어느정도 정립되어지고 있는 여러 수법들이 있습니다. 이번 글에서는 여러 전략들 중 하나인 Few Shot 전략을 통해서 Commit Message 를 생성해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Few Shot 전략이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input 으로 몇 개(few)의 예시(example) 를 LLM 에게 주고 결과를 유추해내는 작업 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0-Shot 프롬프트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT에 가장 일반적으로 사용하고 있는 질문도 Few Shot 기법에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 주지 않고, &amp;ldquo;설명해 줘&amp;rdquo;, &amp;ldquo;답변해 줘&amp;rdquo;, &amp;ldquo;x가 궁금한데 알려줘&amp;rdquo; 등의 모든 지시 or 역할 프롬프트는 0-shot 프롬프트에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 0-Shot 프롬프트를 통해 커밋 메세지를 작성하도록 GPT 에게 유도하는 것에 대한 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;You:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;나의 안드로이드 앱에서 뒤로가기 버튼을 누르면 앱이 죽는 문제를 해결했어 이거에 대한 커밋 메세지를 만들어줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ChatGPT:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;앱에서 발생한 뒤로가기 버튼 문제를 해결한 것에 대한 커밋 메시지를 작성하겠습니다. 
커밋 메시지는 간결하고 명확해야 하며, 변경 사항에 대한 핵심적인 정보를 포함해야 합니다. 
다음은 그러한 조건을 충족하는 예시입니다:

```vbnet
&quot;Fix crash issue on back button press in Android app&quot;

Detailed explanation:
Resolved an issue where pressing the back button caused the app to crash. This was achieved by modifying the onBackPressed() method to handle the back navigation properly, ensuring a smooth user experience.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메시지는 문제의 본질을 명확하게 설명하고, 어떤 방식으로 해결했는지에 대한 간략한 정보를 제공합니다. 커밋 메시지는 기술적인 디테일보다는 변경 사항의 요약을 제공하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;### 1-Shot 프롬프트

1-Shot 프롬프트는 1개의 예제를 함께 input 으로 전달하고 답을 유도해내는 방법입니다. 0-Shot 에 비해 비교적 정확한 답변을 얻을 수 있는 방법으로 간주됩니다.

아래는 1-Shot 프롬프트를 통해 커밋 메세지를 작성하도록 GPT 에게 유도하는 것에 대한 예시입니다.

**You:**

```markdown
Input: 미사용되는 리소스가 해제되지 않아 메모리가 누수되는 문제를 해결했습니다.
commit message: Fix: Resolve Memory Leak by Releasing Unused Resources
Detailed Description: Fixed an issue where unused resources were not being released, causing a memory leak. This update ensures that all unused resources are properly disposed of, preventing unnecessary memory consumption and improving the overall performance and stability of the application.

Input: 안드로이드 앱에서 뒤로가기 버튼을 누르면 앱이 죽는 문제를 해결했습니다.
commit message:
Detailed Description:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;The commit message for the Android app issue could be succinctly described as:

Commit message: Fix: Resolve App Crash on Back Button Press in Android

Detailed Description: Addressed an issue in the Android application where pressing the back button resulted in the app crashing. This fix involves correcting the back button's event handler to ensure smooth and error-free navigation. With this update, the app now handles the back button press correctly, enhancing user experience and application reliability.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 의도한 답변과 가까워진 모습 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Few-Shot 프롬프트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개 이상의 예시를 제공하고 답을 유도해내는 방법입니다. LLM 이 답을 구조적으로 구체화하여 답변할 수 있도록 하기 위한 방법으로 0~1 Shot 에 비해 더욱 더 정확한 답을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;You:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Input: 미사용 코드 임시로 주석화
commit message: Chore: Temporarily Comment Out Unused Code
Detailed Description: I've temporarily commented out sections of code that are currently not in use. This action is taken as a precaution to avoid any potential conflicts or performance issues, while keeping the code available for future reference or reactivation.

Input: 로그인 필요한 페이지 경우 test code 가 실패하여 수정
commit message: Fix: Test Failure for Login-Required Pages
Detailed Description: Adjusted the test code to handle scenarios where login is required. The test now includes steps for authentication, ensuring it successfully navigates through pages that demand user login. This fix ensures our automated tests remain robust and reflective of real-user scenarios, particularly for features gated behind authentication.

Input: toast message 를 출력해야하는 케이스에 대한 핸들링을 ExecutorToastMsg 로 이동하였습니다.
commit message: Refactor: Move Toast Message Handling to ExecutorToastMsg
Detailed Description: I refactored the code to move the handling of toast messages to the ExecutorToastMsg class. This change centralizes toast message processing, leading to cleaner and more maintainable code.

Input: 안드로이드 앱에서 뒤로가기 버튼을 누르면 앱이 죽는 문제를 해결했습니다.
commit message: 
Detailed Description: &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ChatGPT:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Commit Message: Fix: Resolve Crash Issue on Android App Back Button Press

Detailed Description: Addressed a critical issue in the Android app where pressing the back button caused the application to crash. This fix involved modifying the app's back button handling logic to ensure stable functionality. This change not only enhances the user experience but also ensures the app's reliability, particularly for navigating through various screens or functions within the app.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역: Android 앱에서 뒤로 가기 버튼을 누르면 앱이 충돌하는 심각한 문제를 해결했습니다. 이 수정에는 앱의 뒤로 가기 버튼 처리 로직을 수정하여 안정적인 기능을 보장하는 작업이 포함되었습니다. 이 변경으로 사용자 경험이 향상되었을 뿐만 아니라 특히 앱 내의 다양한 화면이나 기능을 탐색할 때 앱의 안정성이 보장되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤가요? 불필요한 메세지 없이 의도한 내용만 답변해주는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tip:&lt;/p&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;...(생략)

Input: 특정기기(Galaxy S21)에서 카메라 기능을 실행하고 뒤로가기 버튼을 누르면 앱이 죽는(Crash) 되는 문제를 해결했습니다.
commit message: 
Detailed Description: &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Input 으로 더욱 구체화된 작업 내용을 전달할 수록 좋은 결과를 얻을 수 있습니다.)&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Commit Message: Fix: Resolve App Crash on Galaxy S21 When Using Camera and Pressing Back

Detailed Description: Addressed an issue causing the app to crash on the Galaxy S21 when the camera function is used and the back button is pressed. This fix involved modifying the camera handling logic to ensure stability and prevent crashes. This change specifically targets the Galaxy S21, enhancing the app's reliability and user experience on this device.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역: 갤럭시 S21에서 카메라 기능을 사용하고 뒤로 가기 버튼을 눌렀을 때 앱이 충돌하는 문제를 해결했습니다. 이 수정에는 안정성을 보장하고 충돌을 방지하기 위해 카메라 처리 로직을 수정하는 작업이 포함되었습니다. 이 변경 사항은 특히 갤럭시 S21을 대상으로 하며, 이 기기에서 앱의 안정성과 사용자 경험을 향상시킵니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Few-Shot 이대로 좋은가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 케이스를 3개로 늘려 우리가 원하는 output 을 유도하는데 어느정도 성공한 것으로 보여진다. 필요에 따라 예시를 늘려가며 commit message 를 얻기위한 고정 프롬프트를 사용할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit message 는 당분간 걱정이 없겠으나,, 만약 다른 목적으로 GPT 를 활용 해야할 때는 또 다른 프롬프트 전략을 채택하고 프롬프트를 개발하고 이를 검증하고 사용하는 단계를 거쳐야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 프롬프트를 작성하기도 번거롭고, 프롬프트 작성에 어려움이 있다면 원하는 결과를 도출하기 어려울 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나만의 GPT&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9PRJP/btsDxmKpN0n/k4BZkeSYhK87BBdk5jNzkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9PRJP/btsDxmKpN0n/k4BZkeSYhK87BBdk5jNzkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9PRJP/btsDxmKpN0n/k4BZkeSYhK87BBdk5jNzkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9PRJP%2FbtsDxmKpN0n%2Fk4BZkeSYhK87BBdk5jNzkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;135&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고맙게도 OpenAi 에서 My GPTs 라는 서비스를 진행중인데, 이를 통해 특정 목적을 가진 GPT 를 생성해줄 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1141&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lT9Nt/btsDrgLp3Hw/twD4b6wgmFpZMukETZ9hz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lT9Nt/btsDrgLp3Hw/twD4b6wgmFpZMukETZ9hz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lT9Nt/btsDrgLp3Hw/twD4b6wgmFpZMukETZ9hz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlT9Nt%2FbtsDrgLp3Hw%2FtwD4b6wgmFpZMukETZ9hz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;1141&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;1141&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 통해서 명령(프롬프트) 을 설정할 수있고 사전 지식도 업로드 가능하며, 더 세세한 설정 또한 포함되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 프롬프트 작성은 어차피 해야하는 것인가.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPT Builder&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM0Pp4/btsDwDez53d/fQSmj3mNrvVzb4kVDESSi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM0Pp4/btsDwDez53d/fQSmj3mNrvVzb4kVDESSi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM0Pp4/btsDwDez53d/fQSmj3mNrvVzb4kVDESSi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM0Pp4%2FbtsDwDez53d%2FfQSmj3mNrvVzb4kVDESSi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;399&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT Builder 를 통해서 원하는 GPT 를 만드는데 도움을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령. 다시 우리가 원했던 Commit Message 를 전문적으로 생성해주는 GPT 를 빌드해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRNuPT/btsDwDeAmJl/FjzJJZOUpbI8so1wBCsoiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRNuPT/btsDwDeAmJl/FjzJJZOUpbI8so1wBCsoiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRNuPT/btsDwDeAmJl/FjzJJZOUpbI8so1wBCsoiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRNuPT%2FbtsDwDeAmJl%2FFjzJJZOUpbI8so1wBCsoiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;332&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT Builder: (output)&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;Great! Let's give your GPT a name. 
How about &quot;Commit Composer&quot;? 
Do you like this name or would you prefer something else?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT 의 이름까지 추천 해주는 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsgpWe/btsDqlGxDZI/5At0GXCBGZCMtrziL9YeX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsgpWe/btsDqlGxDZI/5At0GXCBGZCMtrziL9YeX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsgpWe/btsDqlGxDZI/5At0GXCBGZCMtrziL9YeX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsgpWe%2FbtsDqlGxDZI%2F5At0GXCBGZCMtrziL9YeX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;266&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 나만의 GPT 가 탄생 했다. 무려 프로필 이미지도 생성 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;1015&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdya1l/btsDsYQ6Wx4/uPPhStvysTqawYCLCwKWV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdya1l/btsDsYQ6Wx4/uPPhStvysTqawYCLCwKWV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdya1l/btsDsYQ6Wx4/uPPhStvysTqawYCLCwKWV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdya1l%2FbtsDsYQ6Wx4%2FuPPhStvysTqawYCLCwKWV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;655&quot; height=&quot;1015&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;1015&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 사용 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JdQ8b/btsDs02lrxJ/SWHvvXJkYb5CuQdVuGfMT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JdQ8b/btsDs02lrxJ/SWHvvXJkYb5CuQdVuGfMT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JdQ8b/btsDs02lrxJ/SWHvvXJkYb5CuQdVuGfMT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJdQ8b%2FbtsDs02lrxJ%2FSWHvvXJkYb5CuQdVuGfMT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;283&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명쾌하지 않은 작업 내용을 전달 하니 좀 더 세부적인 내용을 알아서 요청하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFCcMo/btsDqTCZDOA/8eKkk3kQ87aS6FHaJKkeK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFCcMo/btsDqTCZDOA/8eKkk3kQ87aS6FHaJKkeK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFCcMo/btsDqTCZDOA/8eKkk3kQ87aS6FHaJKkeK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFCcMo%2FbtsDqTCZDOA%2F8eKkk3kQ87aS6FHaJKkeK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;626&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역: 이 커밋은 갤럭시 S21에서 카메라 기능을 사용할 때 발생하던 기기별 충돌 문제를 해결합니다. 이 문제는 카메라를 실행한 후 뒤로 버튼을 눌렀을 때 관찰되었습니다. 조사 결과, 이 문제는 카메라 &lt;b&gt;API 상호 작용 및 활동 수명 주기가 갤럭시 S21에서 특별히 관리되는 방식과 관련이 있는 것으로 밝혀&lt;/b&gt;졌습니다. 이 문제를 해결하기 위해 카메라 처리 및 뒤로 버튼 누르기 로직을 수정하여 이 기기 모델에서 호환성과 안정성을 보장했습니다. 이 업데이트는 갤럭시 S21에서 앱의 안정성을 크게 개선하여 원활한 카메라 경험을 제공하고 카메라 기능에서 벗어날 때 예기치 않은 앱 종료를 방지합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀더 명확한 내용을 입력하니 commit message 를 생성 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이런게 가능한 이유는 GPT Builder 와 대화하며 요청한 내용을 기반으로 GPT가 프롬프트를 직접 작성해주었기 때문인데, Configure 탭에서 이를 확인해볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co090e/btsDxLiTveu/PBdL1Cl67qUQAxczhAEXk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co090e/btsDxLiTveu/PBdL1Cl67qUQAxczhAEXk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co090e/btsDxLiTveu/PBdL1Cl67qUQAxczhAEXk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco090e%2FbtsDxLiTveu%2FPBdL1Cl67qUQAxczhAEXk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;743&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;743&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The GPT will assist users in generating commit messages and detailed descriptions based on the development content they provide. It will analyze the input to understand the nature of the changes, improvements, or fixes made in the code and then create a concise, clear, and relevant commit message. Additionally, it will provide a more detailed description that encapsulates the essence of the development work, highlighting key changes and their implications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역: GPT는 사용자가 제공한 개발 콘텐츠를 기반으로 커밋 메시지와 자세한 설명을 생성하는 데 도움을 줍니다. 입력을 분석하여 코드의 변경, 개선 또는 수정 사항의 성격을 파악한 다음 간결하고 명확하며 관련성 있는 커밋 메시지를 생성합니다. 또한 개발 작업의 본질을 요약하고 주요 변경 사항과 그 의미를 강조하는 보다 자세한 설명을 제공합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 제공하지 않았던 정보까지 창작하여 상세 정보를 생성해주었는데 이것은 원하지 않은 동작이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 구체적인 작업 내용을 말해주지 않아도 커밋 메세지를 작성 해줬으면 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 세부적으로 요청하여 GPT 를 보완할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;You:&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;저는 안드로이드(Android) 개발을 하고 있는 개발자 입니다.
개발 언어는 주로 Kotlin 을 사용하고 있으며, 단순한 내용이든
구체적인 내용이든 작업한 내용을 입력하면 적절한 Commit 메세지와
상세 설명을 생성해주는 전문가가 필요합니다.
Commit 메세지 형식은 &quot;Feat: &quot;, &quot;Fix: &quot;, &quot;Refactor: &quot;, &quot;BugFix: &quot;,
&quot;Update: &quot;, &quot;Chore: &quot; 등 처럼 시작했으면 좋겠습니다.
답변 내용은 MarkDown 형식으로 아래 형태로 생성 해주세요.
감사합니다. 잘 부탁드립니다.

## Commit Message
[실제 커밋 메세지]
## Detailed Description
[상세 설명]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 검증 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dp2mEb/btsDqju3k6H/kldn7vYC1GPDZ4gZ4M4wNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dp2mEb/btsDqju3k6H/kldn7vYC1GPDZ4gZ4M4wNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dp2mEb/btsDqju3k6H/kldn7vYC1GPDZ4gZ4M4wNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdp2mEb%2FbtsDqju3k6H%2Fkldn7vYC1GPDZ4gZ4M4wNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;834&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청한 대로 MarkDown 형식으로 출력해 주었으며, 좀 더(너무나..) 구체적인 Detailed Description 을 작성해주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Prompt:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;You are a GPT specialized in assisting Android developers, particularly those working with Kotlin. Your primary function is to generate meaningful commit messages and detailed descriptions based on the work inputted by the developer. Your responses should start with a commit message format like &quot;Feat:&quot;, &quot;Fix:&quot;, &quot;Refactor:&quot;, &quot;BugFix:&quot;, &quot;Update:&quot;, &quot;Chore:&quot;, and so on. Additionally, your answers should be formatted in Markdown, following a specific structure. 

**Commit Message:** The message should succinctly summarize the changes made in the format requested by the user. 

**Detailed Description:** This section provides an in-depth explanation of the changes, why they were made, and any other relevant details.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 Promt 또한 좀 더 구체적으로 작성 되어 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 시 마음에 들면 Save 하여 사용할 수 있다. (직접 만든 GPT 는 나만 사용하거나 공유하여 사용할 수 있는 옵션도 제공한다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oUgLH/btsDxTOFFNH/oEFVLOwfYV2bj6ToN9KGLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oUgLH/btsDxTOFFNH/oEFVLOwfYV2bj6ToN9KGLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oUgLH/btsDxTOFFNH/oEFVLOwfYV2bj6ToN9KGLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoUgLH%2FbtsDxTOFFNH%2FoEFVLOwfYV2bj6ToN9KGLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;258&quot; height=&quot;353&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0HtOx/btsDs2lAFZp/0wbuWUi6dz04FMioEOu5E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0HtOx/btsDs2lAFZp/0wbuWUi6dz04FMioEOu5E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0HtOx/btsDs2lAFZp/0wbuWUi6dz04FMioEOu5E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0HtOx%2FbtsDs2lAFZp%2F0wbuWUi6dz04FMioEOu5E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;883&quot; height=&quot;639&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;639&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 매번 input 으로 직접 프롬프트를 전달하지 않고도 세련된 Commit Message 를 생성할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(또 다른 GPT 도 생성 해보고 싶지 않은가? 가령 Super Senior Android Development Master&amp;hellip;.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 시 유의&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 통신 보안..&lt;/li&gt;
&lt;li&gt;생성된 메세지를 모두 믿지 말자. 직접 검수 한 뒤 실제 commit 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘 설계된 커밋 메세지 작성은 매우중요하다.&lt;/li&gt;
&lt;li&gt;Few-Shot 기법을 통해 프롬트트를 작성하고 ChatGPT 를 이용하여 Commit Message 를 생성 했다.&lt;/li&gt;
&lt;li&gt;GPT Builder 를 통해 프롬프트 생성을 자동화하고, 맞춤형 GPT 를 생성하여 매번 프롬프트를 직접 작성하지 않고 편하게 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://chat.openai.com/&quot;&gt;https://chat.openai.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;프롬프트 엔지니어링 교과서, 서승완&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성자 : 상추님 &lt;a href=&quot;mailto:vviprogrammer@gmail.com&quot;&gt;vviprogrammer@gmail.com&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/655</guid>
      <comments>https://whyprogrammer.tistory.com/655#entry655comment</comments>
      <pubDate>Mon, 15 Jan 2024 16:28:19 +0900</pubDate>
    </item>
    <item>
      <title>키워드 분포(Keyphrase distribution) 분석 구현 (Yoast SEO)</title>
      <link>https://whyprogrammer.tistory.com/654</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;키워드 분포란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;키워드 분포&quot;란 특정 키워드가 웹페이지의 내용 전반에 골고루 퍼져 있는지를 나타내는 용어입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 워드프레스 플러그인 인 Yoast SEO Premium은 키워드 분포를 최적화하고, 사용자에게 텍스트의 키워드 사용이 균형잡혀 있는지 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 검색 엔진이 웹페이지의 주제와 관련성을 더 정확하게 인식하게 도와주며, 결국 웹사이트의 검색 순위를 높이는데 도움을 줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파이썬 스크립트로 구현해보기&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬을 사용하여 텍스트 파일에서 키워드 분포를 측정하는 코드를 작성해 드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 텍스트 파일을 읽어 특정 키워드의 분포를 계산하고 결과를 출력해 줍니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import re

def keyword_distribution(filename, keyword):
    with open(filename, 'r', encoding='utf-8') as file:
        text = file.read()
        words = re.findall(r'\w+', text)
        total_words = len(words)
        keyword_count = text.lower().count(keyword.lower())
        distribution = (keyword_count / total_words) * 100

    return distribution

filename = &quot;blog_post.txt&quot;  # 블로그 글 파일명을 입력하세요.
keyword = &quot;키워드&quot;  # 분석하려는 키워드를 입력하세요.

distribution = keyword_distribution(filename, keyword)
print(f&quot;'{keyword}'의 분포: {distribution:.2f}%&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 사용하려면, 먼저 txt 파일로 저장된 블로그 글의 파일명을 filename 변수에 입력해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 분석하려는 키워드를 keyword 변수에 입력하세요. 코드를 실행하면 키워드의 분포가 백분율로 출력됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 제공한 코드를 사용하여 텍스트 파일에서 키워드 분포를 측정한 예시를 보여드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 블로그 글(txt 파일)의 내용이 다음과 같다고 가정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;블로그 글에 오신 것을 환영합니다. 이 글에서는 데이터 분석에 대해 이야기하려 합니다.
데이터 분석은 매우 중요한 주제입니다. 데이터를 통해 인사이트를 얻고 의사결정을 내릴 수 있습니다.
데이터 분석의 기초부터 고급 기법까지 다양한 주제를 다룰 예정이니, 많은 관심 부탁드립니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, filename 변수에 &quot;blog_post.txt&quot; 라고 설정하고, 분석하려는 키워드를 &quot;데이터&quot;로 설정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;filename = &quot;blog_post.txt&quot;
keyword = &quot;데이터&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행한 후, 결과는 다음과 같이 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;'데이터'의 분포: 6.67%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과는 '데이터'라는 키워드가 전체 텍스트에서 약 6.67%의 분포를 가지고 있다는 것을 의미합니다. 이렇게 코드를 사용하여 블로그 글에서 원하는 키워드의 분포를 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키워드 분포율은 얼마나가 좋은가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드 분포율은 웹페이지 내 특정 키워드가 얼마나 골고루 퍼져 있는지를 나타내는 지표입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지표는 검색 엔진 최적화(SEO)에 중요한 역할을 하는데, 적절한 키워드 분포율을 유지하면 검색 엔진이 웹페이지의 주제와 관련성을 더 정확하게 인식하게 되어 검색 순위를 높이는 데 도움이 됩니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 키워드 분포율은 상황에 따라 다를 수 있지만, 일반적으로 1% ~ 3% 정도로 유지하는 것이 좋다고 여겨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 범위 내에서 키워드 분포율을 유지하면, 내용이 자연스럽게 읽히면서도 검색 엔진에 충분한 정보를 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드 분포율이 너무 낮으면 검색 엔진이 웹페이지의 주제와 관련성을 제대로 파악하지 못할 수 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 너무 높으면 키워드 스태핑(keyword stuffing)으로 간주되어 검색 순위에 오히려 부정적인 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 키워드 분포율을 적절한 범위 내에서 유지하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(해당 글은 자연어 처리 모델의 도움을 받았어요.)&lt;/p&gt;</description>
      <category>Python</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/654</guid>
      <comments>https://whyprogrammer.tistory.com/654#entry654comment</comments>
      <pubDate>Sat, 18 Mar 2023 01:47:23 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 컴포즈에서 테스트 하기 (Testing in jetpack)</title>
      <link>https://whyprogrammer.tistory.com/653</link>
      <description>&lt;h1&gt;Testing in Jetpack Compose&lt;/h1&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;2. What to test?&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 해볼 것들&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Test that the tabs show the intended icon and text&lt;/li&gt;
&lt;li&gt;Test that the animation matches the spec&lt;/li&gt;
&lt;li&gt;Test that the triggered navigation events are correct&lt;/li&gt;
&lt;li&gt;Test the placement and distances of the UI elements in different states&lt;/li&gt;
&lt;li&gt;Take a screenshot of the bar and compare it with a previous screenshot&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;3. Create a simple UI test&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Create the TopAppBarTest file&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 createComposeRule()을 호출하여 얻을 수 있는 ComposeTestRule과 함께 제공됩니다. 이 규칙을 사용하면 테스트 중인 Compose 콘텐츠를 설정하고 상호 작용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Add the ComposeTestRule&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;package com.example.compose.rally

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    // TODO: Add tests
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Testing in isolation&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;createAndroidComposeRule&lt;/code&gt;&lt;/b&gt; 을 통해서 앱의 메인 액티비티를 실행하듯 테스틀 수행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Don't copy this over

@get:Rule
val composeTestRule = createAndroidComposeRule(RallyActivity::class.java)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컴포즈 에서는 더 간단하게 할 수도 있다. setContent 를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Don't copy this over

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun myTest() {
        composeTestRule.setContent { 
            Text(&quot;You can set any Compose content!&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.junit4.createComposeRule
import com.example.compose.rally.ui.components.RallyTopAppBar
import org.junit.Rule
import org.junit.Test

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun rallyTopAppBarTest() {
        composeTestRule.setContent { 
            RallyTopAppBar(
                allScreens = ,
                onTabSelected = { /*TODO*/ },
                currentScreen = 
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;The importance of a testable Composable&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
    fun rallyTopAppBarTest() {
        val allScreens = RallyScreen.values().toList()
        composeTestRule.setContent { 
            RallyTopAppBar(
                allScreens = allScreens,
                onTabSelected = { },
                currentScreen = RallyScreen.Accounts
            )
        }
        Thread.sleep(5000)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 개별로 실행 가능&lt;/li&gt;
&lt;li&gt;sleep 로 동작을 볼 수 있게 대기한다.&lt;/li&gt;
&lt;li&gt;테마 적용도 자유롭게 해도 상관 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Verify that the tab is selected&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;composeTestRule{.finder}{.assertion}{.action}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요런 식으로 테스트를 짜게 되낟.&lt;/li&gt;
&lt;li&gt;치트 시트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/testing-cheatsheet&quot;&gt;https://developer.android.com/jetpack/compose/testing-cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/ui/test/package-summary&quot;&gt;https://developer.android.com/reference/kotlin/androidx/compose/ui/test/package-summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;example: &lt;code&gt;onNodeWithText&lt;/code&gt;&lt;br /&gt;, &lt;code&gt;onNodeWithContentDescription&lt;/code&gt;&lt;br /&gt;, &lt;code&gt;isSelected&lt;/code&gt;&lt;br /&gt;, &lt;code&gt;hasContentDescription&lt;/code&gt;&lt;br /&gt;, &lt;code&gt;assertIsSelected&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
    fun rallyTopAppBarTest_currentTabSelected() {
        val allScreens = RallyScreen.values().toList()
        composeTestRule.setContent {
            RallyTopAppBar(
                allScreens = allScreens,
                onTabSelected = { },
                currentScreen = RallyScreen.Accounts
            )
        }

        composeTestRule
            .onNodeWithContentDescription(RallyScreen.Accounts.name)
            .assertIsSelected()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;4. Debugging tests&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.onNodeWithText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase())
        .assertExists()
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탭이 선택되고, 이 텍스트가 존재하는 지 살피면 될까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 테스트는 실패한다,..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트를 시멘틱 트리를 통해 디버깅하는 방법을 배워보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Semantics tree&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근성에서 사용하는 방법 처럼(톡백) 요소들의 트리를 볼 수 있다.&lt;/li&gt;
&lt;li&gt;경고: Semantics 속성에 대한 Layout Inspector 지원은 아직 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;printToLog&lt;/code&gt;&lt;/b&gt;로 출력할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
...

@Test
    fun rallyTopAppBarTest_currentLabelExists() {
        val allScreens = RallyScreen.values().toList()
        composeTestRule.setContent {
            RallyTopAppBar(
                allScreens = allScreens,
                onTabSelected = { },
                currentScreen = RallyScreen.Accounts
            )
        }

        composeTestRule.onRoot().printToLog(&quot;currentLabelExists&quot;)

        composeTestRule
            .onNodeWithText(RallyScreen.Accounts.name.uppercase(Locale.getDefault()))
            .assertExists() // Still fails
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;...com.example.compose.rally D/currentLabelExists: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주의 : 컴포즈에는 ID가 없으므로 트리에서 매칭되는 아이디는 테스트에 사용할 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대신 hasTestTag, testTag 로 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대문자 Accounts 가 없기 때문에 당연히 테스트는 실패하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private fun RallyTab(text: String...)
...
    Modifier
        .clearAndSetSemantics { contentDescription = text }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탭 컴포저블에서 contentDescription 를 설정하므로, 트리에서 나타나는 것.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 수정자는 하위 항목에서 속성을 지우고 자체 콘텐츠 설명을 설정하므로 &quot;ACCOUNTS&quot;가 아닌 &quot;Accounts&quot;가 표시됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertExists()
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 테스트가 통과합니다. 하지만 별 도움이 안됨. 모든 트리에 name 이 그대로 노출되기에 선택 여부를 테스트하는 것이 아니기 때문임&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;5. Merged and unmerged Semantics trees&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시맨틱 트리는 항상 가능한 한 간결하게 하려고 노력하여 관련 정보만 표시합니다. 예를 들어 TopAppBar에서는 아이콘과 레이블이 서로 다른 노드일 필요가 없습니다. &quot;개요&quot; 노드를 살펴보십시오.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-testing/img/d20c96207c30e44a_1920.png&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-testing/img/d20c96207c30e44a_1920.png&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Text 컴포저블이 포함된 버튼을 나타낼 수 있습니다. MergeDescendants = 'true' 속성은 이 노드에 자손이 있지만 병합되었음을 알려줍니다. 테스트에서 우리는 종종 모든 노드에 액세스해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭 내부의 Text가 표시되는지 여부를 확인하기 위해 onRoot 파인더에 useUnmergedTree = true를 전달하는 병합되지 않은 Semantics 트리를 쿼리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Printing with useUnmergedTree = 'true'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;|-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACCOUNTS 를 갖는 하위 노드가 생겼다!&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNode(
            hasText(RallyScreen.Accounts.name.uppercase()) and
            hasParent(
                hasContentDescription(RallyScreen.Accounts.name)
            ),
            useUnmergedTree = true
        )
        .assertExists()
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대문자 Accounts 를 갖는 노드 이면서 부모가 Accounts 인 노드가 있음을 테스트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;6. Synchronization&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-testing/img/8c467af3570b8de6.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-testing/img/8c467af3570b8de6.gif&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애니메이션이 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;package com.example.compose.rally

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.example.compose.rally.ui.overview.OverviewBody
import org.junit.Rule
import org.junit.Test

class OverviewScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun overviewScreen_alertsDisplayed() {
        composeTestRule.setContent {
            OverviewBody()
        }

        composeTestRule
            .onNodeWithText(&quot;Alerts&quot;)
            .assertIsDisplayed()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 무한 대기하면서 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.android.ComposeIdlingResource@d075f91&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 기본적으로 Compose가 영구적으로 사용 중이므로 앱을 테스트와 동기화할 방법이 없음을 알려줍니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;var currentTargetElevation by remember {  mutableStateOf(1.dp) }
LaunchedEffect(Unit) {
    // Start the animation
    currentTargetElevation = 8.dp
}
val animatedElevation = animateDpAsState(
    targetValue = currentTargetElevation,
    animationSpec = tween(durationMillis = 500),
    finishedListener = {
        currentTargetElevation = if (currentTargetElevation &amp;gt; 4.dp) {
            1.dp
        } else {
            8.dp
        }
    }
)
Card(elevation = animatedElevation.value) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무한 애니메이션은 이렇구 구현되어 있다.&lt;/li&gt;
&lt;li&gt;이 코드는 기본적으로 애니메이션이 완료될 때까지 기다린 다음(finishedListener) 다시 실행합니다.&lt;/li&gt;
&lt;li&gt;이 테스트를 수정하는 한 가지 방법은 개발자 옵션에서 애니메이션을 비활성화하는 것입니다. View 세계에서 이를 처리하는 데 널리 사용되는 방법 중 하나입니다.&lt;/li&gt;
&lt;li&gt;Compose에서 애니메이션 API는 테스트 가능성을 염두에 두고 설계되었으므로 올바른 API를 사용하여 문제를 해결할 수 있습니다. animateDpAsState 애니메이션을 다시 시작하는 대신 무한 애니메이션을 사용할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무한 애니메이션은 Compose 테스트가 이해하는 특별한 경우이므로 테스트를 계속 바쁘게 만들지 않을 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.Dp
...

    val infiniteElevationAnimation = rememberInfiniteTransition()
    val animatedElevation: Dp by infiniteElevationAnimation.animateValue(
        initialValue = 1.dp,
        targetValue = 8.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = tween(500),
            repeatMode = RepeatMode.Reverse
        )
    )
    Card(elevation = animatedElevation) {&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포저블 구현 부에서 infiniteRepeatable 를 사용하도록 수정하면 테스트가 성공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;7. Optional exercise&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 작업(치트 시트 테스트 참조)을 사용하여 RallyTopAppBar의 다른 탭을 클릭하면 선택 항목이 변경되는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;package com.example.compose.rally

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class RallyAppTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun rallyAppTest() {
        composeTestRule.setContent {
            RallyApp()
        }
        Thread.sleep(1000)
        composeTestRule.onRoot().printToLog(&quot;currentLabelExists&quot;)

        composeTestRule
            .onNode(
                hasContentDescription(&quot;Accounts&quot;),
                useUnmergedTree = true
            ).performClick()

        Thread.sleep(1000)
        composeTestRule
            .onNode(
                hasContentDescription(&quot;Accounts&quot;) and isSelected(),
                useUnmergedTree = true
            )
            .assertExists()
        Thread.sleep(1000)

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;homework 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;figure id=&quot;og_1675781986795&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Testing in Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;In this codelab you&amp;rsquo;ll learn about testing UIs created with Jetpack Compose. You will write your first tests while learning about testing in isolation, debugging tests, semantics trees and synchronization.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-testing?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-4%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-testing#2&quot; data-og-url=&quot;https://developer.android.com/codelabs/jetpack-compose-testing&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/A5RtC/hyRwAvXFJY/DkzW6oI917DouHaBfsRCk0/img.png?width=900&amp;amp;height=1600&amp;amp;face=0_0_900_1600,https://scrap.kakaocdn.net/dn/bD4qE3/hyRwH9IvNr/lKckqIS7rUbwbAPAVBRyh0/img.png?width=900&amp;amp;height=1600&amp;amp;face=0_0_900_1600&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-testing?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-4%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-testing#2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-testing?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-4%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-testing#2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/A5RtC/hyRwAvXFJY/DkzW6oI917DouHaBfsRCk0/img.png?width=900&amp;amp;height=1600&amp;amp;face=0_0_900_1600,https://scrap.kakaocdn.net/dn/bD4qE3/hyRwH9IvNr/lKckqIS7rUbwbAPAVBRyh0/img.png?width=900&amp;amp;height=1600&amp;amp;face=0_0_900_1600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Testing in Jetpack Compose &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In this codelab you&amp;rsquo;ll learn about testing UIs created with Jetpack Compose. You will write your first tests while learning about testing in isolation, debugging tests, semantics trees and synchronization.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/652&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.19 - [Android/Jetpack Compose] - [Jetpack Compose] 컴포즈에서 Navigation 사용하는 방법 정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/651&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.18 - [Android/Jetpack Compose] - [Jetpack Compose] 상태 및 사이드이팩트 고급 내용 정리 (State, SideEffects)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/650&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.17 - [Android/Jetpack Compose] - [Jetpack Compose] 애니메이션 활용 및 기본 내용 정리&lt;/a&gt;&lt;/p&gt;</description>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/653</guid>
      <comments>https://whyprogrammer.tistory.com/653#entry653comment</comments>
      <pubDate>Wed, 8 Feb 2023 00:01:28 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 컴포즈에서 Navigation 사용하는 방법 정리</title>
      <link>https://whyprogrammer.tistory.com/652</link>
      <description>&lt;h1&gt;RallyDestinations&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 코드랩에서 배울 내용&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Basics of using Jetpack Navigation with Jetpack Compose&lt;/li&gt;
&lt;li&gt;Navigating between composables&lt;/li&gt;
&lt;li&gt;Integrating a custom tab bar composable into your navigation hierarchy&lt;/li&gt;
&lt;li&gt;Navigating with arguments&lt;/li&gt;
&lt;li&gt;Navigating using deep links&lt;/li&gt;
&lt;li&gt;Testing navigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 프로젝트는 네비게이션이 되고 있지만, 사실 컴포즈 네비게이션을 사용한 구현이 아니다. 이를 컴포즈 네비게이션으로 구현해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Migrating to Compose Navigation&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점진적으로 마이그레이션을 해보자!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 추가 해주기 &lt;b&gt;&lt;b&gt;Add the Navigation dependency&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 수준 gradle&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;buildscript {
    ext {
        // ...
        composeNavigationVersion = '2.5.0-rc01' // 현재는 2.5.3 이 최신 인듯.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 수준 gradle&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;dependencies {
  implementation &quot;androidx.navigation:navigation-compose:$rootProject.composeNavigationVersion&quot;
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Set up the NavController&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NavController&lt;/code&gt; 가 네비게이션의 핵심 컴포넌트이다. 백스택을 기억하고, 이동가능하게 해준다. &lt;code&gt;rememberNavController()&lt;/code&gt; 를 통해 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;navController 는 항상 최상위 컴포저블에 선언해야한다. (즉 주로, App 컴포저블에) 이는 다음 원칙을 만족한다. (아래)&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 호이스팅&lt;/li&gt;
&lt;li&gt;main source of truth&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Routes in Compose Navigation&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 컴포저블 네비게이션 도착지는 route 와 관련있다. route 는 String 으로 정의하는데, 컴포저블의 경로를 설정하고 &lt;code&gt;navController&lt;/code&gt; 에게 알맞은 도착지를 알려주게된다. 절대경로를 갖는 딥링크랑 비슷하다고 생각할 수도 있다. 각 도착지는 유니크한 네임을 가져야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Calling the NavHost composable with the navigation graph&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네비게이션의 3가지 주요 키워드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;NavController&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NavGraph&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NavHost&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NavController&lt;/code&gt; 는 항상 하나의 &lt;code&gt;NavHost&lt;/code&gt; 와 연관되어 진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;NavHost&lt;/code&gt; 는 컨테이너 처럼 작동하며, 현재 목적지 화면을 보여주는 책임이 있다. 여러 컴포저블을 네비게이트 하면서 &lt;code&gt;NavHost&lt;/code&gt; 는 자동으로 리컴포즈된다. 또한 &lt;code&gt;NavController&lt;/code&gt; 는 네비게이션 그래프인 &lt;code&gt;NavGraph&lt;/code&gt; 와 연결된다. &lt;code&gt;NavGraph&lt;/code&gt; 는 네비게이션 간에 컴포저블 도착지를 매핑합니다. (본질적으로 가져올 수 있는 대상의 컬렉션.)&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding -&amp;gt;
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = Modifier.padding(innerPadding)
    ) { 
       // builder parameter will be defined here as the graph
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 선언헤주었던 navController 를 NavHost 와 연결해준다. (또한 Scaffold 의 innerPadding 도 받아들여준다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Adding destinations to the NavGraph&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;import androidx.navigation.compose.composable
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) { 
    composable(route = Overview.route) { 
        Overview.screen()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌더인 후행 람다에서 &lt;code&gt;composable&lt;/code&gt; 확장 메서드를 사용하여 적절한 화면을 보여준다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) { 
    composable(route = Overview.route) {
        Overview.screen()
    }
    composable(route = Accounts.route) {
        Accounts.screen()
    }
    composable(route = Bills.route) {
        Bills.screen()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도착지가 3개 이므로 더 추가해준다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;5. Integrate RallyTabRow with navigation&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 테스트 하기 쉽고 재사용하기 좋게 만들려면 &lt;code&gt;navController&lt;/code&gt; 를 직접 전달하는 것은 삼가야한다. 그 대신 콜백을 전달하자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    // Pass the callback like this,
                    // defining the navigation action when a tab is selected:
                    onTabSelected = { newScreen -&amp;gt;
                        navController.navigate(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;onTabSelected&lt;/code&gt; 콜백으로 &lt;code&gt;navController&lt;/code&gt; 를 사용하여 다른 화면으로 전환될 수 있게 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 탭 시 네비게이션이 잘 동작은 하지만, 몇가지 문제점이 있따. (아래 목록)&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 탭을 한번 더 탭하면 또 열린다. (백스택에 계속 쌓임)&lt;/li&gt;
&lt;li&gt;아이콘 펼쳐짐 동작이 되지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Launching a single copy of a destination&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 이슈를 해결해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.navigation.NavHostController
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;singleTop 으로 실행할 수 있게 헬퍼 확장 함수를 하나 정의하고&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { newScreen -&amp;gt;
                        navController
                            .navigateSingleTopTo(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 사용하면 간단하게 해결할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Controlling the navigation options and back stack state&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 launchSingleTop 옵션 말고도 옵션 빌더에서 사용할 수 있는 다양한 옵션들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;launchSingleTop = true&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;popUpTo(startDestination) { saveState = true }&lt;/code&gt;&lt;/b&gt; 탭을 선택할 때 백 스택에 많은 대상 스택이 쌓이지 않도록 그래프의 시작 대상으로 팝업
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rally에서 이것은 어떤 대상에서든 뒤로 화살표를 누르면 전체 백 스택이 개요로 팝업됨을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;restoreState = true&lt;/code&gt; 이 탐색 작업이 이전에 &lt;code&gt;PopUpToBuilder.saveState&lt;/code&gt; 또는 &lt;code&gt;popUpToSaveState&lt;/code&gt; 특성에 의해 저장된 상태를 복원해야 하는지 여부를 결정합니다. 이전에 탐색 중인 대상 ID와 함께 상태가 저장되지 않은 경우 아무런 효과가 없습니다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rally에서 이는 동일한 탭을 다시 탭하면 다시 로드하지 않고 이전 데이터와 사용자 상태를 화면에 유지함을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 실행 해보자!&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.navigation.NavHostController
import androidx.navigation.NavGraph.Companion.findStartDestination
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { 
        popUpTo(
            this@navigateSingleTopTo.graph.findStartDestination().id
        ) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;popUpTo 부분은 닫힐 때 어디로 이동될지 정해주는데, 여기서는 그래프의 가장 시작점으로 이동시켜준다. 예를들면,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A &amp;rarr; B &amp;rarr; C &amp;rarr; Back key 시 A 로 이동된다. 여기서는 항상 overView(A) 페이지로 이동된다.&lt;/li&gt;
&lt;li&gt;back key 를 두번 누르면 항상 앱이 내려간다. (A 로 이동되고, 한 번 더 back key 시 백스택이 없기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;saveState, restoreState 의 경우에는 스크롤을 내린 상태 등, 상태를 기억하고 탭 이동 후에 다시 복귀할 때 상태(스크롤 등) 를 유지해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Note:&lt;/b&gt; If you need more guidance on managing multiple back stacks, take a look at the &lt;a href=&quot;https://developer.android.com/guide/navigation/multi-back-stacks&quot;&gt;documentation on supporting multiple backstacks&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Fixing the tab UI&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        // Fetch your currentDestination: 
        val currentDestination = currentBackStack?.destination
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;currentBackStackEntryAsState&lt;/code&gt; 을 통해서 백스택 을 state 로 가져올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        val currentDestination = currentBackStack?.destination

        // Change the variable to this and use Overview as a backup screen if this returns null
        val currentScreen = rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Overview
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니크한 route 를 이용해서 현재 화면을 찾는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: 이 시점에서 탐색 구성 요소를 통해 뒤로 동작 탐색도 무료로 지원됩니다. 추가 설정을 수행할 필요가 없습니다. 목적지 사이를 전환한 다음 뒤로 버튼을 누르면 백 스택이 올바르게 팝업되어 이전 목적지로 이동합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;&lt;b&gt;6. Extracting screen composables from RallyDestinations&lt;/b&gt;&lt;/h1&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RallyDestination 과 커플링을 제거하여, 클릭 이벤트 콜백 등을 추가 시 용이하게 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import com.example.compose.rally.ui.accounts.AccountsScreen
import com.example.compose.rally.ui.bills.BillsScreen
import com.example.compose.rally.ui.overview.OverviewScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        OverviewScreen()
    }
    composable(route = Accounts.route) {
        AccountsScreen()
    }
    composable(route = Bills.route) {
        BillsScreen()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Enable clicks on OverviewScreen&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;NavHost(
                navController = navController,
                startDestination = Overview.route,
                modifier = Modifier.padding(innerPadding)
            ) {
                composable(route = Overview.route) {
                    OverviewScreen(
                        onClickSeeAllAccounts = {
                            navController.navigateSingleTopTo(Accounts.route)
                        },
                        onClickSeeAllBills = {
                            navController.navigateSingleTopTo(Bills.route)
                        }
                    )
                }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onClick 콜백을 연결 시켜 화면 이동을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;code&gt;navController&lt;/code&gt; 가 상위에 (app 에) 호이스팅 되어있으므로, 재사용이 용이하다. mock 인스턴스나 직접 생성해줄 필요 없이 콜백 만으로 이벤트 변경을 쉽게 할 수 있다!)&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;7. Navigating to SingleAccountScreen with arguments : 인자를 추가하여 네비게이션 하기&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;arguments 를 통해 특정 아이템을 클릭 했는지 정보를 담을 수 있다. 한개 또는 그 이상의 argument 를 통해서 네비게이션의 라우팅을 다이나믹하게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;{argument}&lt;/code&gt;&lt;/b&gt; 형식으로 지정하면 된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import com.example.compose.rally.ui.accounts.SingleAccountScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    ...
    composable(route = SingleAccount.route) {
        SingleAccountScreen()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;NavHost&lt;/code&gt; 에 새로운 화면의 route 를 추가해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Set up the SingleAccountScreen landing destination&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;route/{argument}&quot;&lt;/code&gt; 같은 패턴으로 인자를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        &quot;${SingleAccount.route}/{${SingleAccount.accountTypeArg}}&quot;
) { 
    SingleAccountScreen()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;arguments&lt;/code&gt; 를 지정함으로써&lt;/b&gt; 컴포저블이 이를 알게 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        &quot;${SingleAccount.route}/{${SingleAccount.accountTypeArg}}&quot;,
    arguments = listOf(
        navArgument(SingleAccount.accountTypeArg) { type = NavType.StringType }
    )
) { 
    SingleAccountScreen()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;****&lt;/b&gt;타입을 지정하지 않으면 알아서 추론된다.&lt;b&gt;****&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;object SingleAccount : RallyDestination {
    // ...
    override val route = &quot;single_account&quot;
    const val accountTypeArg = &quot;account_type&quot;
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 샘플 프로젝트 구조상 RallyDestination 에 모든 라우트 정보가 들어가므로, 여기에 인자 리스트도 추가한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;composable(
    route = &quot;${SingleAccount.route}/{${SingleAccount.accountTypeArg}}&quot;,
    arguments =  SingleAccount.arguments
) {
    SingleAccountScreen()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 다시 navHost 안의 arguments 가 참조하도록 해주면 코드가 깔끔해진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;NavHost(...) {
    // ...
    composable(
        route =
          &quot;${SingleAccount.route}/{${SingleAccount.accountTypeArg}}&quot;,
        arguments = SingleAccount.arguments
    ) { navBackStackEntry -&amp;gt;
        // Retrieve the passed argument
        val accountType =
            navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)

        // Pass accountType to SingleAccountScreen
        SingleAccountScreen(accountType)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;navBackStackEntry 에서 인자를 찾아서 가져온다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object SingleAccount : RallyDestination {
    // Added for simplicity, this icon will not in fact be used, as SingleAccount isn't
    // part of the RallyTabRow selection
    override val icon = Icons.Filled.Money
    override val route = &quot;single_account&quot;
    const val accountTypeArg = &quot;account_type&quot;
    val routeWithArgs = &quot;${route}/{${accountTypeArg}}&quot;
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;routeWithArgs&lt;/code&gt; 를 가지도록 리팩토링 하자.&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments
) {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SingleAccount.routeWithArgs 참조로 리팩토링.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인자 활용한 네비게이션 컨트롤 적용 &lt;b&gt;&lt;b&gt;Setup the Accounts and Overview starting destinations&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;OverviewScreen(
    // ...
    onAccountClick = { accountType -&amp;gt;
        navController
          .navigateSingleTopTo(&quot;${SingleAccount.route}/$accountType&quot;)
    }
)
// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType -&amp;gt;
        navController
          .navigateSingleTopTo(&quot;${SingleAccount.route}/$accountType&quot;)
    }
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;refactoring&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 제거한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.navigation.NavHostController
// ...
OverviewScreen(
    // ...
    onAccountClick = { accountType -&amp;gt;
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType -&amp;gt;
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo(&quot;${SingleAccount.route}/$accountType&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;8. Enable deep link support 딥링크를 지원 하는 방법&lt;/b&gt;&lt;/h1&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 에서는 딥링크로 특정 앱의 특정 화면으로 이동시킬 수 있는데 이 또한 네비게이션으로 적용할 수 있다. (명시적 딥링크를 지원 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥링크는 기본적으로 비활성화 되어 있음. 인텐트 필터를 추가해야 활성화 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메니페스트 안에 액티비티 안에 인텐트 필터를 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt; 태그로 스킴을 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;activity
    android:name=&quot;.RallyActivity&quot;
    android:windowSoftInputMode=&quot;adjustResize&quot;
    android:label=&quot;@string/app_name&quot;
    android:exported=&quot;true&quot;&amp;gt;
    &amp;lt;intent-filter&amp;gt;
        &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
    &amp;lt;intent-filter&amp;gt;
        &amp;lt;action android:name=&quot;android.intent.action.VIEW&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&amp;gt;
        &amp;lt;data android:scheme=&quot;rally&quot; android:host=&quot;single_account&quot; /&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
&amp;lt;/activity&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;Trigger and verify the deep link&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 딥링크에 반응할 수 있게 되었다!&lt;/li&gt;
&lt;li&gt;하나의 컴포저블 목적지에 여러 딥링크를 연관지어줄 수도 있다&lt;/li&gt;
&lt;li&gt;uri 패턴으로 전달해주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import androidx.navigation.navDeepLink
// ...

composable(
    route = SingleAccount.routeWithArgs,
    // ...
    deepLinks = listOf(navDeepLink {
        uriPattern = &quot;rally://${SingleAccount.route}/{${SingleAccount.accountTypeArg}}&quot;
    })
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;refactoring&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 리팩토링도 잊지 말자. ㅋㅋ&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;object SingleAccount : RallyDestination {
    // ...
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
    val deepLinks = listOf(
       navDeepLink { uriPattern = &quot;rally://$route/{$accountTypeArg}&quot;}
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments,
    deepLinks = SingleAccount.deepLinks
) {...}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;adb 로 딥링크 테스트 하기 : &lt;b&gt;&lt;b&gt;Test the deep link using adb&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;adb shell am start -d &quot;rally://single_account/Checking&quot; -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 을 pc에 물리고, 커맨드에 위 커맨드를 입력해주면 딥링크가 잘 실행된다!&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;Last login: Mon Nov 28 10:41:57 on console

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
LM-046905-00:~ sangsulee$ adb
-bash: adb: command not found
LM-046905-00:~ sangsulee$ export PATH=$PATH:/Users/
.localized     Guest/         Shared/        administrator/ sangsulee/
LM-046905-00:~ sangsulee$ export PATH=$PATH:/Users/sangsulee/Library/Android/sdk/platform-tools/
LM-046905-00:~ sangsulee$ adb
Android Debug Bridge version 1.0.41
Version 33.0.3-8952118
Installed as /Users/sangsulee/Library/Android/sdk/platform-tools/adb

global options:
 -a                       listen on all network interfaces, not just localhost
 -d                       use USB device (error if multiple devices connected)
 -e                       use TCP/IP device (error if multiple TCP/IP devices available)
 -s SERIAL                use device with given serial (overrides $ANDROID_SERIAL)
 -t ID                    use device with given transport id
 -H                       name of adb server host [default=localhost]
 -P                       port of adb server [default=5037]
 -L SOCKET                listen on given socket for adb server [default=tcp:localhost:5037]
 --one-device SERIAL|USB  only allowed with 'start-server' or 'server nodaemon', server will only connect to one USB device, specified by a serial number or USB device address.
 --exit-on-write-error    exit if stdout is closed

general commands:
 devices [-l]             list connected devices (-l for long output)
 help                     show this help message
 version                  show version num

networking:
 connect HOST[:PORT]      connect to a device via TCP/IP [default port=5555]
 disconnect [HOST[:PORT]]
     disconnect from given TCP/IP device [default port=5555], or all
 pair HOST[:PORT] [PAIRING CODE]
     pair with a device for secure TCP/IP communication
 forward --list           list all forward socket connections
 forward [--no-rebind] LOCAL REMOTE
     forward socket connection using:
       tcp:&amp;lt;port&amp;gt; (&amp;lt;local&amp;gt; may be &quot;tcp:0&quot; to pick any open port)
       localabstract:&amp;lt;unix domain socket name&amp;gt;
       localreserved:&amp;lt;unix domain socket name&amp;gt;
       localfilesystem:&amp;lt;unix domain socket name&amp;gt;
       dev:&amp;lt;character device name&amp;gt;
       jdwp:&amp;lt;process pid&amp;gt; (remote only)
       vsock:&amp;lt;CID&amp;gt;:&amp;lt;port&amp;gt; (remote only)
       acceptfd:&amp;lt;fd&amp;gt; (listen only)
 forward --remove LOCAL   remove specific forward socket connection
 forward --remove-all     remove all forward socket connections
 reverse --list           list all reverse socket connections from device
 reverse [--no-rebind] REMOTE LOCAL
     reverse socket connection using:
       tcp:&amp;lt;port&amp;gt; (&amp;lt;remote&amp;gt; may be &quot;tcp:0&quot; to pick any open port)
       localabstract:&amp;lt;unix domain socket name&amp;gt;
       localreserved:&amp;lt;unix domain socket name&amp;gt;
       localfilesystem:&amp;lt;unix domain socket name&amp;gt;
 reverse --remove REMOTE  remove specific reverse socket connection
 reverse --remove-all     remove all reverse socket connections from device
 mdns check               check if mdns discovery is available
 mdns services            list all discovered services

file transfer:
 push [--sync] [-z ALGORITHM] [-Z] LOCAL... REMOTE
     copy local files/directories to device
     --sync: only push files that are newer on the host than the device
     -n: dry run: push files to device without storing to the filesystem
     -z: enable compression with a specified algorithm (any/none/brotli/lz4/zstd)
     -Z: disable compression
 pull [-a] [-z ALGORITHM] [-Z] REMOTE... LOCAL
     copy files/dirs from device
     -a: preserve file timestamp and mode
     -z: enable compression with a specified algorithm (any/none/brotli/lz4/zstd)
     -Z: disable compression
 sync [-l] [-z ALGORITHM] [-Z] [all|data|odm|oem|product|system|system_ext|vendor]
     sync a local build from $ANDROID_PRODUCT_OUT to the device (default all)
     -n: dry run: push files to device without storing to the filesystem
     -l: list files that would be copied, but don't copy them
     -z: enable compression with a specified algorithm (any/none/brotli/lz4/zstd)
     -Z: disable compression

shell:
 shell [-e ESCAPE] [-n] [-Tt] [-x] [COMMAND...]
     run remote shell command (interactive shell if no command given)
     -e: choose escape character, or &quot;none&quot;; default '~'
     -n: don't read from stdin
     -T: disable pty allocation
     -t: allocate a pty if on a tty (-tt: force pty allocation)
     -x: disable remote exit codes and stdout/stderr separation
 emu COMMAND              run emulator console command

app installation (see also `adb shell cmd package help`):
 install [-lrtsdg] [--instant] PACKAGE
     push a single package to the device and install it
 install-multiple [-lrtsdpg] [--instant] PACKAGE...
     push multiple APKs to the device for a single package and install them
 install-multi-package [-lrtsdpg] [--instant] PACKAGE...
     push one or more packages to the device and install them atomically
     -r: replace existing application
     -t: allow test packages
     -d: allow version code downgrade (debuggable packages only)
     -p: partial application install (install-multiple only)
     -g: grant all runtime permissions
     --abi ABI: override platform's default ABI
     --instant: cause the app to be installed as an ephemeral install app
     --no-streaming: always push APK to device and invoke Package Manager as separate steps
     --streaming: force streaming APK directly into Package Manager
     --fastdeploy: use fast deploy
     --no-fastdeploy: prevent use of fast deploy
     --force-agent: force update of deployment agent when using fast deploy
     --date-check-agent: update deployment agent when local version is newer and using fast deploy
     --version-check-agent: update deployment agent when local version has different version code and using fast deploy
     --local-agent: locate agent files from local source build (instead of SDK location)
     (See also `adb shell pm help` for more options.)
 uninstall [-k] PACKAGE
     remove this app package from the device
     '-k': keep the data and cache directories

debugging:
 bugreport [PATH]
     write bugreport to given PATH [default=bugreport.zip];
     if PATH is a directory, the bug report is saved in that directory.
     devices that don't support zipped bug reports output to stdout.
 jdwp                     list pids of processes hosting a JDWP transport
 logcat                   show device log (logcat --help for more)

security:
 disable-verity           disable dm-verity checking on userdebug builds
 enable-verity            re-enable dm-verity checking on userdebug builds
 keygen FILE
     generate adb public/private key; private key stored in FILE,

scripting:
 wait-for[-TRANSPORT]-STATE...
     wait for device to be in a given state
     STATE: device, recovery, rescue, sideload, bootloader, or disconnect
     TRANSPORT: usb, local, or any [default=any]
 get-state                print offline | bootloader | device
 get-serialno             print &amp;lt;serial-number&amp;gt;
 get-devpath              print &amp;lt;device-path&amp;gt;
 remount [-R]
      remount partitions read-write. if a reboot is required, -R will
      will automatically reboot the device.
 reboot [bootloader|recovery|sideload|sideload-auto-reboot]
     reboot the device; defaults to booting system image but
     supports bootloader and recovery too. sideload reboots
     into recovery and automatically starts sideload mode,
     sideload-auto-reboot is the same but reboots after sideloading.
 sideload OTAPACKAGE      sideload the given full OTA package
 root                     restart adbd with root permissions
 unroot                   restart adbd without root permissions
 usb                      restart adbd listening on USB
 tcpip PORT               restart adbd listening on TCP on PORT

internal debugging:
 start-server             ensure that there is a server running
 kill-server              kill the server if it is running
 reconnect                kick connection from host side to force reconnect
 reconnect device         kick connection from device side to force reconnect
 reconnect offline        reset offline/unauthorized devices to force reconnect

usb:
 attach                   attach a detached USB device
 detach                   detach from a USB device to allow use by other processes
environment variables:
 $ADB_TRACE
     comma-separated list of debug info to log:
     all,adb,sockets,packets,rwx,usb,sync,sysdeps,transport,jdwp
 $ADB_VENDOR_KEYS         colon-separated list of keys (files or directories)
 $ANDROID_SERIAL          serial number to connect to (see -s)
 $ANDROID_LOG_TAGS        tags to be used by logcat (see logcat --help)
 $ADB_LOCAL_TRANSPORT_MAX_PORT max emulator scan port (default 5585, 16 emus)
 $ADB_MDNS_AUTO_CONNECT   comma-separated list of mdns services to allow auto-connect (default adb-tls-connect)
LM-046905-00:~ sangsulee$ -bash: adb: command not found
-bash: -bash:: command not found
LM-046905-00:~ sangsulee$ export PATH=$PATH:/Users/sangsulee/Library/Android/sdk/platform-tools/
LM-046905-00:~ sangsulee$ adb shell am start -d &quot;rally://single_account/Checking&quot; -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking }
Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking flg=0x10000000 }
LM-046905-00:~ sangsulee$ 
LM-046905-00:~ sangsulee$ adb shell am start -d &quot;rally://single_account/Checking&quot; -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking }
Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking flg=0x10000000 }
LM-046905-00:~ sangsulee$ adb shell am start -d &quot;rally://single_account/Checking&quot; -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking }
Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking flg=0x10000000 }
LM-046905-00:~ sangsulee$ adb shell am start -d &quot;rally://single_account/Checking&quot; -a android.intent.action.VIEW
Starting: Intent { act=android.intent.action.VIEW dat=rally://single_account/Checking }
LM-046905-00:~ sangsulee$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;띄어 쓰기 포함된 String 은 불가능 한듯,,,?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; %20 으로 가능&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;9. Extract the NavHost into RallyNavHost 코드를 좀 더 깔끔하게 만들어 보자!&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        composable(route = Overview.route) {
            OverviewScreen(
                onClickSeeAllAccounts = {
                    navController.navigateSingleTopTo(Accounts.route)
                },
                onClickSeeAllBills = {
                    navController.navigateSingleTopTo(Bills.route)
                },
                onAccountClick = { accountType -&amp;gt;
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Accounts.route) {
            AccountsScreen(
                onAccountClick = { accountType -&amp;gt;
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Bills.route) {
            BillsScreen()
        }
        composable(
            route = SingleAccount.routeWithArgs,
            arguments = SingleAccount.arguments,
            deepLinks = SingleAccount.deepLinks
        ) { navBackStackEntry -&amp;gt;
            val accountType =
              navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
            SingleAccountScreen(accountType)
        }
    }
}

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo(&quot;${SingleAccount.route}/$accountType&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RallyNavHost 를 Rally app 에서 분리&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding -&amp;gt;
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )
        }
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;10. Testing Compose Navigation&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해서 의존성 추가&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;dependencies {
// ...
  androidTestImplementation &quot;androidx.navigation:navigation-testing:$rootProject.composeNavigationVersion&quot;
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Prepare the NavigationTest class&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈를 테스트 하기위해 컴포즈 rule 를 적용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Write your first test&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import org.junit.Assert.fail
import org.junit.Test
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            // Creates a TestNavHostController
            navController = 
                TestNavHostController(LocalContext.current)
            // Sets a ComposeNavigator to the navController so it can navigate through composables
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }
        fail()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;composeTestRule 의 setContent 를 사용하면 실제 환경 처럼 테스트 환경에서도 컴포즈 코드를 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;TestNavHostController 로 테스트 가능한 NavController 를 생성해줄 수 있다.&lt;/li&gt;
&lt;li&gt;fail() 은 테스트를 실패시키는데, 구현 전에 실패되어야 하기 때문에 임시적으로 추가해둔 것.&lt;/li&gt;
&lt;li&gt;검증 하는 테스트 코드는 setContent 밖에서 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule.setContent {
            navController = 
                TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }

        composeTestRule
            .onNodeWithContentDescription(&quot;Overview Screen&quot;)
            .assertIsDisplayed()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 화면이 Overview Screen 인 것을 테스트 하는 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import org.junit.Before
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupRallyNavHost() {
        composeTestRule.setContent {
            navController = 
                TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }
    }

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule
            .onNodeWithContentDescription(&quot;Overview Screen&quot;)
            .assertIsDisplayed()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Before&lt;/code&gt; 를 통해서 사전 준비 코드(공통된 부분) 을 작성하여 보일러 플레이트를 제거할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Navigating in tests&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클릭 하는 테스트도 작성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Testing via UI clicks and screen contentDescription&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.performClick
// ...

@Test
fun rallyNavHost_clickAllAccount_navigatesToAccounts() {
    composeTestRule
        .onNodeWithContentDescription(&quot;All Accounts&quot;)
        .performClick()

    composeTestRule
        .onNodeWithContentDescription(&quot;Accounts Screen&quot;)
        .assertIsDisplayed()
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 버튼을 클릭하고 나서&lt;/li&gt;
&lt;li&gt;다음 화면으로 이동되는 것을 검증하는 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Testing via UI clicks and routes comparison&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
// ...

@Test
fun rallyNavHost_clickAllBills_navigateToBills() {
    composeTestRule.onNodeWithContentDescription(&quot;All Bills&quot;)
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, &quot;bills&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음은 조금 다른 방법으로 확인하는 테스트 코드인데&lt;/li&gt;
&lt;li&gt;Bills 의 SEE ALL 을 누르기 위해서는 스크롤을 해주어야 하는데 이때는 performScrollTo 를 사용할 수 있고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;navController.currentBackStackEntry?.destination?.route&lt;/code&gt; 를 통해 bills 로 이동하였는지를 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;figure id=&quot;og_1671026736435&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Jetpack Compose 탐색 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 Codelab에서는 Compose의 탐색 기본사항을 알아봅니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-navigation&quot; data-og-url=&quot;https://developer.android.com/codelabs/jetpack-compose-navigation?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/goWwB/hyQTyLsTLz/RfNNdJLqSOd1YQqQEtkPh1/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600,https://scrap.kakaocdn.net/dn/bBrU0n/hyQTE5Uq95/8ER5hl62vaO878PoAg64Vk/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600,https://scrap.kakaocdn.net/dn/LUy3L/hyQTK576DX/NwugArklkWcXDdkajX4wr1/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-navigation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-navigation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/goWwB/hyQTyLsTLz/RfNNdJLqSOd1YQqQEtkPh1/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600,https://scrap.kakaocdn.net/dn/bBrU0n/hyQTE5Uq95/8ER5hl62vaO878PoAg64Vk/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600,https://scrap.kakaocdn.net/dn/LUy3L/hyQTK576DX/NwugArklkWcXDdkajX4wr1/img.png?width=758&amp;amp;height=1600&amp;amp;face=0_0_758_1600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose 탐색 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 Codelab에서는 Compose의 탐색 기본사항을 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/651&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.18 - [Android/Jetpack Compose] - [Jetpack Compose] 상태 및 사이드이팩트 고급 내용 정리 (State, SideEffects)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/650&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.17 - [Android/Jetpack Compose] - [Jetpack Compose] 애니메이션 활용 및 기본 내용 정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/649&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.16 - [Android/Jetpack Compose] - [Jetpack Compose] view + xml 기반에서 컴포즈로 마이그레이션&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>android</category>
      <category>Compose</category>
      <category>Jetpack</category>
      <category>Navigation</category>
      <category>네비게이션</category>
      <category>안드로이드</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/652</guid>
      <comments>https://whyprogrammer.tistory.com/652#entry652comment</comments>
      <pubDate>Mon, 19 Dec 2022 19:19:11 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 상태 및 사이드이팩트 고급 내용 정리 (State, SideEffects)</title>
      <link>https://whyprogrammer.tistory.com/651</link>
      <description>&lt;h1&gt;AdvancedStateAndSideEffectsCodelab&lt;/h1&gt;
&lt;h1&gt;1. 어떤 내용을 배울까? Introduction&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젯팩 컴포즈의 State 와 Side Effects 에 대한 고급 개념을 실습해봤다. 로직이 사소하지 않은 stateful 한 컴포저블의 state holder 를 만드는 방법을 다룬다. 컴포즈 코드에서 코루틴과 suspend 함수를 사용하는 방법과 어떻게 다양한 유즈케이스를 위해 side effects 를 트리거하는지 알아 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미완성된 프로젝트 하나를 통해 점차적으로 개선하면서 완성하여 아래와 같은 앱을 완성하려고한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-advanced-state-side-effects/img/b2c6b8989f4332bb.gif?authuser=2&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-advanced-state-side-effects/img/b2c6b8989f4332bb.gif?authuser=2&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 코드랩에 기본적인 테스트 코드가 작성되어 있기 때문에, 코드랩 중간 중간 테스트를 돌려보면서 잘 돌아가나 보도록 하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(시간 나면 해보기) &lt;b&gt;&lt;b&gt;Displaying the map on the details screen&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디테일 스크린에 맵을 표시하려면 API key 를 받고 설정해야하는데.. 이건 시간나면 해보자.&lt;/p&gt;
&lt;h1&gt;3. 뷰 모델에 있는 Flow 를 소비해보자! : Consuming a Flow from the ViewModel&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StateFlow.collectAsState()&lt;/code&gt; 를 사용하면 된다. 컴포저블에서 &lt;code&gt;collectAsState()&lt;/code&gt; 는 &lt;code&gt;StateFlow&lt;/code&gt; 로부터 값을 수집하고 최신 값을 Compose&amp;rsquo;s State API 를 통해 가져옵니다. 그러면 새로 방출되는 값에 의해 리컴포저블이 가능합니다&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 컴포즈 에서는 많이 사용되는 스트림 데이터형식인 LiveData 와 rxjava 를 지원한다 (아래 참고)&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;[LiveData.observeAsState()](https://developer.android.com/reference/kotlin/androidx/compose/runtime/livedata/package-summary?authuser=2#observeAsState(androidx.lifecycle.LiveData))&lt;/code&gt; included in the &lt;code&gt;androidx.compose.runtime:runtime-livedata:$composeVersion&lt;/code&gt; artifact.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[Observable.subscribeAsState()](https://developer.android.com/reference/kotlin/androidx/compose/runtime/rxjava2/package-summary?authuser=2#subscribeAsState(io.reactivex.Observable,kotlin.Any))&lt;/code&gt; included in the &lt;code&gt;androidx.compose.runtime:runtime-rxjava2:$composeVersion&lt;/code&gt; or &lt;code&gt;androidx.compose.runtime:runtime-rxjava3:$composeVersion&lt;/code&gt; artifact.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;4. LaunchedEffect 와 rememberUpdatedState&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드랩에서 말하고 있는 랜딩 스크린은 우리가 익히 알고있는 스플래시 화면과 동일한듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;onTimeout&lt;/code&gt; 콜백 으로 랜딩 스크린을 적절할 때 사라지게 해줄 예정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈 에서는 백그라운드에서 안전하게 코루틴을 실행할 수 있게 해주는 API 를 제공한다. (백그라운드 작업으로 보통 코루틴 코틀린을 사용하도록 권장된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 앱 에서는 백앤드와 직접 통신은 하지 않고 &lt;code&gt;delay&lt;/code&gt; 중단 함수를 사용해서 이를 시뮬레이션 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈에서 사이드 이팩트는 컴포저블 함수 스코브 밖에서 발생하는 앱의 상태변화를 말한다. (예를 들어 사용자가 버튼을 탭할 때 새 화면을 열거나 앱이 인터넷에 연결되어 있지 않을 때 메시지를 표시하는 것)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 이팩트 == 컴포저블 함수 스코프 밖에서 일어나는 상태변화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랜딩 화면을 표시하거나 숨기는 일은 &lt;code&gt;onTimeout&lt;/code&gt; 콜백에서 일어나는데, 코루틴 안에서 이를 로드 해야하기 때문에, 상태 변경은 컴포저블 함수 내부가 아닌 코루틴 내부에서 발생하게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 컴포저블 내부에서 코루틴을 안전하게 수행할 수 있는 &lt;code&gt;LaunchedEffect&lt;/code&gt; 를 이용할 수 있다! (&lt;code&gt;LaunchedEffect&lt;/code&gt; 는 컴포즈 스코프 안에서 사이드 이팩트 코루틴을 실행 시킬 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LaunchedEffect&lt;/code&gt; 가 컴포지션 된다면 코루틴이 전달 블록 기반으로 실행되고, &lt;code&gt;LaunchedEffect&lt;/code&gt; 가 컴포지션을 벗어나면 코루틴은 자동으로 캔슬된다. (컴포저블 생명주기에 따라 자연스럽게 캔슬 해준다는듯하다.)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -&amp;gt; Unit) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        // Start a side effect to load things in the background
        // and call onTimeout() when finished.
        // Passing onTimeout as a parameter to LaunchedEffect
        // is wrong! Don't do this. We'll improve this code in a sec.
        LaunchedEffect(onTimeout) {
            delay(SplashWaitTime) // Simulates loading things
            onTimeout()
        }
        Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LaunchedEffect&lt;/code&gt; 같은 몇몇 사이드 이팩트 API 는 가변 개수 키를 파라미터로 받습니다. 1개 이상의 키가 변할 때 이팩트를 재실행 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포저블 라이프사이클 동안 단 한번만 실행 하려면 키로 상수 값을 넘기면 됩니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 &lt;code&gt;onTimeout&lt;/code&gt; 의 변화에 대한 방지책이 없다. 만약에 side-effect 도중에 &lt;code&gt;onTimeout&lt;/code&gt; 이 변경된다면 마지막에 호풀된 &lt;code&gt;onTimeout&lt;/code&gt;() 이 무사히 마쳐진다는 보장이 없다. 이를 보장하기 위해서는 &lt;code&gt;rememberUpdatedState&lt;/code&gt; API 를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -&amp;gt; Unit) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        // This will always refer to the latest onTimeout function that
        // LandingScreen was recomposed with
        val currentOnTimeout by rememberUpdatedState(onTimeout)

        // Create an effect that matches the lifecycle of LandingScreen.
        // If LandingScreen recomposes or onTimeout changes, 
        // the delay shouldn't start again.
        LaunchedEffect(true) {
            delay(SplashWaitTime)
            currentOnTimeout()
        }

        Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;rememberUpdatedState&lt;/code&gt; 를 사용하여 항상 최신의 &lt;code&gt;onTimeout&lt;/code&gt; 함수를 참조할 수 있다. (만약에 컴포저블이 리컴포지션 된다고 하더라도!)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LaunchedEffect(true)&lt;/code&gt; 를 통해서 랜딩 스크린 컴포저블의 라이프사이클과 연동하여 사이드이팩트를 줄 수 있으며, 리컴포지션 또는 &lt;code&gt;onTimeout&lt;/code&gt; 이 변경된다고 하더라도 내부에 있는 (여기서는 &lt;code&gt;delay&lt;/code&gt;) 작업이 또 다시 실행되지 않도록 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 랜딩 스크린을 보여주도록 만들자! : Showing the landing screen&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import androidx.compose.runtime.getValue // 요거 빼먹지 말자
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue // 요거 빼먹지 말자 (오토 임포트가 잘 안됨;)

@Composable
private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
    Surface(color = MaterialTheme.colors.primary) {
        var showLandingScreen by remember { mutableStateOf(true) }
        if (showLandingScreen) {
            LandingScreen(onTimeout = { showLandingScreen = false })
        } else {
            CraneHome(onExploreItemClicked = onExploreItemClicked)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5. rememberCoroutineScope 로 컴포저블 밖에서 코루틴 안전하게 실행하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;scaffoldState&lt;/code&gt; 는 &lt;code&gt;DrawerState&lt;/code&gt; 를 갖고 있으며, &lt;code&gt;DrawerState&lt;/code&gt; 는 네비게이션 드로워를 코드수준에서 닫고 열 수 있게 해줌. 하지만 코드랩의 openDrawer 콜백으로 &lt;code&gt;scaffoldState.drawerState.open()&lt;/code&gt; 를 시도하면 에러가 발생한다! 왜냐면 &lt;code&gt;open()&lt;/code&gt; 이 suspend 이기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세이프한 코루틴 실행 외에도 몇몇 API는 suspend 인데, 그 예중 하나로 햄버거 메뉴 열고 닫기는 애니메이션 등으로 인해 suspend 되기 적합하기 때문&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 어떻게 호출해야할까? 컴포저블 내부가 아니라서 &lt;b&gt;&lt;code&gt;LaunchedEffect&lt;/code&gt; 사용도 불가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈의 라이프 사이클을 따르는 코루틴 스코프에서 suspend 함수를 실행하고 싶은데, 이 경우라면 &lt;code&gt;rememberCoroutineScope&lt;/code&gt; 를 사용하면된다. (해당 스코프는 컴포지션 범위에서 벗어나면 자동으로 캔슬된다.) (이로써 컴포즈 스코프가 아닌 곳 에서도 코루틴을 안전하게 실행할 수 있게된다.)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// home/CraneHome.kt file

import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@Composable
fun CraneHome(
    onExploreItemClicked: OnExploreItemClicked,
    modifier: Modifier = Modifier,
) {
    val scaffoldState = rememberScaffoldState()
    Scaffold(
        scaffoldState = scaffoldState,
        modifier = Modifier.statusBarsPadding(),
        drawerContent = {
            CraneDrawer()
        }
    ) {
        val scope = rememberCoroutineScope()
        CraneHomeContent(
            modifier = modifier,
            onExploreItemClicked = onExploreItemClicked,
            openDrawer = {
                scope.launch {
                    scaffoldState.drawerState.open()
                }
            }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;LaunchedEffect vs rememberCoroutineScope&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 LaunchedEffect 를 rememberCoroutineScope 로 대체해서 스플래시 뷰를 띄우도록 해도 될까? 사실 되는 것 처럼 보인다. 하지만 공식 문서에서 설명하기를 컴포저블은 언제든지 컴포지션이 일어날 수 있는데, LaunchedEffect 는 컴포지션이 일어날 때 호출되는 것을 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 rememberCoroutineScope 로 코루틴을 호출하게 되면, 컴포지션 여부와 상관 없이 컴포즈에 의해 실행이되게 된다. 그렇기 때문에 리소스가 낭비되고 통제된 환경에서 실행할 수 없게된다.&lt;/p&gt;
&lt;h1&gt;6. Creating a state holder&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드랩 샘플 앱에서 목적지 검색으로 리스트를 필터링할 수 있고, 텍스트 스타일도 바뀌는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(검색 시에 텍스트 앞에 To 가 붙고, 비행기 아이콘이 활성화 되고 있다)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-advanced-state-side-effects/img/dde9ef06ca4e5191.gif?authuser=2&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-advanced-state-side-effects/img/dde9ef06ca4e5191.gif?authuser=2&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 state holder 를 사용하여 상태를 호이스팅 해야하는가 &lt;b&gt;&lt;b&gt;Why?&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직이 내부에 다들어있기 때문에, 상태를 호이스팅하지 않음, 그렇기에 테스트가 어려워진다. 더 복잡해 질 수 있고 내부 상태는 싱크하기가 더 어려워 질 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 상태를 책임지는 state holder 를 하나 만들어서 한 곳에서 상태를 관리할 수 있게 하자. 그렇게 함으로써 상태를 동기화 하기 쉽게 할 수 있고 한클래스 안에서 관련 로직을 그룹화할 수 있다. 이런 상태는 쉽게 호이스팅 될 수 있고 (위로) 컴포저블에서 소비될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 앱 등에서 재사용되어야 하는 로우한 UI 이기 때문에 상태를 호이스팅하는 것은 좋은 아이디어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 유연하고 더 컨트롤하기 좋게 할 수 있다는 말씀&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상태 홀더 정의 &lt;b&gt;&lt;b&gt;Creating the state holder&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// base/EditableUserInput.kt file

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

class EditableUserInputState(private val hint: String, initialText: String) {

    var text by mutableStateOf(initialText)

    val isHint: Boolean
        get() = text == hint
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상태 홀더 클래스를 하나 만들자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mutableStateOf 로 만듦으로써 텍스트가 변하면 리컴포지션이 일어날 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;var 로 지정하여 외부에서 수정할 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 텍스트 값을 위한 initialText 를 생성시 주입받아야 한다.&lt;/li&gt;
&lt;li&gt;isHint 로 힌트인지 판단하는 프로퍼티가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직이 나중에 더 복잡해 진다고 쳤을 때, EditableUserInputState 만 수정하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Remembering the state holder&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// base/EditableUserInput.kt file

@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
    remember(hint) {
        EditableUserInputState(hint, hint)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 홀더를 컴포저블 에서 기억되게 하여, 쓸데없이 계속해서 새로 초기화 되는 것을 피해야 하고, 생성하는 부분을 컴포저블로 지정하여 보일러플레이트를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;remember&lt;/code&gt; 를 사용하면 액티비티 재생성 시에는 기억할 수 없다. 이 이슈를 해결하기 위해선 &lt;code&gt;rememberSaveable&lt;/code&gt; 을 사용해야 한다. 그러나 액티비티나 프로세스 생생성 시도 내부적으로 유지된다. 직접 번들로 저장할 필요 없이 상태를 유지해준다는 건데, &lt;code&gt;EditableUserInputState&lt;/code&gt; 클래스의 경우에는 이를 저장하는 방법을 &lt;code&gt;Saver&lt;/code&gt; 를 통해 알줘야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Creating a custom saver&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Saver&lt;/code&gt; 는 어떤 객체가 어떻게 &lt;code&gt;Saveable&lt;/code&gt; 로 변환되는지를 기술한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;save&lt;/code&gt; 는 원본 값을 saveable 로 변환한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restore&lt;/code&gt; 는 saveable 을 원본 인스턴스로 변환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 경우 EditableUserInputState 클래스에 대해 Saver의 사용자 지정 구현을 만드는 대신 listSaver 또는 mapSaver(목록 또는 맵에 저장할 값을 저장함)와 같은 기존 Compose API 중 일부를 사용하여 코드 양을 줄일 수 있습니다. 우리가 써야 한다는 것.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// base/EditableUserInput.kt file

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver

class EditableUserInputState(private val hint: String, initialText: String) {
    var text by mutableStateOf(initialText)

    val isHint: Boolean
        get() = text == hint

    companion object {
        val Saver: Saver&amp;lt;EditableUserInputState, *&amp;gt; = listSaver(
            save = { listOf(it.hint, it.text) },
            restore = {
                EditableUserInputState(
                    hint = it[0],
                    initialText = it[1],
                )
            }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EditableUserInputState 와 밀접하게 사용되는 static 함수 이므로 companion object 로 정의해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;listSaver&lt;/code&gt; 로 &lt;code&gt;EditableUserInputState&lt;/code&gt; 를 저장하고 복구하는 세이버를 구현한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState =
    rememberSaveable(hint, saver = EditableUserInputState.Saver) {
        EditableUserInputState(hint, hint)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 세이버 사용.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;Using the state holder&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 정의한 state holder 를 실제 사용해보자.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;@Composable
fun CraneEditableUserInput(
    state: EditableUserInputState = rememberEditableUserInputState(&quot;&quot;),
    caption: String? = null,
    @DrawableRes vectorImageId: Int? = null
) {
    CraneBaseUserInput(
        caption = caption,
        tintIcon = { !state.isHint },
        showCaption = { !state.isHint },
        vectorImageId = vectorImageId
    ) {
        BasicTextField(
            value = state.text,
            onValueChange = { state.text = it },
            textStyle = if (state.isHint) {
                captionTextStyle.copy(color = LocalContentColor.current)
            } else {
                MaterialTheme.typography.body1.copy(color = LocalContentColor.current)
            },
            cursorBrush = SolidColor(LocalContentColor.current)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;State holder callers&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사용처도 수정 해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// home/SearchUserInput.kt file

import androidx.compose.samples.crane.base.rememberEditableUserInputState

@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -&amp;gt; Unit) {
    val editableUserInputState = rememberEditableUserInputState(hint = &quot;Choose Destination&quot;)
    CraneEditableUserInput(
        state = editableUserInputState,
        caption = &quot;To&quot;,
        vectorImageId = R.drawable.ic_plane
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;snapshotFlow&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력이 변경될 때마다 LaunchedEffect를 사용하여 부작용을 트리거하고 onToDestinationChanged 람다를 호출&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// home/SearchUserInput.kt file

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter

@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -&amp;gt; Unit) {
    val editableUserInputState = rememberEditableUserInputState(hint = &quot;Choose Destination&quot;)
    CraneEditableUserInput(
        state = editableUserInputState,
        caption = &quot;To&quot;,
        vectorImageId = R.drawable.ic_plane
    )

// 이 부분 추가됨 
    val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
    LaunchedEffect(editableUserInputState) {
        snapshotFlow { editableUserInputState.text }
            .filter { !editableUserInputState.isHint } // 텍스트가 hint 가 아닐 떄만
            .collect {
                currentOnDestinationChanged(editableUserInputState.text)
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;snapshotFlow&lt;/code&gt; 를 사용하여 &lt;code&gt;State&amp;lt;T&amp;gt;&lt;/code&gt; 객체를 Flow 로 변환 하는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;snapshotFlow&lt;/code&gt; 내부에서 editableUserInputState 상태가 변경 되면, Flow 는 새 값을 쏜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rememberUpdatedState&lt;/code&gt; 를 통해서 항상 최신의 &lt;code&gt;onToDestinationChanged&lt;/code&gt; 함수를 참조 가능하고 여기에 collect 한 값을 쏴주는 것이다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects?authuser=2&amp;amp;continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-3%3Fauthuser%3D2%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-advanced-state-side-effects#6&quot;&gt;7. DisposableEffect&lt;/a&gt;&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// details/MapViewUtils.kt file - code in the main branch

@Composable
fun rememberMapViewWithLifecycle(): MapView {
    val context = LocalContext.current
    // TODO Codelab: DisposableEffect step. Make MapView follow the lifecycle
    return remember {
        MapView(context).apply {
            id = R.id.map
            onCreate(Bundle())
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵뷰의 기본 구현은, 라이프 사이클과 연동이 되어 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 구동은 잘 되는 것 처럼 보이지만, 라이프 사이클을 따르지 않는 다는 것이 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(pause, stop 등의 시점을 알 수 없으니, mapView 를 효율적으로 돌리기 어렵다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;MapView&lt;/code&gt; 는 컴포저블이 아니라 View 다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;// details/MapViewUtils.kt file

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
    LifecycleEventObserver { _, event -&amp;gt;
        when (event) {
            Lifecycle.Event.ON_CREATE -&amp;gt; mapView.onCreate(Bundle())
            Lifecycle.Event.ON_START -&amp;gt; mapView.onStart()
            Lifecycle.Event.ON_RESUME -&amp;gt; mapView.onResume()
            Lifecycle.Event.ON_PAUSE -&amp;gt; mapView.onPause()
            Lifecycle.Event.ON_STOP -&amp;gt; mapView.onStop()
            Lifecycle.Event.ON_DESTROY -&amp;gt; mapView.onDestroy()
            else -&amp;gt; throw IllegalStateException()
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵View 와 라이프사이클을 연동하는 옵저버를 하나 정의할건데, 이걸 &lt;code&gt;LocalLifecycleOwner&lt;/code&gt; 와 연결하면 되는데 연결만 하는게 아니라 제거 해주는 작업도 필요하다. 이때 &lt;code&gt;DisposableEffect&lt;/code&gt; 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DisposableEffect&lt;/code&gt; 는 사이드 이팩트인데, 키 값이 변동되거나 클린업이 필요 한 경우 즉 컴포저블이 컴포지션을 벗어나는 경우에 발생된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// details/MapViewUtils.kt file

import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner

@Composable
fun rememberMapViewWithLifecycle(): MapView {
    val context = LocalContext.current
    val mapView = remember {
        MapView(context).apply {
            id = R.id.map
        }
    }

    val lifecycle = LocalLifecycleOwner.current.lifecycle // 현재 생명주기 가져오기
    DisposableEffect(key1 = lifecycle, key2 = mapView) {
        // Make MapView follow the current lifecycle
        val lifecycleObserver = getMapLifecycleObserver(mapView)
        lifecycle.addObserver(lifecycleObserver)
        onDispose { // 라이프사이클이 변경되거나, 컴포저블이 컴포지션을 떠날 때 제거된다. // 수명주기 or mapView 가 변경될 때마다 옵저버가 제거되고 올바른 수명주기에 추가된다.
            lifecycle.removeObserver(lifecycleObserver)
        }
    }

    return mapView
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(위 코드의 주석 설명 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 동작에 변경이 생기지는 않으며, 생명주기를 옳바르게 연동했다는 점이 포인트다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;8. produceState&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun DetailsScreen(
    onErrorLoading: () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {
    // TODO Codelab: produceState step - Show loading screen while fetching city details
    val cityDetails = remember(viewModel) { viewModel.cityDetails }
    if (cityDetails is Result.Success&amp;lt;ExploreModel&amp;gt;) {
        DetailsContent(cityDetails.data, modifier.fillMaxSize())
    } else {
        onErrorLoading()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DetailsScreen&lt;/code&gt; 의 경우 현재 뷰 모델의 &lt;code&gt;cityDetails&lt;/code&gt; 를 로드하여 사용하는데, 이는 UI 스레드를 저하시킬 수 있다. 그러므로 이를 코루틴 내부로 이동하고 차라리 로딩을 보여주도록 개선해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// details/DetailsActivity.kt file

data class DetailsUiState(
    val cityDetails: ExploreModel? = null,
    val isLoading: Boolean = false,
    val throwError: Boolean = false
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상태로 스크린의 상태를 모델링 할 수 있다. 즉, 데이터가 표시되는 상태, 로딩 상태 그리고 에러상태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 상태를 매핑하기 위해서 컴포즈에서는 &lt;code&gt;produceState&lt;/code&gt; 를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;produceState&lt;/code&gt; 는 컴포즈 상태가 아닌 상태를 컴포즈 상태로 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// details/DetailsActivity.kt file

import androidx.compose.runtime.produceState

@Composable
fun DetailsScreen(
    onErrorLoading: () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {

    val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
        // In a coroutine, this can call suspend functions or move
        // the computation to different Dispatchers
        val cityDetailsResult = viewModel.cityDetails
        value = if (cityDetailsResult is Result.Success&amp;lt;ExploreModel&amp;gt;) {
            DetailsUiState(cityDetailsResult.data)
        } else {
            DetailsUiState(throwError = true)
        }
    }

    // TODO: ... 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;produceState&lt;/code&gt; 로 상태를 생성하고, viewModel 내부 데이터 로드에 따라 변경한다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// details/DetailsActivity.kt file

import androidx.compose.foundation.layout.Box
import androidx.compose.material.CircularProgressIndicator

@Composable
fun DetailsScreen(
    onErrorLoading: () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {
    val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
        val cityDetailsResult = viewModel.cityDetails
        value = if (cityDetailsResult is Result.Success&amp;lt;ExploreModel&amp;gt;) {
            DetailsUiState(cityDetailsResult.data)
        } else {
            DetailsUiState(throwError = true)
        }
    }

    when {
        uiState.cityDetails != null -&amp;gt; {
            DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
        }
        uiState.isLoading -&amp;gt; {
            Box(modifier.fillMaxSize()) {
                CircularProgressIndicator(
                    color = MaterialTheme.colors.onSurface,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
        else -&amp;gt; { onErrorLoading() }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태에 따라 화면을 그려준다.&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;9. derivedStateOf&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroll to top 기능을 구현해보자. 구현은 간단할 수 있지만, &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#derivedStateOf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;derivedStateOf(kotlin.Function0)&lt;/code&gt;&lt;/a&gt; 이라는 처음보는 api 를 활용하는 것이 포인트.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 State에서 파생된 Compose State를 원할 때 derivedStateOf가 사용됩니다. 이 함수를 사용하면 연산에 사용된 상태 중 하나가 변경될 때마다 연산이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 스크롤을 해서 아이템 하나가 가려지는 것은 &lt;code&gt;listState&lt;/code&gt; 를 통해서 &lt;code&gt;listState.firstVisibleItemIndex &amp;gt; 0&lt;/code&gt; 으로 알 수 있다. 이때 &lt;code&gt;firstVisibleItemIndex&lt;/code&gt; 는 &lt;code&gt;mutableStateOf&lt;/code&gt; 로 래핑된다. (observable 한 컴포즈 상태로 쓰일 수 있다는 것.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이렇게 구현하지 마세요&lt;/h2&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;// DO NOT DO THIS - It's executed on every recomposition
val showButton = listState.firstVisibleItemIndex &amp;gt; 0&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대신 이렇게 구현하세요&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// Show the button if the first visible item is past
// the first item. We use a remembered derived state to
// minimize unnecessary compositions
val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex &amp;gt; 0
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;derivedStateOf&lt;/code&gt; 를 사용하여 &lt;code&gt;firstVisibleItemIndex &amp;gt; 0&lt;/code&gt; 인 경우에만 버튼을 보여지게.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun ExploreSection(
    modifier: Modifier = Modifier,
    title: String,
    exploreList: List&amp;lt;ExploreModel&amp;gt;,
    onItemClicked: OnExploreItemClicked
) {
    Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
        Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.caption.copy(color = crane_caption)
            )
            Spacer(Modifier.height(8.dp))
            // Box 를 사용하여 floating 버튼이 lazyList 위에 올라와 보이도록 함.
            Box(Modifier.weight(1f)) {
                val listState = rememberLazyListState()
                ExploreList(exploreList, onItemClicked, listState = listState)

                // Show the button if the first visible item is past
                // the first item. We use a remembered derived state to
                // minimize unnecessary compositions
                val showButton by remember {
                    derivedStateOf {
                        listState.firstVisibleItemIndex &amp;gt; 0
                    }
                }
                if (showButton) {
                    val coroutineScope = rememberCoroutineScope()
                    FloatingActionButton(
                        backgroundColor = MaterialTheme.colors.primary,
                        modifier = Modifier
                            .align(Alignment.BottomEnd)
                            .navigationBarsPadding()
                            .padding(bottom = 8.dp),
                        onClick = {
                            // rememberCoroutineScope 를 사용하여 button 의 onclick 에서 suspend fun 호출
                            coroutineScope.launch {
                                listState.scrollToItem(0)
                            }
                        }
                    ) {
                        Text(&quot;Up!&quot;)
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rememberCoroutineScope 를 사용하여 button 의 onclick 에서 suspend fun 호출&lt;/li&gt;
&lt;li&gt;Box 를 사용하여 floating 버튼이 lazyList 위에 올라와 보이도록 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;figure id=&quot;og_1671026444574&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Jetpack Compose의 고급 상태 및 부작용 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 Codelab에서는 Jetpack Compose의 상태 및 부수 효과에 관한 고급 개념을 알아봅니다. 복잡한 스테이트풀(Stateful) 컴포저블의 상태 홀더를 만드는 방법, Compose 코드에서 코루틴을 만들고 정지 함수를&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects&quot; data-og-url=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iB6Ht/hyQTNWctB1/ikjsfALgNlaz1FRJbPigR0/img.png?width=605&amp;amp;height=1010&amp;amp;face=0_0_605_1010,https://scrap.kakaocdn.net/dn/lqYlD/hyQTy5JF7V/rZTq2Xi1etvmGIP43hrTKk/img.png?width=369&amp;amp;height=504&amp;amp;face=0_0_369_504&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iB6Ht/hyQTNWctB1/ikjsfALgNlaz1FRJbPigR0/img.png?width=605&amp;amp;height=1010&amp;amp;face=0_0_605_1010,https://scrap.kakaocdn.net/dn/lqYlD/hyQTy5JF7V/rZTq2Xi1etvmGIP43hrTKk/img.png?width=369&amp;amp;height=504&amp;amp;face=0_0_369_504');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose의 고급 상태 및 부작용 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 Codelab에서는 Jetpack Compose의 상태 및 부수 효과에 관한 고급 개념을 알아봅니다. 복잡한 스테이트풀(Stateful) 컴포저블의 상태 홀더를 만드는 방법, Compose 코드에서 코루틴을 만들고 정지 함수를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/650&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.17 - [Android/Jetpack Compose] - [Jetpack Compose] 애니메이션 활용 및 기본 내용 정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/649&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.16 - [Android/Jetpack Compose] - [Jetpack Compose] view + xml 기반에서 컴포즈로 마이그레이션&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/648&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.15 - [Android/Jetpack Compose] - [Jetpack Compose] 테마 Theming 기본 정리&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>android</category>
      <category>Compose</category>
      <category>Jetpack</category>
      <category>사이드이팩트</category>
      <category>상태</category>
      <category>안드로이드</category>
      <category>컴포즈</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/651</guid>
      <comments>https://whyprogrammer.tistory.com/651#entry651comment</comments>
      <pubDate>Sun, 18 Dec 2022 19:14:12 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 애니메이션 활용 및 기본 내용 정리</title>
      <link>https://whyprogrammer.tistory.com/650</link>
      <description>&lt;h1&gt;1. &lt;b&gt;What you'll learn&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;How to use several basic Animation APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. Getting set up&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle 버전 업 경고가 뜰 시, 업그레이드 해주어도 무방하다. (시간이 부족하면 skip)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;3. Animating a simple value change&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;animate*AsState&lt;/code&gt; 를 통해 간단한 값 변화가 일어나는 에니메이션을 줄 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;코드랩의 경우 색상 변경 이므로 &lt;b&gt;&lt;code&gt;animateColorAsState&lt;/code&gt; 를 사용할 수 있다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;return value 는 &lt;code&gt;State&amp;lt;T&amp;gt;&lt;/code&gt; 이므로 &lt;code&gt;by&lt;/code&gt; 델리 게이터를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;4. Animating visibility&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에서 extended 에 따라 텍스트가 보여지고 가려진다. 이를 애니메이션 처리하는 것은 간단한데, &lt;b&gt;&lt;code&gt;AnimatedVisibility&lt;/code&gt; 를 사용하면 된다. (아래)&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;AnimatedVisibility(extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
private fun EditMessage(shown: Boolean) {
    // TODO 2-2: The message should slide down from the top on appearance and slide up on
    //           disappearance.
    AnimatedVisibility(
        visible = shown
    ) {
        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = MaterialTheme.colors.secondary,
            elevation = 4.dp
        ) {
            Text(
                text = stringResource(R.string.edit_message),
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EditMessage 에서도 AnimatedVisibility 를 통해서 애니메이션을 생해주고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애니메이션 커스터마이징&lt;/h2&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(),
    exit = slideOutVertically()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enter, exit 파라미터에 &lt;b&gt;&lt;code&gt;EnterTransition&lt;/code&gt; 인스턴스를 전달하여 커스텀이 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/bb2fab24ab5f5d7e.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/bb2fab24ab5f5d7e.gif&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아지만 &lt;code&gt;slideInVertically&lt;/code&gt; , &lt;code&gt;slideOutVertically&lt;/code&gt; 이 기본적으로 아이템의 절반 높이를 사용하기 때문에 자연스럽지가 않아보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;&lt;code&gt;initialOffsetY&lt;/code&gt; 를 사용하면 이를 자연스럽게 할 수 있다. (&lt;code&gt;initialOffsetY&lt;/code&gt; 는 람다로 요소의 높이를 받는다. 그리고 시작 지점을 반환한다.)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;AnimatedVisibility(
        visible = shown,
        enter = slideInVertically(
            // Enters by sliding down from offset -fullHeight to 0.
            initialOffsetY = { fullHeight -&amp;gt; -fullHeight }
        ),
        exit = slideOutVertically(
            // Exits by sliding up from offset 0 to -fullHeight.
            targetOffsetY = { fullHeight -&amp;gt; -fullHeight }
        )
    ) {
        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = MaterialTheme.colors.secondary,
            elevation = 4.dp
        ) {
            Text(
                text = stringResource(R.string.edit_message),
                modifier = Modifier.padding(16.dp)
            )
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 전체 높이 만큼 애니메이션 되도록 수정한 코드이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/295147e3f66d6477.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/295147e3f66d6477.gif&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;animationSpec&lt;/code&gt;&lt;/b&gt; 을 사용하면 애니메이션을 추가로 커스텀 가능하다. 어떤 식으로 값이 변경되는지 지정 할 수 있는 등.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -&amp;gt; -fullHeight },
        animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -&amp;gt; -fullHeight },
        animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colors.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/e8148c03e9364121.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/e8148c03e9364121.gif&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;5. Animating content size change&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
        .animateContentSize()
) {
    // ... the title and the body
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;animateContentSize&lt;/code&gt; 를 통해 사이즈 변경 애니메이션을 줄 수 있다. 마찬가지로 &lt;b&gt;&lt;code&gt;animationSpec&lt;/code&gt; 를 통해서 커스터마이징도 가능하다. (&lt;/b&gt;See the &lt;a href=&quot;https://developer.android.com/jetpack/compose/animation#customize-animations&quot;&gt;Customizing Animations documentation&lt;/a&gt; for more information.)&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;6. Animating multiple values&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 애니메이션을 만들기 위해서 &lt;code&gt;Transition&lt;/code&gt; 을 살펴보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Transition&lt;/code&gt; 은 모든 애니메이션의 트랜지션이 끝나는 지점을 트랙할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 &lt;code&gt;animate*AsState&lt;/code&gt; 를 개별로 사용했을 때는 불가능 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Transition&lt;/code&gt; 은 또한 &lt;b&gt;&lt;code&gt;transitionSpec&lt;/code&gt;&lt;/b&gt; 을 다르게 정의할 수 있게 해준다. 어떤 경우에? 각각 다른 상태 간에 트랜지션이 일어나고 있을 때.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/9c340d99866191b_856.png&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/9c340d99866191b_856.png&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 현재는 탭 인디케이터 변경이 일어나면 사격형의 테두리를 보여준다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) Purple700 else Green800&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 구현되어있는 것을 살펴보면, &lt;code&gt;indicatorLeft&lt;/code&gt; 는 탭 라인에 있는 인디케이터의 왼쪽 가장자리의 위치이다. (즉 사각형의 좌변(?) 시작위치 이라는 것) &lt;code&gt;indicatorRight&lt;/code&gt; 는 우변(끝 위치 가 되겠다.) 그래서 left, right 를 가지고 인디케이터 사각형 테두리 (Box) 를 그려주는 것이다. (물론 색상도 변경해주고 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 다수의 값을 변경하여 애니메이션을 주기위해서는 &lt;code&gt;Transition&lt;/code&gt; 을 사용하면 된다. 트랜지션은 &lt;code&gt;updateTransition&lt;/code&gt; 함수를 통해서 생성할 수 있다. 현재 선택된 탭의 인덱스를 &lt;code&gt;targetState&lt;/code&gt; 파라미터로 넘겨준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val transition = updateTransition(tabPage, label = &quot;Tab indicator&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 값의 경우 &lt;code&gt;animate*&lt;/code&gt; 확장 함수를 통해서 정의할 수 있다. 이때 람다로 타겟 값을 특정할 수 있다. (이 경우에는 이미 어떤 값을 타겟으로 해야하는지 알고 있으므로 그대로 래핑만 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;by&lt;/code&gt; 델리게이트를 사용하여 정의하고 있다. (&lt;code&gt;animate*&lt;/code&gt; 도 &lt;code&gt;State&lt;/code&gt; 를 반환하는 함수이기 때문이다.)&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;val indicatorLeft by transition.animateDp(label = &quot;Indicator left&quot;) { page -&amp;gt;
   tabPositions[page.ordinal].left
}
//...
val color by transition.animateColor(label = &quot;Border color&quot;) { page -&amp;gt;
   if (page == TabPage.Home) Purple700 else Green800
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;val transition = updateTransition(tabPage, label = &quot;Tab indicator&quot;)
val indicatorLeft by transition.animateDp(label = &quot;Indicator left&quot;) { page -&amp;gt;
   tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = &quot;Indicator right&quot;) { page -&amp;gt;
   tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = &quot;Border color&quot;) { page -&amp;gt;
   if (page == TabPage.Home) Purple700 else Green800
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/452f7a5a34f2939f.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/452f7a5a34f2939f.gif&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용하고 나면 보다 흥미로운 애니메이션을 감상할 수 있다. 여기서 애니메이션이 발생되게 하는 상태 값은 &lt;code&gt;tabPage&lt;/code&gt; 이며 모든 애니메이션 값이 이 상태를 보고 타겟된 값으로 애니메이션을 show 한다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, &lt;b&gt;&lt;code&gt;transitionSpec&lt;/code&gt;&lt;/b&gt; 를 사용하여 애니메이션의 모습을 커스터마이징 할 수도 있다. (예를 들어 탄성 효과)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 변화의 방향을 결정짓기 위해&lt;code&gt;transitionSpec&lt;/code&gt; 의 람다안의 중위함수인 &lt;code&gt;isTransitioningTo&lt;/code&gt; 를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;val transition = updateTransition(
    tabPage,
    label = &quot;Tab indicator&quot;
)
val indicatorLeft by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right.
            // The left edge moves slower than the right edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        } else {
            // Indicator moves to the left.
            // The left edge moves faster than the right edge.
            spring(stiffness = Spring.StiffnessMedium)
        }
    },
    label = &quot;Indicator left&quot;
) { page -&amp;gt;
    tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right
            // The right edge moves faster than the left edge.
            spring(stiffness = Spring.StiffnessMedium)
        } else {
            // Indicator moves to the left.
            // The right edge moves slower than the left edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        }
    },
    label = &quot;Indicator right&quot;
) { page -&amp;gt;
    tabPositions[page.ordinal].right
}
val color by transition.animateColor(
    label = &quot;Border color&quot;
) { page -&amp;gt;
    if (page == TabPage.Home) Purple700 else Green800
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/79376bb05022de00.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/79376bb05022de00.gif&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 스튜디오에서는 컴포즈 프리뷰 에서 애니메이션 트랜지션 인스팩션을 지원한다. 이를 위해 interactive mode 를 클릭하여 애니메이션 프리뷰를 시작해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이 버튼을 눌러 애니메이션을 재생할 수도 있고, 식바로 움직여서 특정 지점일때 애니메이션을 직접 볼 수 있다. 라벨로 지정한 명칭으로 특정 애니메이션 값을 식별 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/efbd7a1d0780ad43.gif&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/efbd7a1d0780ad43.gif&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;7. Repeating animations&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드랩에서는 날씨 새로고침을 하면 그저 회색의 임시 도형을 보여주는데, 사실 최신 날씨가 업데이트 되며 로딩이 되는 것을 유저에게 보여주어야 자연스럽다. 그럼 구현 해보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/cd37873f5a6102c4_856.png&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/cd37873f5a6102c4_856.png&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;val alpha = 1f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 값을 0 ~ 1f 까지 반복적으로 애니메이션 하려면 &lt;code&gt;InfiniteTransition&lt;/code&gt; 을 사용하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Transition&lt;/code&gt;&lt;/b&gt;. 의 경우에는 state 가 변하는 것에 초점을 두고 애니메이션이 보여지지만, &lt;b&gt;&lt;code&gt;InfiniteTransition&lt;/code&gt;&lt;/b&gt; 의 경우에는 무한으로다가 보여준다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;InfiniteTransition&lt;/code&gt;을 만들기 위해서는 &lt;code&gt;rememberInfiniteTransition&lt;/code&gt;함수를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;InfiniteTransition&lt;/code&gt; 의 확장 함수인 &lt;code&gt;animate*&lt;/code&gt; 꼴의 함수를 사용하여 값 변화를 줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = keyframes {
            durationMillis = 1000
            0.7f at 500
        },
        repeatMode = RepeatMode.Reverse
    )
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 모드를 Reverse 를 주어서 0&lt;/p&gt;
&lt;p&gt;&lt;del&gt;1&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0~1&amp;hellip; 으로 반복되는 값으로 애니메이션 되도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;keyFrames&lt;/code&gt; 는 &lt;code&gt;animationSpec&lt;/code&gt; 의 또다른 타입이다. 특정 밀리초 에서 값을 달리 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;animation = keyframes {
   durationMillis = 1000
   0.7f at 500
   0.9f at 800
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키프레임을 추가할 수도 있다. (구간을 추가해 준다고 생각하면 된다.) (애니메이션이 왜 배터리를 갉아 먹는지 알것 같다..)&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;&lt;b&gt;8. Gesture animation&lt;/b&gt;&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 터치에 기반한 애니메이션은 어떻게 주는지 알아보자. 코드랩 샘플에 있는 커스텀 Modifier 인 &lt;code&gt;swipeToDismiss&lt;/code&gt; 를 사용할거다. 이 Modifier를 적용하면 터치로 스와이프 할 수 있게되는 컴포저블이 되게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Modifier 가 적용된 컴포저블이 스크린 양옆으로 스와이프 되면 파라미터로 받은 onDismissed 콜백을 호출할 것이다. (여기서는 지우는 작업을 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cf. &lt;a href=&quot;https://developer.android.com/jetpack/compose/gestures&quot;&gt;Compose Gesture documentation&lt;/a&gt;&lt;br /&gt;유저 터치 기반의 좌우 스와이프 애니메이션을 줘보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pointerInput&lt;/code&gt; 수정자를 통해 들어오는 포인터 터치 이벤트에 대한 저수준 액세스를 얻을 수 있고 동일한 포인터를 사용하여 사용자가 드래그하는 속도를 추적할 수 있습니다. 항목이 해제 경계를 지나기 전에 손을 놓으면 항목이 제자리로 되돌아옵니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫째, 진행 중인 애니메이션이 터치 이벤트에 의해 차단될 수 있습니다.&lt;/li&gt;
&lt;li&gt;둘째, 애니메이션 값이 유일한 출처가 아닐 수도 있습니다. (애니메이션 값이 터치로 오는 값인지, 애니메이션 으로 인해 변경되고 있는 값인지 알 수 없다는 거인듯)
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 애니메이션 값을 터치 이벤트에서 오는 값과 동기화해야 할 수도 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기능이 이미 있다. 예를 들어 제스처에서 들어오는 새 값에 즉시 스냅하고 새 터치 이벤트가 트리거되면 진행 중인 애니메이션을 중지하는 기능이 있습니다. &lt;code&gt;Animatable&lt;/code&gt; 에 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
    // Used to calculate a settling position of a fling animation.
    val decay = splineBasedDecay&amp;lt;Float&amp;gt;(this)
    // Wrap in a coroutine scope to use suspend functions for touch events and animation.
    coroutineScope {
        while (true) {
            // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션 도중에 &lt;code&gt;stop&lt;/code&gt; 을 통해 애니메이션을 중단할 수 있다. 사용자 터치가 닿는 순간 기존 애니메이션을 중단하면된다. 물론 애니메이션이 구동중이 아닐 때는 &lt;code&gt;stop&lt;/code&gt; 은 그냥 무시된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 터치이벤트를 연속적으로다가 받게 될건데, 이를 애니메이션과 동기화 해줄 필요가 있다. (그래야 유저가 보기에 자연스러우니 당연하겠다.) 이를 위해서 &lt;code&gt;snapTo&lt;/code&gt; 를 사용할 수 있따.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;awaitPointerEventScope 및 horizontalDrag는 제한된 코루틴 범위이므로 다른 &lt;code&gt;launch&lt;/code&gt; 블록 내에서 snapTo를 호출해야 합니다. 즉, awaitPointerEvents에 대해서만 일시 중지할 수 있으며 snapTo는 포인터 이벤트가 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;horizontalDrag(pointerId) { change -&amp;gt;
    // Add these 4 lines
    // Get the drag amount change to offset the item with
    val horizontalDragOffset = offsetX.value + change.positionChange().x
    // Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
    launch {
        // Instantly set the Animable to the dragOffset to ensure its moving
        // as the user's finger moves
        offsetX.snapTo(horizontalDragOffset)
    }
    // Record the velocity of the drag.
    velocityTracker.addPosition(change.uptimeMillis, change.position)

    // Consume the gesture event, not passed to external
    if (change.positionChange() != Offset.Zero) change.consume()

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 스와이프를 했을 때, 요소를 원래 위치로 다시 밀어야 하는지 아니면 멀리 밀어서 콜백을 호출해야 하는지 결정하기 위해 플링이 안정되는 최종 위치를 계산해야 합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 targetOffsetX를 계산하기 위해 이전에 생성된 &lt;code&gt;decay&lt;/code&gt; 개체를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with 
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애니메이션 바운드를 정해줍니다. (삭제되거나 다시 원래대로 돌아갈 바운더리)&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애니메이션 구동하기&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;launch {
    if (targetOffsetX.absoluteValue &amp;lt;= size.width) {
        // 충분히 빠르게 스와이프 하지 않은 경우 다시 0으로 돌려보낸다.
        // Not enough velocity; Slide back.
        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
    } else {
        // 충분히 쓸어 날려준 경우 삭제해버리자
        // Enough velocity to slide away the element to the edge.
        offsetX.animateDecay(velocity, decay)
        // The element was swiped away.
        onDismissed()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offsetX 에 값을 주었으니 이제 적용만 하면 될 터, 모든 애니메이션과 제스처가 설정되었으므로 x 값에 적용하자. 이렇게 하면 화면의 아이템이 제스처 또는 애니메이션에 의해 생성된 값으로 이동한다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/e46132d5b20e0e8a_856.png&quot; alt=&quot;https://developer.android.com/static/codelabs/jetpack-compose-animation/img/e46132d5b20e0e8a_856.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;배운 것들&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;High-level animation APIs:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;animatedContentSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AnimatedVisibility&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lower level animation APIs:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;animate*AsState&lt;/code&gt; for animating a single value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updateTransition&lt;/code&gt; for animating multiple values&lt;/li&gt;
&lt;li&gt;&lt;code&gt;infiniteTransition&lt;/code&gt; for animating values indefinitely&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Animatable&lt;/code&gt; for building custom animations with touch gestures&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #569cd6; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; --darkreader-inline-color: #60a4d8;&quot; data-darkreader-inline-color=&quot;&quot;&gt;Reference&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;figure id=&quot;og_1671026093866&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Jetpack Compose의 요소 애니메이션 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 Codelab에서는 Compose Animation API를 사용하는 방법을 알아봅니다.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-animation&quot; data-og-url=&quot;https://developer.android.com/codelabs/jetpack-compose-animation?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bSNFpD/hyQTBnNmNt/1XEtI1RKo8QoKdk3MaSbNk/img.png?width=904&amp;amp;height=443&amp;amp;face=0_0_904_443,https://scrap.kakaocdn.net/dn/P34zy/hyQTx0dGyn/he1CkXCdBQTzbceeFoS851/img.png?width=1080&amp;amp;height=360&amp;amp;face=0_0_1080_360,https://scrap.kakaocdn.net/dn/bR46tw/hyQTFDIODa/ijXQLrqcdYAOJaA8iFvZu1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-animation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/codelabs/jetpack-compose-animation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bSNFpD/hyQTBnNmNt/1XEtI1RKo8QoKdk3MaSbNk/img.png?width=904&amp;amp;height=443&amp;amp;face=0_0_904_443,https://scrap.kakaocdn.net/dn/P34zy/hyQTx0dGyn/he1CkXCdBQTzbceeFoS851/img.png?width=1080&amp;amp;height=360&amp;amp;face=0_0_1080_360,https://scrap.kakaocdn.net/dn/bR46tw/hyQTFDIODa/ijXQLrqcdYAOJaA8iFvZu1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose의 요소 애니메이션 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 Codelab에서는 Compose Animation API를 사용하는 방법을 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/649&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.16 - [Android/Jetpack Compose] - [Jetpack Compose] view + xml 기반에서 컴포즈로 마이그레이션&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/648&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.15 - [Android/Jetpack Compose] - [Jetpack Compose] 테마 Theming 기본 정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/647&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.14 - [Android/Jetpack Compose] - [Android] compose basic state (상태의 기본 내용 정리)&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Android/Jetpack Compose</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/650</guid>
      <comments>https://whyprogrammer.tistory.com/650#entry650comment</comments>
      <pubDate>Sat, 17 Dec 2022 19:10:32 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] view + xml 기반에서 컴포즈로 마이그레이션</title>
      <link>https://whyprogrammer.tistory.com/649</link>
      <description>&lt;h1&gt;pathway1-2: MigrationCodelab 정리 (스터디)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 뷰 기반 레이아웃을 컴포즈 레이아웃으로 마이그레이션 하는 방법을 알아보자.&lt;/p&gt;
&lt;h1&gt;view-based &amp;rarr; compose&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈로 대체할 뷰 기반 레이아웃을 모두 주석 처리해주자. (xml 기준이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석으로 대체한 부분에는 아래와 같이 컴포즈 뷰를 삽입해주자. (xml 에 마이그레이션 하는 것이기 때문에 어쩔 수 없이 얘도 뷰가 되는 듯하다.)&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;&amp;lt;androidx.compose.ui.platform.ComposeView
  android:id=&quot;@+id/compose_view&quot;
  android:layout_width=&quot;match_parent&quot;
  android:layout_height=&quot;match_parent&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 뷰 에서 컴포즈 사용하기&lt;/h2&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;composeView.setContent {
    // You're in Compose world!
    MaterialTheme {
        PlantDetailDescription()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식의 안드로이드 뷰(액티비티 등) 에서 컴포즈 레이아웃을 사용하는 방법이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본격적인 migration&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 주석처리한 텍스트뷰를 대체할 컴포저블을 정의하자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName(&quot;Apple&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ui 마이그레이션이라 기존 뷰 속성과 상응하는 컴포저블을 작성해 주기만 하면된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 뷰모델을 사용한 데이터바인딩을 해주고 있었으므로, 동일하게 컴포저블에서도 뷰모델을 받아준다. (이전 코드랩에서는 뷰모델을 직접 받지 말라고 권고하였는데, 요 코드랩은 마이그레이션 가이드이므로 일단 넘어가주자)&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription(plantDetailViewModel)
                }
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일하게 setContent 에서도 전달해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;observe livedata&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData&amp;lt;Plant&amp;gt; field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰모델에 있는 라이브데이터를 &lt;code&gt;observeAsState&lt;/code&gt; 로 관찰하여 컴포저블을 방출한다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlantWatering 컴포저블의 정의. 마찬가지로 기존 뷰와 동일하게 보이도록 해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlantDetailContent 에 기본 패딩을 추가하고, PlantWatering 를 포함시킨다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Lifecycle of ComposeView&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 ComposeView가 창에서 분리될 때마다 컴포지션을 삭제한다. 이것은 여러 가지 이유로 ComposeView가 프래그먼트에서 사용될 때 바람직하지 않다. 화면 전환이 일어나는 경우에는 유지하고, 실제 라이프사이클이 제거 이벤트인 경우에만 제거되도록 수정해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/codelabs/jetpack-compose-migration?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fjetpack-compose-for-android-developers-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-migration#9&quot;&gt;Migrating to Jetpack Compose | Android Developers&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;composeView.apply {
      // Dispose the Composition when the view's LifecycleOwner
      // is destroyed
      setViewCompositionStrategy(
          ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
      )
      setContent {
          // You're in Compose world!
          MaterialTheme {
              PlantDetailDescription(plantDetailViewModel)
          }
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;setViewCompositionStrategy&lt;/code&gt; 으로 컴포즈뷰의 라이프사이클을 뷰 라이프사이클과 맞춰준다.&lt;/p&gt;
&lt;h1&gt;MdcTheme&lt;/h1&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;setContent {
    // You're in Compose world!
    MdcTheme {
        PlantDetailDescription(plantDetailViewModel)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MdcTheme 함수는 호스트 컨텍스트의 MDC 테마를 자동으로 읽고 사용자를 대신하여 밝은 테마와 어두운 테마 모두를 위해 MaterialTheme로 전달한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;dark theme preview&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant(&quot;id&quot;, &quot;Apple&quot;, &quot;HTML&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;description&quot;, 3, 30, &quot;&quot;)
    MdcTheme {
        PlantDetailContent(plant)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlantDescription 컴포저블 추가, html 을 보여주기 위해서는 html 을 string 으로 받아 이를 변환하여 보여주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context -&amp;gt;
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription(&quot;HTML&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;description&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;테스트 코드 작성&lt;/h1&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 작성되어있던 테스트 코드를 수정해보자.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Rule
    @JvmField
    val activityTestRule = ActivityScenarioRule(GardenActivity::class.java)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule&amp;lt;GardenActivity&amp;gt;()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;activityTestRule &amp;rarr; composeTestRule&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;activityTestRule.scenario.onActivity { gardenActivity -&amp;gt;
            activity = gardenActivity

            val bundle = Bundle().apply { putString(&quot;plantId&quot;, &quot;malus-pumila&quot;) }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;composeTestRule.activityRule.scenario.onActivity { gardenActivity -&amp;gt;
            activity = gardenActivity

            val bundle = Bundle().apply { putString(&quot;plantId&quot;, &quot;malus-pumila&quot;) }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;activityTestRule.scenario.onActivity &amp;rarr; composeTestRule.activityRule.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
    fun testPlantName() {
        onView(ViewMatchers.withText(&quot;Apple&quot;))
            .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
    fun testPlantName() { // 화면에 표시되는 식물의 이름을 확인합니다.
        composeTestRule.onNodeWithText(&quot;Apple&quot;).assertIsDisplayed()

    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보너스 : 공유 버튼 클릭 동작을 테스트하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 버튼을 탭한 후 알맞은 인텐트가 트리거되는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Test
    fun testShareTextIntent() { // 
        val shareText = activity.getString(R.string.share_text_plant, testPlant.name)

        Intents.init()
        onView(withId(R.id.action_share)).perform(click())
        intended(
            chooser(
                allOf(
                    hasAction(Intent.ACTION_SEND),
                    hasType(&quot;text/plain&quot;),
                    hasExtra(Intent.EXTRA_TEXT, shareText)
                )
            )
        )
        Intents.release()

        // dismiss the Share Dialog
        InstrumentationRegistry.getInstrumentation()
            .uiAutomation
            .performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
    }

    // TODO: This workaround is needed due to the real database being used in tests.
    //  A fake database created with a Room.inMemoryDatabaseBuilder should be used instead.
    //  That's difficult to do in the current state of the project since there are no
    //  dependency injection best practices in place.
    private fun populateDatabase() {
        val request = TestListenableWorkerBuilder&amp;lt;SeedDatabaseWorker&amp;gt;(
            InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
        ).build()
        runBlocking {
            request.doWork()
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;figure id=&quot;og_1671025871885&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&quot; data-og-description=&quot;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; data-og-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/648&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.15 - [Android/Jetpack Compose] - [Jetpack Compose] 테마 Theming 기본 정리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/647&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.14 - [Android/Jetpack Compose] - [Android] compose basic state (상태의 기본 내용 정리)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/646&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.15 - [Android/Google Play] - 구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/649</guid>
      <comments>https://whyprogrammer.tistory.com/649#entry649comment</comments>
      <pubDate>Fri, 16 Dec 2022 19:03:17 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] 테마 Theming 기본 정리</title>
      <link>https://whyprogrammer.tistory.com/648</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 테마를 사용하는 방법을 알아보자.&lt;/p&gt;
&lt;h1&gt;커스텀 테마 사용하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 JetnewsTheme 라는 이름으로 테마를 사용할 것이므로 테마를 아래와 같이 정의 해준다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Composable
fun JetnewsTheme(content: @Composable () -&amp;gt; Unit) {
    MaterialTheme(content = content)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MaterialTheme&lt;/code&gt; 를 단순 감싸고 있으며, 이후에 &lt;code&gt;MaterialTheme&lt;/code&gt; 의 생성자 옵셔널한 파라미터를 전달하면서 테마를 커스텀하는 방식으로 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Home() {
    val featured = remember { PostRepo.getFeaturedPost() }
    val posts = remember { PostRepo.getPosts() }
    JetnewsTheme {
        Scaffold(
            topBar = { AppBar() }
        ) { innerPadding -&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 화면인 &lt;code&gt;Home&lt;/code&gt; 컴포저블을 위에서 정의하였던 테마를 사용하도록 수정한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Preview(&quot;Featured Post&quot;)
@Composable
private fun FeaturedPostPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme {
        FeaturedPost(post = post)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰도 정의하여 미리 볼 수 있도록 만들자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테마 색상 설정&lt;/h2&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 테마의 색상을 커스텀 하기 위해서 전역 필드로 컬러 값을 정의해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun JetnewsTheme(content: @Composable () -&amp;gt; Unit) {
    MaterialTheme(
        colors = LightColors,
        content = content
    )
}

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 컬러셋을 하나 생성하고, 위에서 정의했던 컬러를 사용하여, &lt;code&gt;MaterialTheme&lt;/code&gt; 의 colors 인자로 전달한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Typography 설정&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글자 스타일 정의를 위해 FontFamily 를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Composable
fun JetnewsTheme(content: @Composable () -&amp;gt; Unit) {
    MaterialTheme(
        colors = LightColors,
        typography = JetnewsTypography,
        shapes = JetnewsShapes,
        content = content
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MaterialTheme&lt;/code&gt; 의 typography 로 전달하면 적용 끝.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Text(
                text = post.title,
                style = MaterialTheme.typography.h6,
                modifier = padding
            )
            Text(
                text = post.metadata.author.name,
                style = MaterialTheme.typography.body2,
                modifier = padding
            )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 style 에 전달하여 커스텀 스타일을 사용가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다크 테마 사용하기&lt;/h2&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다크 테마에 사용될 &lt;code&gt;darkColors&lt;/code&gt; 를 정의해주고,&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Composable
fun JetnewsTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -&amp;gt; Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        typography = JetnewsTypography,
        shapes = JetnewsShapes,
        content = content
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설정이 다크테마인 경우에 다크컬러셋을 사용하도록 수정하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다크 모드 프리뷰&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Preview(&quot;Featured Post &amp;bull; Dark&quot;)
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰에서 다크모드를 확인할 수 있도록 추가한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Surface 로 배경색 지정하기&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun Header(
    text: String,
    modifier: Modifier = Modifier
) {
    Surface(
        color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
        contentColor = MaterialTheme.colors.primary,
        modifier = modifier
    ) {
        Text(
            text = text,
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 8.dp)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배경색을 지정할 때는 Surface 를 사용하는 것이 권장된다. 테마의 컬러를 사용하고 있으며 알파 값만 수정하기 위해 copy 메서드를 활용하여 개별적으로 수정된 색상을 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CompositionLocalProvider&lt;/h2&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
        Text(
            text = text,
            modifier = modifier
        )
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 알파를 사용하여 추천 게시물의 정보 계층 구조를 명확히 합니다. Home.kt의 PostMetadata 컴포저블에서 메타데이터 medium을 강조&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;primarySurface&lt;/h2&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;backgroundColor = MaterialTheme.colors.primarySurface&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primarySurface 를 사용하여 다크/라이트 테마일 때 적절한 색상 사용&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;withStyle 로 개별 스타일 적용&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val text = buildAnnotatedString {
        append(post.metadata.date)
        append(divider)
        append(stringResource(R.string.read_time, post.metadata.readTimeMinutes))
        append(divider)
        val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
            background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
        )
        post.tags.forEachIndexed { index, tag -&amp;gt;
            if (index != 0) {
                append(tagDivider)
            }
            withStyle(tagStyle) {
                append(&quot; ${tag.uppercase(Locale.getDefault())} &quot;)
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;buildAnnotatedString 에서 withStyle 로 개별 배경 스타일 주기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스판 스타일 적용과 비슷하게 withStyle 로 특정 텍스트에 특정 스타일을 적용할 수도 있다. 여기서는 태그 글씨 뒷 배경으로 연한 색상을 깔아준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Modifier.clip&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;Image(
          painter = painterResource(post.imageThumbId),
          modifier = Modifier.clip(shape = MaterialTheme.shapes.small),
          contentDescription = null
      )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클립 효과를 주어 이미지 코너를 잘라 보여줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 현재 커스텀 테마 Shapes 는 아래와 같이 적용되어 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;figure id=&quot;og_1671025700055&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&quot; data-og-description=&quot;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; data-og-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/647&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.12.14 - [Android/Jetpack Compose] - [Android] compose basic state (상태의 기본 내용 정리)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/646&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.15 - [Android/Google Play] - 구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/645&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Jetpack Compose] - [Android] This annotation should be used with the compiler argument '-opt-in=kotlin.RequiresOptIn' 해결하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <category>android</category>
      <category>Jetpack Compose</category>
      <category>Theme</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/648</guid>
      <comments>https://whyprogrammer.tistory.com/648#entry648comment</comments>
      <pubDate>Thu, 15 Dec 2022 19:02:38 +0900</pubDate>
    </item>
    <item>
      <title>[Jetpack Compose] compose basic state (상태의 기본 내용 정리)</title>
      <link>https://whyprogrammer.tistory.com/647</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈에서 상태는 어떻게 정의하고 사용하는지 알아보는 코드랩을 진행하였다. 코드를 보면서 전체적인 이해를 해보도록 하자.&lt;/p&gt;
&lt;h1&gt;Composable 에서의 상태&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    val count = 0
    Text(
        text = &quot;You've had $count glasses.&quot;,
        modifier = modifier.padding(16.dp)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composable 함수 (WaterCounter 함수)를 사용하여 새 파일 WaterCounter.kt 를 생성 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
    WaterCounter(modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 화면을 나타내는 WellnessScreen.kt 파일을 만들고 WaterCounter 함수를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicStateCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    WellnessScreen()
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 액티비티 에서 이를 메인 화면으로 호출한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count = 0
        Text(&quot;You've had $count glasses.&quot;)
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WaterCounter 를 &lt;code&gt;Column&lt;/code&gt; 으로 포함하고, 버튼 클릭 시 마다 카운트가 갱신 되도록 설정한다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        // Changes to count are now tracked by Compose
        val count: MutableState&amp;lt;Int&amp;gt; = mutableStateOf(0)

        Text(&quot;You've had ${count.value} glasses.&quot;)
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mutableStateOf&lt;/code&gt; 으로 카운트를 수정한다. 하지만 수정이 반영되지 않는다. 왜냐하면 리컴포지션이 일어나면서 동시에 다시 &lt;code&gt;count&lt;/code&gt; 변수가 0으로 초기화 되기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;remember&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState&amp;lt;Int&amp;gt; = remember { mutableStateOf(0) } //+

        Text(&quot;You've had ${count.value} glasses.&quot;)
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;remember&lt;/code&gt; 를 사용하여 상태를 정의하면 리컴포지션 시에도 이를 유지할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;by 델리게이트&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) } //+

        Text(&quot;You've had $count glasses.&quot;) //+
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) { //+
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;by&lt;/code&gt; 델리게이트를 사용하여 상태를 정의, 변수명에 바로 접근하여 값을 수정 가능하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }

        if (count &amp;gt; 0) {
            // This text is present if the button has been clicked
            // at least once; absent otherwise
            Text(&quot;You've had $count glasses.&quot;)
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번이라도 버튼을 클릭해야 ui가 존재하도록 설정한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }

        if (count &amp;gt; 0) {
            // This text is present if the button has been clicked
            // at least once; absent otherwise
            Text(&quot;You've had $count glasses.&quot;)
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count &amp;lt; 10) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼이 10 미만 일 때만 활성화 되도록 설정&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter() {
    Column(modifier = Modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }

        if (count &amp;gt; 0) {
            var showTask by remember { mutableStateOf(true) }
            if (showTask) {
                WellnessTaskItem(
                    onClose = { },
                    taskName = &quot;Have you taken your 15 minute walk today?&quot;
                )
            }
            Text(&quot;You've had $count glasses.&quot;)
        }

        Button(onClick = { count++ }, enabled = count &amp;lt; 10) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운트가 1이상이면, TaskItem 을 표시&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter() {
    Column(modifier = Modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        if (count &amp;gt; 0) {
            var showTask by remember { mutableStateOf(true) }
            if (showTask) {
                WellnessTaskItem(
                    onClose = { showTask = false },
                    taskName = &quot;Have you taken your 15 minute walk today?&quot;
                )
            }
            Text(&quot;You've had $count glasses.&quot;)
        }

        Button(onClick = { count++ }, enabled = count &amp;lt; 10) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;닫은 경우 task 를 미표시 하도록 함.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        if (count &amp;gt; 0) {
            var showTask by remember { mutableStateOf(true) }
            if (showTask) {
                WellnessTaskItem(
                    onClose = { showTask = false },
                    taskName = &quot;Have you taken your 15 minute walk today?&quot;
                )
            }
            Text(&quot;You've had $count glasses.&quot;)
        }

        Row(Modifier.padding(top = 8.dp)) {
            Button(onClick = { count++ }, enabled = count &amp;lt; 10) {
                Text(&quot;Add one&quot;)
            }
            Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
                Text(&quot;Clear water count&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운트 클리어 버튼 추가, 이때 카운트가 0으로 클리어 되면서 &lt;code&gt;if(count&amp;gt;0)&lt;/code&gt; 라인틀 타지 않아 &lt;code&gt;showTask&lt;/code&gt; remember 변수가 제거 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;rememberSaveable&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by rememberSaveable { mutableStateOf(0) }
        if (count &amp;gt; 0) {
            Text(&quot;You've had $count glasses.&quot;)
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count &amp;lt; 10) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rememberSaveable&lt;/code&gt; 을 사용하면 구성변경 시에도 상태를 유지할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Stateless / Stateful&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun StatelessCounter(count: Int, onIncrement: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        if (count &amp;gt; 0) {
            Text(&quot;You've had $count glasses.&quot;)
        }
        Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count &amp;lt; 10) {
            Text(&quot;Add one&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatelessCounter 상태를 갖지 않는 컴포저블&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
    var count by rememberSaveable { mutableStateOf(0) }
    StatelessCounter(count, { count++ }, modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulCounter 상태를 갖는 컴포저블&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
    StatefulCounter(modifier)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stateful 한 컴포저블 사용하기&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
    StatefulCounter()
}

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 가지지 않는 컴포저블을 재사용하는 예시 (&lt;code&gt;StatelessCounter&lt;/code&gt;)&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -&amp;gt; Unit,
    onClose: () -&amp;gt; Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = &quot;Close&quot;)
        }
    }
}

@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
    var checkedState by remember { mutableStateOf(false) }

    WellnessTaskItem(
        taskName = taskName,
        checked = checkedState,
        onCheckedChange = { newValue -&amp;gt; checkedState = newValue },
        onClose = {}, // we will implement this later!
        modifier = modifier,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 상태를 가지는 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 과 아닌 &lt;code&gt;WellnessTaskItem&lt;/code&gt; 상태를 가지지 않는 컴포저블의 경우에는 상태에 대한 콜백을 함수 파라미터로 받으며, 상태를 상위로 끌어올렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;observable 한 list&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        StatefulCounter()

        val list = remember { getWellnessTasks().toMutableStateList() }
        WellnessTasksList(list = list, onCloseTask = { task -&amp;gt; list.remove(task) })
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찰가능한 MutableList (list.remove 를 알아챈다)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessTasksList(
    list: List&amp;lt;WellnessTask&amp;gt;,
    onCloseTask: (WellnessTask) -&amp;gt; Unit, // list 를 상위 수준으로 호이스팅 했기 때문에, 람다로 close 콜백을 받는다.
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier = modifier) {
        items(
            items = list,
            key = { task -&amp;gt; task.id }
        ) { task -&amp;gt;
            WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;list 를 상위 수준으로 호이스팅 했기 때문에, 람다로 close 콜백을 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 rememberSavable 로 리스트를 저장하는 경우에는 에러가 발생한다. (커스텀 save 기능을 정의해 주어야 하는데, 큰 데이터의 경우 Saveable 로 저장하는 것은 권장되지 않는다.)&lt;/p&gt;
&lt;h1&gt;ViewModel 사용&lt;/h1&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List&amp;lt;WellnessTask&amp;gt;
        get() = _tasks

    fun remove(item: WellnessTask) {
        _tasks.remove(item)
    }
}

private fun getWellnessTasks() = List(30) { i -&amp;gt; WellnessTask(i, &quot;Task # $i&quot;) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel (MutableStateList 와 이를 수정하는 메서드를 갖는 뷰모델)&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;implementation &quot;androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel compose dependencies 추가&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
    Column(modifier = modifier) {
        StatefulCounter()

        WellnessTasksList(
            list = wellnessViewModel.tasks,
            onCloseTask = { task -&amp;gt; wellnessViewModel.remove(task) })
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WellnessScreen 에서 뷰모델 사용하기. viewModel()은 기존 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WellnessTask&lt;/code&gt; 데이터 클래스에 체크 상태를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List&amp;lt;WellnessTask&amp;gt;
        get() = _tasks

    fun remove(item: WellnessTask) {
        _tasks.remove(item)
    }

    fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
        tasks.find { it.id == item.id }?.let { task -&amp;gt;
            task.checked = checked
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel 에 체크 상태를 바꾸는 메서드 추가하고, 이전에 작성하였던 stateful 컴포저블을 제거한다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessTasksList(
    list: List&amp;lt;WellnessTask&amp;gt;,
    onCheckedTask: (WellnessTask, Boolean) -&amp;gt; Unit,
    onCloseTask: (WellnessTask) -&amp;gt; Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(
            items = list,
            key = { task -&amp;gt; task.id }
        ) { task -&amp;gt;
            WellnessTaskItem(
                taskName = task.label,
                checked = task.checked,
                onCheckedChange = { checked -&amp;gt; onCheckedTask(task, checked) },
                onClose = { onCloseTask(task) }
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 전반적 리팩토링을 해주고, 이제 뷰 모델로 부터 콜백을 제공받을 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
    Column(modifier = modifier) {
        StatefulCounter()

        WellnessTasksList(
            list = wellnessViewModel.tasks,
            onCheckedTask = { task, checked -&amp;gt;
                wellnessViewModel.changeTaskChecked(task, checked)
            },
            onCloseTask = { task -&amp;gt;
                wellnessViewModel.remove(task)
            }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 모델 로부터 콜백을 구현한다. 하지만 위 코드에는 버그가 있다 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로, Boolean 이기 때문에 재구성이 일어나지 않는다. (MutableState 를 사용해야한다.)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class WellnessTask(val id: Int, val label: String, val checked: MutableState&amp;lt;Boolean&amp;gt; = mutableStateOf(false))&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/**
 * WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경합니다. WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있습니다.
 */
class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경합니다. WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;ReadMe.md&lt;/h1&gt;
&lt;h1&gt;로직 유형 2가지&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비즈니스 로직 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직은 상태 변경 시(예: 결제하기 또는 사용자 환경설정 저장) 실행할 작업입니다. 이 로직은 대개 비즈니스 레이어나 데이터 영역에 배치되고 UI 레이어에는 배치되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI 로직 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 로직은 화면에 상태 변경을 표시하는 방법(예: 탐색 로직 또는 스낵바 표시)과 관련이 있습니다.&lt;/p&gt;
&lt;h1&gt;ViewModel&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;뷰모델의 역할&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel은 UI 상태와 앱의 다른 레이어에 있는 비즈니스 로직에 대한 액세스 권한을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;끈질긴 뷰모델의 생명주기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel은 구성 변경 후에도 유지되므로 컴포지션보다 전체 기간이 더 깁니다.&lt;/li&gt;
&lt;li&gt;Compose 콘텐츠 호스트의 수명 주기(즉, 활동이나 프래그먼트, Compose Navigation을 사용하는 경우 탐색 그래프의 대상)를 따를 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compose 에서 ViewModel 사용 시 주의 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경고: ViewModel은 컴포지션의 일부가 아닙니다. 따라서 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 상태(예: 기억된 값)를 보유해서는 안 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compose 의 &lt;code&gt;viewModel()&lt;/code&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;viewModel()은 기존 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성합니다.&lt;/li&gt;
&lt;li&gt;ViewModel 인스턴스는 범위가 활성화되어 있는 동안 유지됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 컴포저블이 활동에서 사용되는 경우 viewModel()은 활동이 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의 : 뷰모델을 다른 컴포저블로 전달해서는 안됩니다.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel은 탐색 그래프의 활동이나 프래그먼트, 대상에서 호출되는 루트 컴포저블에 가까운 화면 수준 컴포저블에서 사용하는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;ViewModel은 다른 컴포저블로 전달하면 안 됩니다. 대신 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 합니다.&lt;br /&gt;No newline at end of file&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&gt;
&lt;figure id=&quot;og_1671025526640&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&quot; data-og-description=&quot;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; data-og-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Compose 기본사항 &amp;nbsp;|&amp;nbsp; Jetpack Compose for Android Developers - Compose essentials&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/646&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.15 - [Android/Google Play] - 구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/645&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Jetpack Compose] - [Android] This annotation should be used with the compiler argument '-opt-in=kotlin.RequiresOptIn' 해결하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/644&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Groovy] - [Android Groovy] build.gradle plugins apply false? 왜 false 인가&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/647</guid>
      <comments>https://whyprogrammer.tistory.com/647#entry647comment</comments>
      <pubDate>Wed, 14 Dec 2022 22:45:39 +0900</pubDate>
    </item>
    <item>
      <title>구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션</title>
      <link>https://whyprogrammer.tistory.com/646</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 구글 플레이스토어 target API 수준 요구사항 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;APK 파일을 구글 플레이에 업로드 할 때, 구글 플레이가 요구하는 target API 레벨 요구사항을 충족해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글이 작성되는 시점 기준(2022.10.14)으로 새로운 앱은 반드시 Android 12 (API level 31) 이상을 타겟팅 해야합니다. (Wear OS 의 경우에는 Android 11(API 30) 또는 그 이상을 만족해야 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 11월 부터는 target SDK 수준을 31 로 하거나 그 이상으로 맞춰야 합니다. 그에 따라 나타나는 수정 사항도 적합하게 반영해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 2022년 부터, 안드로이드의 새 버전 디바이스를 사용하는 새로운 유저는 일부 오래된 앱을 사용할 수 없을 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 왜 새로운 SDK 를 타겟팅 하나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 새로운 안드로이드 버전은 새 업데이트 내역을 소개합니다. 이는 보안과 성능 향상 그리고 보다 나은 UX 가 포함됩니다. 몇몇 이런 변경 사항들은 오로지 명시적으로 targetSdkVersion 으로 매니페스트에 지정된 앱에 대해서만 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;만약 우리 앱이 여전히 오래된 버전의 안드로이드를 사용하고 있다면, &lt;/span&gt;유저들에게 더 좋은 경험과 이익을 주기위해 우리의 앱을 최신의 API level 로 타겟팅 설정 해야합니다. 타겟 sdk 를 올리면 우리 앱이 최신 기능을 사용할 수 있는 이점도 있습니다. 게다가 Andorid 10 (API level 29) 유저들은 Andorid 5.1 (API level 22) 이하를 타겟팅하는 앱을 최초 실행 시 경고를 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서는 우리가 타겟 sdk 를 업데이트 할 떄 중요한 점을 강조합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 버전에서 마이그레이트 하는 경우 아래 리스트를 확인 해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만약 Gradle 파일이 메니페스트 앤트리를 포함한다면, targetSdkVersion을 확인 또는 변경할 수 있습니다. 다른 대안 으로는 android:targetSdkVersion 속성을 매니페스트 파일에서 &amp;lt;uses-sdk&amp;gt; 속성을 통해&amp;nbsp;사용할 수 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Android 11 (API level 30) 에서 Android 12 (API level 31) 로 마이그레이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 권한 및 보안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블루투스: BLUETOOTH 와 BLUETOOTH_ADMIN 퍼미션을 BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, or BLUETOOTH_CONNECT 퍼미션 으로 교체해야 합니다. 더이상 LOCATION 퍼미션 요청은 필요 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치: 유저는 대략적인 위치 정보만 제공하도록 설정할 수 있습니다. ACCESS_COARSE_LOCATION 퍼미션을 ACCESS_FINE_LOCATION 퍼미션을 요청할 때 반드시 함께 요청해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텐트 필터: 만약 앱에서 activitys, services, or broadcast receivers 들이 인텐트 필터를 사용하고 있다면, 반드시 android:exported 속성을 해당 컴포넌트에 설정해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동면(Hibernation): 오랜 기간 사용이 되지 않는다면, 앱들은 동면 모드로 돌입할 수 있습니다. 동면 모드에서 앱에 주어진 권한과 캐시가 초기화 될 수 있습니다. 그리고 jobs 나 알림을 사용할 수 없게 됩니다.&amp;nbsp;&lt;a href=&quot;https://developer.android.com/topic/performance/app-hibernation&quot;&gt;앱 동면&lt;/a&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;에서 상태를 체크하는 방법을 확인 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;팬딩 인텐트 변경가능성: 반드시 앱에서 생성하는 각각 pending intent 객체 의 가변성을 특정해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;2.2. UX&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;알림 커스터마이징: &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;사용자 지정 콘텐츠 보기가 있는 알림은 더 이상 전체 알림 영역을 사용하지 않습니다. 대신 시스템이 표준 템플릿을 적용합니다. &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;이 템플릿은 사용자 지정 알림이 모든 상태에서 다른 알림과 동일한 장식을 갖도록 합니다. &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;이 동작은 Notification.DecoratedCustomViewStyle의 동작과 거의 동일합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #d1cdc7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;안드로이드 앱 링크 인증 변경: &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;Android 앱 링크 확인을 사용할 때 인텐트 필터에 BROWSABLE 카테고리가 포함되고 HTTPS 체계를 지원하는지 확인하세요.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #d1cdc7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;2.3. 성능&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #d1cdc7;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;포그라운드 서비스 실행 제한: Android 12 이상을 타겟팅 하는 앱에서는 백그라운드에서 포그라운드 서비스를 실행할 수 없게됩니다. 몇몇 특별한 경우를 제외하면 말이죠. &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;앱이 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작하려고 하면 예외가 발생합니다(몇몇 특별한 경우 제외).&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;앱이 백그라운드에서 실행되는 동안 WorkManager를 사용하여 빠른 작업을 예약하고 시작하는 것이 좋습니다. 사용자가 요청하는 시간에 민감한 작업을 완료하려면 정확한 알람 내에서 포그라운드 서비스를 시작하십시오.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;알림 trampoline 제한: &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;사용자가 알림을 탭하면 일부 앱은 사용자가 보고 상호 작용하는 활동을 시작하는 앱 구성 요소를 실행하여 응답합니다. 이 앱 구성 요소는 알림 트램폴린으로 불리고 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;앱은 알림 trampoline 을 통한 service 또는 broadcast receivers 로부터 액티비티를 실행해서는 안됩니다. &lt;span style=&quot;color: #252525;&quot; data-darkreader-inline-color=&quot;&quot;&gt;사용자가 알림 내의 알림 또는 작업 버튼을 탭한 후 앱은 서비스 또는 브로드캐스트 수신기 내부에서 startActivity()를 호출할 수 없습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;1952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwGmXe/btrOD4lFR8W/ewiwUpkVQWa7ZUgfEikhB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwGmXe/btrOD4lFR8W/ewiwUpkVQWa7ZUgfEikhB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwGmXe/btrOD4lFR8W/ewiwUpkVQWa7ZUgfEikhB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwGmXe%2FbtrOD4lFR8W%2FewiwUpkVQWa7ZUgfEikhB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1740&quot; height=&quot;1952&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;1952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/google/play/requirements/target-sdk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.android.com/google/play/requirements/target-sdk&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1665763774921&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google Play의 대상 API 수준 요구사항 충족하기 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;Google Play의 대상 API 수준 요구사항 충족하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. APK를 업로드하는 경우 Google Play의 대상 API 수준 요구사항을 충족&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/google/play/requirements/target-sdk&quot; data-og-url=&quot;https://developer.android.com/google/play/requirements/target-sdk?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.android.com/google/play/requirements/target-sdk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/google/play/requirements/target-sdk&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google Play의 대상 API 수준 요구사항 충족하기 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google Play의 대상 API 수준 요구사항 충족하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. APK를 업로드하는 경우 Google Play의 대상 API 수준 요구사항을 충족&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/645&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Jetpack Compose] - [Android] This annotation should be used with the compiler argument '-opt-in=kotlin.RequiresOptIn' 해결하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/644&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Groovy] - [Android Groovy] build.gradle plugins apply false? 왜 false 인가&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Google Play</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/646</guid>
      <comments>https://whyprogrammer.tistory.com/646#entry646comment</comments>
      <pubDate>Sat, 15 Oct 2022 00:47:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] This annotation should be used with the compiler argument '-opt-in=kotlin.RequiresOptIn' 해결하기</title>
      <link>https://whyprogrammer.tistory.com/645</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Android]&amp;nbsp;This&amp;nbsp;annotation&amp;nbsp;should&amp;nbsp;be&amp;nbsp;used&amp;nbsp;with&amp;nbsp;the&amp;nbsp;compiler&amp;nbsp;argument&amp;nbsp;'-opt-in=kotlin.RequiresOptIn'&amp;nbsp;해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose 사용하여 개발을 할 때 실험 적인 기능, 일부 베타 기능을 사용할 때 아래와 같은 어노테이션을 적용하여 사용하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1665314572529&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OptIn(ExperimentalMaterial3Api::class)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 어노테이션 적용 후 빌드, 컴파일은 문제 없이 잘 되지만, 경고가 발생하여 좀 거슬렸다. 이를 제거하기 위해 app 수준 build.gradle 에 아래와 같은 옵션을 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1665314653937&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;android {
    //...
    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
        kotlinOptions {
            freeCompilerArgs += &quot;-Xopt-in=kotlin.RequiresOptIn&quot;
        }
    }
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 sync 를 해주고 다시 돌아가보면 경고가 사라진 것을 볼 수 있다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/644&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.10.09 - [Android/Groovy] - [Android Groovy] build.gradle plugins apply false? 왜 false 인가&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/643&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.30 - [Android/Retrofit] - [Android] Retrofit HTTP Converter 결과값 파싱이 null 로 올 때 ☠️&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/642&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.08.23 - [SELECT *] - [Android] 해상도별 drawable 이미지 리소스 추가하기 (dpi 별)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/645</guid>
      <comments>https://whyprogrammer.tistory.com/645#entry645comment</comments>
      <pubDate>Sun, 9 Oct 2022 20:24:53 +0900</pubDate>
    </item>
    <item>
      <title>[Android Groovy] build.gradle plugins apply false? 왜 false 인가</title>
      <link>https://whyprogrammer.tistory.com/644</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Android&amp;nbsp;Groovy]&amp;nbsp;build.gradle&amp;nbsp;plugins&amp;nbsp;apply&amp;nbsp;false?&amp;nbsp;왜&amp;nbsp;false&amp;nbsp;인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 Groovy 를 통해 build.gradle 를 작성하여 빌드 설정을 할 수 있다. 이때 플러그인 적용 시 문법 중에 아래와 같은 문법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1665306215212&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'com.android.application' version '7.3.0' apply false
    id 'com.android.library' version '7.3.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 apply 에 항상 false 를 넣어주는 것 같아서 의아했다. 플러그인을 적용하려면 apply true 가 되어야 하는거 아닌가? 하는 의문이었다. 한 번 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1665306269717&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.gradle.plugin.use;

import javax.annotation.Nullable;
import org.gradle.api.Incubating;
import org.gradle.api.provider.Provider;

public interface PluginDependencySpec {
    PluginDependencySpec version(@Nullable String var1);

    @Incubating
    default PluginDependencySpec version(Provider&amp;lt;String&amp;gt; version) {
        return this.version((String)version.get());
    }

    PluginDependencySpec apply(boolean var1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apply 정의 내부로 들어가보면 위와 같이 boolean 타입의 var1 을 받는 것을 알 수 있다. 그렇다 이 것만 봐서는 알 수 있는게 전혀 없다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docs.gradle.org 에서 gradle 7.5.1 문서를 뒤져보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Groovy apply&lt;/h2&gt;
&lt;pre id=&quot;code_1665306356015&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PluginDependencySpec apply​(boolean apply)
Specifies whether the plugin should be applied to the current project. Otherwise it is only put on the project's classpath.
This is useful when reusing classes from a plugin or to apply a plugin to sub-projects:

 plugins {
     id &quot;org.company.myplugin&quot; version &quot;1.0&quot; apply false
 }

 subprojects {
     if (someCondition) {
         apply plugin: &quot;org.company.myplugin&quot;
     }
 }
 
Parameters:
apply - whether to apply the plugin to the current project or not. Defaults to true
Returns:
this&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살펴보니, apply 로 전달되는 불리언 값은 현재 플러그인을 현재 프로젝트에 (전체) 적용할지 또는 오로지 프로젝트의 classpath 에만 적용을 해줄 지 여부에 대해서 설정하는 옵션이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;default 값은 true 이기때문에, apply false 를 통해서 클래스 경로에만 적용을 해주는 것. 때로는 위 예시로 보면 subproject 전체에 적용해주고 싶은 경우에는 이를 생략하여 전달해주면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 왜 apply false 인지 알게되었다. applyProject 로 좀 더 직관적으로 알 수 있게 해주면 더 좋지 않았을까.  &amp;zwj; ️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/643&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.09.30 - [Android/Retrofit] - [Android] Retrofit HTTP Converter 결과값 파싱이 null 로 올 때 ☠️&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/642&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.08.23 - [SELECT *] - [Android] 해상도별 drawable 이미지 리소스 추가하기 (dpi 별)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/641&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.04.24 - [SELECT *] - [Hilt] Hilt Gradle 빌드 설정하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Groovy</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/644</guid>
      <comments>https://whyprogrammer.tistory.com/644#entry644comment</comments>
      <pubDate>Sun, 9 Oct 2022 18:10:28 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Retrofit HTTP Converter 결과값 파싱이 null 로 올 때 ☠️</title>
      <link>https://whyprogrammer.tistory.com/643</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Android]&amp;nbsp;Retrofit&amp;nbsp;HTTP&amp;nbsp;Converter&amp;nbsp;결과값&amp;nbsp;파싱이&amp;nbsp;null&amp;nbsp;로&amp;nbsp;올&amp;nbsp;때&amp;nbsp;☠️&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱 에서 Retrofit 을 통해 HTTP 통신(POST)을 했을 때, 일부 값이 응답에는 제대로 온 것을 확인 하였으나, Converter.Factory() 를 통과하면 null 로 값이 떨어지는 이슈가 있어서 삽질을 좀 하였다..(&lt;s&gt;원인은 정말 어이가 없었다는 결말&lt;/s&gt;)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;debug 상황 재현&lt;/h3&gt;
&lt;pre id=&quot;code_1664515275986&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface MyService {
    /**
     * # 거래 id 생성 for 등록
     */
    @POST(&quot;/MyService&quot;)
    suspend fun postMyService(@Body body: MyServiceParams): MyServiceModel?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 프로젝트 코드를 그대로 가져올 수는 없으니 해당 인터페이스를 사용하여 레스로핏 서비스를 빌드 한다고 가정한다. 이때 요청 body 의 경우는 응답은 로그상 잘 받아온 것으로 확인되어 확인할 필요는 없고 응답으로 떨어지느 MyServiceModel 과 ConverterFactory 를 붙여줄 때 넘기는 컨버터에 버그가 있다고 판단했다.&lt;/p&gt;
&lt;pre id=&quot;code_1664515403112&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    return Retrofit.Builder()
        .baseUrl(baseUrl)
        .client(getHttpClient())
        .addConverterFactory(getConverterFactory(envelopeClass))
        .addConverterFactory(defaultConverterFactory)
        .build()
        .create()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서 사용중인 레트로핏 빌드를 하는 사내 api 가 있는데 여기서 addConverterFactory 를 통해서 컨버터 팩토리를 붙여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 응답이 아래와 같다면&lt;/p&gt;
&lt;pre id=&quot;code_1664515574495&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;Result&quot;: {
    &quot;ResultCode&quot;: &quot;string&quot;,
    &quot;ResultMessage&quot;: &quot;string&quot;,
    &quot;ResultData&quot;: {
      &quot;name&quot;: &quot;string&quot;,
      &quot;nickname&quot;: &quot;string&quot;
    }
  },
  &quot;ResultBase&quot;: {
    &quot;ErrorMsg&quot;: &quot;string&quot;,
    &quot;ErrorCode&quot;: &quot;string&quot;,
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 응답이 오는 경우, 여기서 응답이 성공한 경우에는 ResultData 항목(이름, 닉네임)만 받아서 처리할 수 있게 데이터를 컨버팅 하여 보내주는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를통해 api 그룹 별로 각 결과가 동일한 형식으로 오기 때문에, 매번 새로운 데이터 클래스에 중복 정의해야하는 비효율 또한 개선이 가능하다. (즉 ResultData 만 별도로 제네릭 타입으로 빼서 정의 해 두면 된다.)&lt;/p&gt;
&lt;pre id=&quot;code_1664515670486&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class MyServiceModel(

    @SerializedName(&quot;Name&quot;)
    val name: String,

    @SerializedName(&quot;NickName&quot;)
    val nickname: String,
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무튼, 그리하여 우리가 필요한 데이터는 위와 같은 형식의 데이터가 된다. 여기 까지 봤을 때 아무 문제가 없어보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만! 앱을 run 시켜보면..&lt;/p&gt;
&lt;pre id=&quot;code_1664515953583&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;D/OkHttp: {&quot;Result&quot;:{&quot;ResultCode&quot;:&quot;000&quot;,&quot;ResultMessage&quot;:&quot;success&quot;,&quot;ResultData&quot;:{&quot;name&quot;:&quot;kim&quot;,&quot;nickname&quot;:&quot;wow&quot;}},&quot;ResultBase&quot;:{&quot;ReturnDomain&quot;:&quot;0&quot;,&quot;ReturnCode&quot;:&quot;000&quot;,&quot;ReturnValue&quot;:null,&quot;ErrorMessage&quot;:null,&quot;ProcCnt&quot;:0}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런식으로 Log 상에는 찍여서 제대로 응답이 온 것을 확인 할 수 있다. 하지만! 계속 실제 사용처에서 출력 값은 null 이 나왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7d0DK/btrNuepWkpe/gGPs6k9v1WVgfwWiGKRHfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7d0DK/btrNuepWkpe/gGPs6k9v1WVgfwWiGKRHfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7d0DK/btrNuepWkpe/gGPs6k9v1WVgfwWiGKRHfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7d0DK%2FbtrNuepWkpe%2FgGPs6k9v1WVgfwWiGKRHfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1260&quot; height=&quot;160&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하여 파싱하는 곳을 break point 로 걸어 디버그 해본 결과, 기본 result (resultCode, resultMessage 등) 영역의 데이터는 잘 파싱이 되는 듯 하였지만, 서비스 모델의 부분은 null 로 파싱되는 문제가 발견되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;왜 되지&quot; 보다 심각한 &quot;왜 안되지&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아무리 코드를 둘러보고, 파싱하는 부분을 다르게 바꿔보고 해볼 수 있는 건 다 해보았지만 안되었고 코드상으로 문제가 전혀 없어보였다. 도대체 왜 안되지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 기적같이 혹시 하는 생각으로 아래 녀석을 의심하게된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SerializedName&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SerializedName 은 gson 파싱 시, 파싱 데이터의 타겟 이름을 지칭하고 이를 변환하여 우리가 정해준 변수명을 가지는 타입으로 적절하게 변환해주는 역할을 하는데 이 녀석이 잘못 동작하는거 아닌가 하는 킹리적 갓심을 하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1664516520382&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.서드파티라이브러리.gson.annotations.SerializedName&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 진짜 이왜진! 심상치 않은 녀석이 import 되어있었다. 최근에 추가된 라이브러리로 내부적으로는 gson 어노테이션과 내용은 동일 하였으나, 이것이 문제가 되는 것 같았다.&lt;/p&gt;
&lt;pre id=&quot;code_1664516578201&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.google.gson.annotations.SerializedName&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거 바꿔서 되면 정말 어이없을 것 같은데 하는 마음으로 위 import 로 교체 하고 재빌드 후 실행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는? ... 아주 잘 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 황당 했던 것은 IDE 에서 import 할 수 있는 어노테이션이 중복인데도 불구하고 이를 선택하겠냐는 물음도 없이 문제의 패키지를 import 하였다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 해결되서 정말 기뻤고 앞으로는 IDE를 좀 더 의심해보고 import 문도 debug 항목에 포함 해야겠다는 다짐을 하게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/642&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.08.23 - [SELECT *] - [Android] 해상도별 drawable 이미지 리소스 추가하기 (dpi 별)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/641&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.04.24 - [SELECT *] - [Hilt] Hilt Gradle 빌드 설정하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/640&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.04.23 - [SELECT *] - [Hilt] Hilt 를 사용 하면 좋은 점 (Dagger2 에비해 장점)&lt;/a&gt;&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 488px; top: 252.234px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Android/Retrofit</category>
      <category>android</category>
      <category>retrofit</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/643</guid>
      <comments>https://whyprogrammer.tistory.com/643#entry643comment</comments>
      <pubDate>Fri, 30 Sep 2022 14:47:09 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 해상도별 drawable 이미지 리소스 추가하기 (dpi 별)</title>
      <link>https://whyprogrammer.tistory.com/642</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Android] 해상도별 drawable 이미지 리소스 추가하기 (hdpi, mdipi, xhdpi, xxdpi, xxxdpi)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발 시, 이미지 (png 등) 리소스를 추가할 때, 여러 해상도에서 깨지지 않는 화질의 이미지를 표현하고자 할 때 dpi 별 이미지를 추가하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hdpi, mdpi, xhdpi, xxdpi 등 4개의 각각 해상도의 png 를 디자이너로 부터 제공받았고 이를 수동으로 추가하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;1145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9F2B4/btrKkS4JGp0/exkCPwmeIotcP9Ue0w4K71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9F2B4/btrKkS4JGp0/exkCPwmeIotcP9Ue0w4K71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9F2B4/btrKkS4JGp0/exkCPwmeIotcP9Ue0w4K71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9F2B4%2FbtrKkS4JGp0%2FexkCPwmeIotcP9Ue0w4K71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;1145&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;1145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 drawable 폴더로 이동합니다. 프로젝트 네비게이션 탭에서 drawable 폴더 우클릭 후, open in finder 를 통해 해당 폴더로 바로 이동할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xxj34/btrKlpHZ4UU/xvo49f3V7aDGirIdYDjWRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xxj34/btrKlpHZ4UU/xvo49f3V7aDGirIdYDjWRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xxj34/btrKlpHZ4UU/xvo49f3V7aDGirIdYDjWRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxxj34%2FbtrKlpHZ4UU%2Fxvo49f3V7aDGirIdYDjWRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;742&quot; height=&quot;439&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위와 같이 drawbla 이 존재하는 것을 볼 수 있습니다. 없으면 위와 동일하게 폴더를 만들어주시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mGTxz/btrKkhxdYVN/vMHxsmXSnMlO1dVKkkoTk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mGTxz/btrKkhxdYVN/vMHxsmXSnMlO1dVKkkoTk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mGTxz/btrKkhxdYVN/vMHxsmXSnMlO1dVKkkoTk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmGTxz%2FbtrKkhxdYVN%2FvMHxsmXSnMlO1dVKkkoTk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;338&quot; height=&quot;270&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 제공 받은 이미지 를 복사하여 해당 폴더에 그대로 넣어주시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5heK0/btrKlTWbdD0/kX0PedFrRLZDWkq4YdMaZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5heK0/btrKlTWbdD0/kX0PedFrRLZDWkq4YdMaZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5heK0/btrKlTWbdD0/kX0PedFrRLZDWkq4YdMaZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5heK0%2FbtrKlTWbdD0%2FkX0PedFrRLZDWkq4YdMaZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;516&quot; height=&quot;260&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 폴더 구조를 그대로 복사하여 붙여넣어서 경고 메세지가 뜨는데, 이때는 주의해야할 점이 대치 가 아닌 병합을 통해서 기존 폴더와 머지를 해주어야 기존 리소르는 유지된 채로 이미지 리소스 추가가 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 (nodpi)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 dpi 별 해상도 지원을 하지 않겠다 하면, nodpi 에 이미지 파일 하나만 추가하면됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;41&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bet117/btrKkSwSyxT/LDcMI6ou4VH2QYdZ7qbK9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bet117/btrKkSwSyxT/LDcMI6ou4VH2QYdZ7qbK9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bet117/btrKkSwSyxT/LDcMI6ou4VH2QYdZ7qbK9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbet117%2FbtrKkSwSyxT%2FLDcMI6ou4VH2QYdZ7qbK9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;422&quot; height=&quot;41&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;41&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 탐색기에서 보면 위와 같이 nodpi 라고 표기 되는 리소스로 확인 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/641&quot;&gt;2022.04.24 - [SELECT *] - [Hilt] Hilt Gradle 빌드 설정하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/640&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.04.23 - [SELECT *] - [Hilt] Hilt 를 사용 하면 좋은 점 (Dagger2 에비해 장점)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/639&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.04.23 - [SELECT *] - [Hilt] Dagger Hilt 개요&lt;/a&gt;&lt;/p&gt;</description>
      <category>android</category>
      <category>dpi</category>
      <category>안드로이드</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/642</guid>
      <comments>https://whyprogrammer.tistory.com/642#entry642comment</comments>
      <pubDate>Tue, 23 Aug 2022 18:29:46 +0900</pubDate>
    </item>
    <item>
      <title>[Hilt] Hilt Gradle 빌드 설정하기</title>
      <link>https://whyprogrammer.tistory.com/641</link>
      <description>&lt;h1&gt;Gradle Build Setup&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt를 사용하려면 Android Gradle &lt;b&gt;모듈&lt;/b&gt;의 build.gradle 파일에 다음 빌드 종속성을 추가하세요.&lt;/p&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;dependencies {
  implementation 'com.google.dagger:hilt-android:2.41'
  annotationProcessor 'com.google.dagger:hilt-compiler:2.41'

  // For instrumentation tests
  androidTestImplementation  'com.google.dagger:hilt-android-testing:2.41'
  androidTestAnnotationProcessor 'com.google.dagger:hilt-compiler:2.41'

  // For local unit tests
  testImplementation 'com.google.dagger:hilt-android-testing:2.41'
  testAnnotationProcessor 'com.google.dagger:hilt-compiler:2.41'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kotlin에서 Hilt 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin을 사용하는 경우 &lt;a href=&quot;https://kotlinlang.org/docs/reference/kapt.html&quot;&gt;kapt 플러그인&lt;/a&gt;을 적용하고 annotationProcessor 대신 kapt를 사용하여 컴파일러 종속성을 선언합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 correctErrorTypes를 true로 설정하여 오류 유형을 수정하도록 kapt를 구성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hilt Gradle plugin&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt Gradle 플러그인은 바이트코드 변환을 실행하여 API를 더 쉽게 사용할 수 있도록 합니다. 생성된 클래스가 기본 클래스의 메서드에 대한 코드 완성을 방해할 수 있기 때문에 플러그인은 IDE에서 더 나은 개발자 경험을 위해 만들어졌습니다. 문서 전체의 예제에서는 플러그인 사용을 가정합니다. Hilt Gradle 플러그인을 구성하려면 먼저 프로젝트의 &lt;b&gt;루트&lt;/b&gt; build.gradle 파일에서 종속성을 선언합니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;buildscript {
  repositories {
    // other repositories...
    mavenCentral()
  }
  dependencies {
    // other plugins...
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 Android Gradle &lt;b&gt;모듈&lt;/b&gt;의 build.gradle에서 플러그인을 적용합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;apply plugin: 'com.android.application'
apply plugin: 'dagger.hilt.android.plugin'

android {
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고: Hilt Gradle 플러그인은 annotation processor 인수를 설정합니다. annotation processor 인수가 필요한 다른 라이브러리를 사용하는 경우 인수를 재정의하는 대신 추가하고 있는지 확인하십시오. 예는 &lt;a href=&quot;https://dagger.dev/hilt/gradle-setup#applying-other-processor-arguments&quot;&gt;아래&lt;/a&gt;를 참조하십시오.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Why use the plugin?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle 플러그인의 한 가지 이점은 Hilt의 생성된 클래스(Hilt&amp;rsquo;s generated classes)를 참조할 필요가 없기 때문에 &lt;code&gt;@AndroidEntryPoint&lt;/code&gt; 및 &lt;code&gt;@HiltAndroidApp&lt;/code&gt;을 더 쉽게 사용할 수 있다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle 플러그인이 없으면 기본 클래스가 annotation에 지정되어야 하고 annotation이 있는 클래스는 생성된 클래스를 확장해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;@HiltAndroidApp(MultiDexApplication::class)
class MyApplication : Hilt_MyApplication()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle 플러그인을 사용하면 주석이 달린 클래스가 기본 클래스를 직접 확장할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@HiltAndroidApp
class MyApplication : MultiDexApplication()&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Aggregating Task&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt Gradle 플러그인은 전용 Gradle 작업에서 Hilt의 클래스 경로 집계(classpath aggregation)를 수행하기 위한 옵션을 제공합니다. 이렇게 하면 Hilt 주석 프로세서가 &lt;a href=&quot;https://docs.gradle.org/current/userguide/java_plugin.html#isolating_annotation_processors&quot;&gt;격리&lt;/a&gt;되어 필요할 때만 호출됩니다. 이렇게 하면 증분 변경으로 인해 Dagger 구성 요소가 다시 빌드되는 빈도가 감소하여 증분 컴파일 시간이 줄어듭니다. 이 옵션을 활성화하면 &lt;a href=&quot;https://dagger.dev/hilt/flags.html#sharing-test-components&quot;&gt;테스트 구성 요소&lt;/a&gt;와 &lt;a href=&quot;https://dagger.dev/hilt/gradle-setup#classpath-aggregation&quot;&gt;클래스 경로 집계&lt;/a&gt;를 공유할 수도 있습니다. 이 옵션은 주의 사항 없이 동일한 이점이 있으므로 &lt;code&gt;enableExperimentalClasspathAggregation&lt;/code&gt;을 대체합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집계 작업을 활성화하려면 Android 모듈의 build.gradle에 다음 구성을 적용하세요.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;hilt {
    enableAggregatingTask = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Applying other processor arguments&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt Gradle 플러그인은 주석 프로세서 인수(annotation processor arguments)를 설정합니다. 주석 프로세서 인수(annotation processor arguments)가 필요한 다른 라이브러리를 사용하는 경우 인수를 재정의하는 대신 추가하고 있는지 확인하십시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음은 특히 +=를 사용하여 Hilt 인수 재정의를 방지합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;javaCompileOptions {
  annotationProcessorOptions {
    arguments += [&quot;foo&quot; : &quot;bar&quot;]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+가 누락되고 인수가 재정의되면 Hilt가 다음과 같은 오류와 함께 컴파일에 실패할 가능성이 높습니다. @HiltAndroidApp에 값이 있어야 합니다. Gradle 플러그인 적용을 잊으셨나요? (&lt;code&gt;Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?&lt;/code&gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Local test configuration (AGP &amp;lt; 4.2 only)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고: 이 플래그는 AGP &amp;lt; 4.2에서만 사용해야 합니다. 최신 버전의 AGP에는 더 이상 이 플래그가 필요하지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 사용된 Android Gradle 플러그인(AGP) 버전이 4.2 미만인 경우 기본적으로 Hilt Gradle 플러그인은 계측된 테스트 클래스(일반적으로 androidTest 소스 폴더에 있음)만 변환하지만 그러나 플러그인이 로컬 jvm 테스트(일반적으로 테스트 소스 폴더에 있음)를 변환하려면 추가 구성이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 jvm 테스트에서 @AndroidEntryPoint 클래스 변환을 활성화하려면 모듈의 build.gradle에 다음 구성을 적용하세요.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;hilt {
    enableTransformForLocalTests = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;enableTransformForLocalTests 구성은 명령줄에서 실행할 때만 작동합니다. ./gradlew 테스트. Android Studio에서 테스트를 실행할 때는 작동하지 않습니다(테스트 메서드 또는 클래스의 재생 버튼을 통해). 이 문제를 해결할 수 있는 몇 가지 옵션이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 옵션은 프로젝트의 AGP 버전을 4.2 이상으로 업그레이드하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 옵션은 Gradle 작업을 통해 테스트를 실행하는 자체 Android Studio 구성을 만드는 것입니다. 이렇게 하려면 다음 매개변수를 사용하여 Android Studio 내에서 'Gradle' 유형의 새 '실행 구성'을 만듭니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gradle 프로젝트: 테스트가 위치한 Gradle 모듈&lt;/li&gt;
&lt;li&gt;작업: 테스트 작업(일반적으로 test 또는 testDebug)&lt;/li&gt;
&lt;li&gt;인수: 테스트 목록(예: --tests MyTestClassSee)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래 설정을 참조하세요.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dagger.dev/hilt/robolectric-test-configuration.jpg&quot; alt=&quot;설정&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Classpath Aggregation (Deprecated)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고: 이 플래그는 더 이상 사용되지 않으며 Dagger의 향후 릴리스에서 제거됩니다. 대신 enableAggregatingTask를 사용하십시오.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt Gradle 플러그인은 또한 Hilt 및 Dagger가 애플리케이션 Gradle 모듈 내에서 모든 전이적 종속성에 걸쳐 클래스를 탐색하고 검사할 수 있도록 주석 처리를 위한 컴파일 클래스 경로를 구성하기 위한 실험적 옵션을 제공합니다. 이 옵션이 없으면 구현 종속성이 @InstallIn 모듈 또는 @EntryPoint 인터페이스에 대한 중요한 정보를 컴파일 클래스 경로에서 삭제할 수 있으므로 이 옵션을 활성화하는 것이 좋습니다. 이것은 다중 바인딩의 경우 런타임에만 나타날 수 있는 미묘하고/또는 혼란스러운 오류로 이어질 수 있습니다. 이 옵션을 사용하면 구현 종속성을 api로 완화할 필요가 없습니다. 이 옵션은 컴파일 클래스 경로의 증가로 인해 빌드 성능에 영향을 미칠 수 있습니다. 이 방법으로 해결되는 문제에 대한 자세한 내용은 문제 #1991 및 #970을 참조하세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고: 프로젝트에 사용된 Android Gradle 플러그인 버전이 7.0 미만인 경우 이전 버전 AGP의 기존 버그로 인해 enableExperimentalClasspathAggregation이 true로 설정되었을 때 android.lintOptions.checkReleaseBuilds를 false로 설정해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 경로 집계를 활성화하려면 Android 모듈의 build.gradle에 다음 구성을 적용하세요.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;hilt {
    enableExperimentalClasspathAggregation = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dagger.dev/hilt/gradle-setup&quot;&gt;hilt/gradle-setup&lt;/a&gt;&lt;/p&gt;</description>
      <category>Hilt Gradle 빌드 설정하기</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/641</guid>
      <comments>https://whyprogrammer.tistory.com/641#entry641comment</comments>
      <pubDate>Sun, 24 Apr 2022 11:41:07 +0900</pubDate>
    </item>
    <item>
      <title>[Hilt] Hilt 를 사용 하면 좋은 점 (Dagger2 에비해 장점)</title>
      <link>https://whyprogrammer.tistory.com/640</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Hilt] Hilt 를 사용 하면 좋은 점 (Dagger2 에비해 장점)&lt;/h2&gt;
&lt;h1&gt;Benefits of using Hilt&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 힐트를 사용합니까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상용구 감소&lt;/li&gt;
&lt;li&gt;분리된 빌드 종속성&lt;/li&gt;
&lt;li&gt;단순화된 구성&lt;/li&gt;
&lt;li&gt;향상된 테스트&lt;/li&gt;
&lt;li&gt;표준화된 components&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reduced boilerplate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt의 목표는 사용자가 Dagger 설정의 나머지 부분에 대해 걱정할 필요 없이 Dagger 바인딩 정의 및 사용법에 집중할 수 있도록 하는 것입니다. 즉, 모듈 및 인터페이스 목록이 있는 component 정의, 수명 주기의 올바른 지점에서 구성 요소를 만들고 유지하는 코드, 상위 구성 요소를 가져오기 위한 인터페이스 및 캐스트 등과 같은 것을 숨깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 단순성은 모놀리식 구성 요소를 사용하는 Hilt에서도 나옵니다(즉, 모든 활동에 단일 구성 요소 사용, 모든 조각에 단일 구성 요소 사용 등). Hilt는 기본적으로 전역 바인딩 네임스페이스를 권장하므로 어떤 활동이나 조각에서 주입되었는지 추적하지 않고도 어떤 바인딩 정의가 사용되는지 쉽게 알 수 있습니다. 이 디자인 결정에 대한 자세한 내용은 &lt;a href=&quot;https://dagger.dev/hilt/monolithic.html&quot;&gt;여기&lt;/a&gt;를 읽어보세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Decoupled build dependencies&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger를 순진하게 사용하면 코드가 Dagger 구성 요소를 직접 참조하는 경우 빌드 문제가 발생할 수 있습니다. 이러한 문제는 Dagger 구성 요소에 설치된 모든 모듈에 대한 참조가 있기 때문에 발생합니다. 이로 인해 빌드 속도가 느려지는 종속성이 커질 수 있습니다. 이를 해결하는 자연스러운 방법에는 인터페이스와 안전하지 않은 캐스트가 포함됩니다. 런타임 오류가 발생할 수 있기 때문에 이것은 절충안입니다. 예를 들어, 새로운 인젝터 인터페이스를 도입하면 컴포넌트에 직접 의존하는 것을 피할 수 있지만 컴포넌트가 인젝터 인터페이스를 확장하는 것을 잊어버리면 캐스트 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(unsafe casts)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스, 안전하지 않은 캐스트 및 모듈/인터페이스 목록을 후드 아래에서 생성하는 코드를 통해 Hilt는 코드 생성 및 모듈/진입점 검색의 보장으로 인해 이러한 런타임 안전하지 않은 캐스트를 안전하게 만듭니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Configuration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에는 기능이 다른 프로덕션 또는 개발 빌드와 같이 빌드 구성이 다른 경우가 많습니다. 이러한 다양한 기능 집합은 종종 다른 Dagger 모듈 집합을 의미합니다. 일반 Dagger 빌드에서 다른 모듈 세트에는 일반적으로 많은 부분이 반복되는 별도의 구성 요소 트리(모든 범위에 대해 별도의 구성 요소)가 있어야 합니다. Hilt는 빌드 종속성을 통해 모듈을 설치하고 코드는 구성 요소를 생성하므로 빌드의 다른 특징을 만드는 것은 추가 또는 제거된 종속성을 사용하여 컴파일하는 것만큼 간단합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Testing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger로 테스트하는 것은 위에서 언급한 구성 문제로 인해 어려울 수 있습니다. Hilt는 마찬가지로 구성 요소의 코드 생성으로 인해 테스트 모듈 및 바인딩을 쉽게 변경할 수 있습니다. Hilt에는 모듈을 관리하고 테스트에서 Dagger를 사용할 수 있도록 테스트 바인딩을 더 쉽게 제공하는 특정 테스트 유틸리티가 내장되어 있습니다. 테스트에서 Dagger를 사용하면 테스트에서 상용구를 줄이는 데 도움이 되며 프로덕션에서 인스턴스화되는 것과 동일한 방식으로 코드를 인스턴스화하여 테스트를 더욱 강력하게 만듭니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Standardization&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt는 &lt;a href=&quot;https://dagger.dev/hilt/components.html#component-heirarchy&quot;&gt;구성 요소 계층을 표준화&lt;/a&gt;합니다. 이는 Hilt와 통합되는 라이브러리가 이러한 알려진 구성 요소의 바인딩을 쉽게 추가하거나 사용할 수 있음을 의미합니다. 이를 통해 Hilt 앱에 더 간단하고 명확하게 통합할 수 있는 더 복잡한 라이브러리를 구축할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;figure id=&quot;og_1650676906100&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Benefits of using Hilt&quot; data-og-description=&quot;Why use Hilt? Reduced boilerplate Decoupled build dependencies Simplified configuration Improved testing Standardized components Reduced boilerplate The goal of Hilt is to enable users to focus on the Dagger binding definitions and usages without needing t&quot; data-og-host=&quot;dagger.dev&quot; data-og-source-url=&quot;https://dagger.dev/hilt/benefits&quot; data-og-url=&quot;https://dagger.dev/hilt/benefits&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dagger.dev/hilt/benefits&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dagger.dev/hilt/benefits&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Benefits of using Hilt&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Why use Hilt? Reduced boilerplate Decoupled build dependencies Simplified configuration Improved testing Standardized components Reduced boilerplate The goal of Hilt is to enable users to focus on the Dagger binding definitions and usages without needing t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dagger.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: -51px; top: 135.797px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Benefits of using Hilt</category>
      <category>Hilt</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/640</guid>
      <comments>https://whyprogrammer.tistory.com/640#entry640comment</comments>
      <pubDate>Sat, 23 Apr 2022 11:21:58 +0900</pubDate>
    </item>
    <item>
      <title>[Hilt] Dagger Hilt 개요</title>
      <link>https://whyprogrammer.tistory.com/639</link>
      <description>&lt;h1&gt;Hilt&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt는 Dagger 종속성 주입을 Android 애플리케이션에 통합하는 표준 방법을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt의 목표는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Android 앱용 Dagger 관련 인프라를 단순화합니다.&lt;/li&gt;
&lt;li&gt;앱 간의 설정, 가독성/이해 및 코드 공유를 용이하게 하기 위해 표준 component 및 범위 세트를 생성합니다.&lt;/li&gt;
&lt;li&gt;다양한 빌드 유형(예: 테스트, 디버그 또는 릴리스)에 서로 다른 바인딩을 프로비저닝하는 쉬운 방법을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hilt Design Overview&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt는 Dagger 설정 코드를 생성하는 코드로 작동합니다. 이것은 Dagger를 사용하는 대부분의 상용구를 없애고 실제로 개체를 생성하는 방법과 개체를 주입할 위치를 정의하는 측면만 남깁니다. Hilt는 Dagger component와 코드를 생성하여 Android 클래스(예: activities 및 fragments)를 자동으로 삽입합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hilt는 전이 클래스 경로를 기반으로 표준 Android Dagger component 세트를 생성합니다. 이를 위해서는 Hilt 주석으로 Dagger 모듈을 표시하여 Hilt에 들어가야 하는 component를 알려야 합니다. Android 프레임워크 클래스에서 객체를 가져오는 것은 확장할 기본 클래스에 Dagger 주입 코드를 생성하는 다른 Hilt 주석을 사용하여 수행됩니다. Gradle 사용자의 경우 이 클래스를 확장하는 것은 내부에서 &lt;a href=&quot;https://dagger.dev/hilt/gradle-setup.html#hilt-gradle-plugin&quot;&gt;바이트코드 변환&lt;/a&gt;으로 수행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 Hilt는 프로덕션과 마찬가지로 Dagger component를 생성합니다. 테스트에는 테스트 바인딩을 추가하거나 교체하는 데 도움이 되는 다른 특수 유틸리티가 있습니다.&lt;/p&gt;
&lt;h1&gt;Reference&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dagger.dev/hilt/&quot;&gt;hilt&lt;/a&gt;&lt;/p&gt;</description>
      <category>Hilt</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/639</guid>
      <comments>https://whyprogrammer.tistory.com/639#entry639comment</comments>
      <pubDate>Sat, 23 Apr 2022 10:20:42 +0900</pubDate>
    </item>
    <item>
      <title>[Dagger] Dagger2 사용 방법</title>
      <link>https://whyprogrammer.tistory.com/638</link>
      <description>&lt;h1&gt;Dagger&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 애플리케이션에서 최고의 클래스는 다음과 같은 작업을 수행하는 클래스입니다: &lt;code&gt;BarcodeDecoder&lt;/code&gt;, &lt;code&gt;KoopaPhysicsEngine&lt;/code&gt; 및 &lt;code&gt;AudioStreamer&lt;/code&gt;. 이러한 클래스에는 종속성이 있습니다. 아마도 &lt;code&gt;BarcodeCameraFinder&lt;/code&gt;, &lt;code&gt;DefaultPhysicsEngine&lt;/code&gt; 및 &lt;code&gt;HttpStreamer&lt;/code&gt;일 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대조적으로, 모든 응용 프로그램에서 최악의 클래스는 많은 작업을 수행하지 않고 공간을 차지하는 클래스입니다: &lt;code&gt;BarcodeDecoderFactory&lt;/code&gt;, &lt;code&gt;CameraServiceLoader&lt;/code&gt; 및 &lt;code&gt;MutableContextWrapper&lt;/code&gt;. 이 클래스는 흥미로운 것들을 함께 연결하는 서투른 덕트 테이프입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 보일러 플레이트를 작성하는 부담 없이 종속성 주입 디자인 패턴을 구현하는 이러한 FactoryFactory 클래스를 대체합니다. 그래서 개발자가 중요한 일에만 집중할 수 있습니다. 종속성을 선언하고 이를 충족하는 방법을 지정하고 앱을 출시하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 javax.inject 주석(JSR 330)을 기반으로 하여 각 클래스를 쉽게 테스트할 수 있습니다. RpcCreditCardService를 FakeCreditCardService로 교체하기 위해 많은 상용구가 필요하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 주입은 테스트만을 위한 것이 아닙니다. 또한 재사용 가능하고 교체 가능한 모듈을 쉽게 생성할 수 있습니다. 모든 앱에서 동일한 AuthenticationModule을 공유할 수 있습니다. 또한 개발 중에는 DevLoggingModule을 실행하고 프로덕션에서는 ProdLoggingModule을 실행하여 각 상황에서 올바른 동작을 얻을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Why Dagger 2 is Different&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종속성 주입 프레임워크는 구성 및 주입을 위한 다양한 API와 함께 수년 동안 존재해 왔습니다. 그렇다면 바퀴를 재발명하는 이유는 무엇입니까? Dagger 2는 &lt;b&gt;생성된 코드로 전체 스택을 구현한 최초의 제품&lt;/b&gt;입니다. 기본 원칙은 종속성 주입이 가능한 한 간단하고 추적 가능하며 성능이 좋은지 확인하기 위해 사용자가 손으로 작성한 코드를 모방하는 코드를 생성하는 것입니다. 디자인에 대한 자세한 내용은 Gregory Kick의 이 강연(슬라이드)을 시청하십시오.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Using Dagger&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 커피 메이커를 구축하여 의존성 주입과 Dagger를 시연할 것입니다. 컴파일하고 실행할 수 있는 전체 샘플 코드는 Dagger의 커피 예제를 참조하세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Declaring Dependencies&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 애플리케이션 클래스의 인스턴스를 구성하고 해당 종속성을 충족합니다. javax.inject.Inject 주석을 사용하여 관심 있는 생성자와 필드를 식별합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Inject를 사용하여 Dagger가 클래스의 인스턴스를 생성하는 데 사용해야 하는 생성자에 주석을 달 수 있습니다. 새 인스턴스가 요청되면 Dagger는 필수 매개변수 값을 얻고 이 생성자를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject // 생성자애 inject 주석
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 필드를 직접 주입할 수 있습니다. 이 예에서는 히터 필드에 대한 Heater 인스턴스와 펌프 필드에 대한 Pump 인스턴스를 얻습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스에 @Inject 주석 필드가 있지만 @Inject 주석 생성자가 없는 경우 Dagger는 요청 시 해당 필드를 삽입하지만 새 인스턴스를 생성하지는 않습니다. Dagger가 인스턴스도 생성할 수 있음을 나타내기 위해 @Inject 주석과 함께 인수가 없는 생성자를 추가하십시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 메서드 주입도 지원하지만 일반적으로 생성자 또는 필드 주입이 선호됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Inject 주석이 없는 클래스는 Dagger로 생성할 수 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 충족&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Dagger는 위에서 설명한 대로 요청된 유형의 인스턴스를 구성하여 각 종속성을 충족합니다. CoffeeMaker를 요청하면 new CoffeeMaker()를 호출하고 주입 가능한 필드를 설정하여 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 @Inject는 모든 곳에서 작동하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스를 구성할 수 없습니다.&lt;/li&gt;
&lt;li&gt;제3자(third party) 클래스는 주석을 달 수 없습니다.&lt;/li&gt;
&lt;li&gt;구성 가능한 개체를 구성해야 합니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Inject가 충분하지 않거나 어색한 경우 @Provides-annotated 메서드를 사용하여 종속성을 충족합니다. 메서드의 반환 유형은 충족하는 종속성을 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 히터가 필요할 때마다 providerHeater()가 호출됩니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Provides static Heater provideHeater() {
  return new ElectricHeater();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Provides 메소드가 자체적으로 종속성을 가질 수도 있습니다. 예를 들어 ElectricHeater에는 @Inject 생성자가 있으므로 위의 메서드를 다음과 같이 대신 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Provides static Heater provideHeater(ElectricHeater heater) {
  return heater;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 Dagger는 ElectricHeater 인스턴스화를 처리하고 @Provides 메서드는 Heater 유형에 별칭을 지정하는 데만 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특별한 경우에는 &lt;code&gt;@Binds&lt;/code&gt; 메서드를 사용하여 별칭을 정의하여 더 단순화할 수 있습니다. &lt;code&gt;@Provides&lt;/code&gt;와 달리 &lt;code&gt;@Binds&lt;/code&gt; 메서드는 추상적이고 &lt;b&gt;구현이 없습&lt;/b&gt;니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ElectricHeater 에는 @Inject 생성자가 있으므로 bind를 사용하여 아래처럼 표현이 가능함.)&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Binds Heater bindHeater(ElectricHeater impl);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: @Binds를 사용하는 것은 별칭(alias)을 정의하는 데 선호되는 방법입니다. Dagger는 컴파일 시간에만 모듈이 필요하고 런타임에 모듈을 로드하는 클래스를 피할 수 있기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(울 회사에서 쓰는 DI 중에 viewModel 을 DI 하는 경우 직접 뷰모델 팩토리를 주입받고,&lt;br /&gt;거기에 더해 Provides 에서 각 (연결된)액티비티와, 뷰모델 팩토리를 파라미터로 갖는 Provider로 &lt;code&gt;ViewModelProvider(activity, viewModelFactory).get()&lt;/code&gt; 으로 뷰모델을 주입하여 viewModel 필드에 바로 Injection 이 가능하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://trend21c.tistory.com/2111&quot;&gt;dagger2 android viewmodel 과 사용하기&lt;/a&gt;&lt;br /&gt;[&lt;a href=&quot;https://www.charlezz.com/?p=1315%5D&quot;&gt;https://www.charlezz.com/?p=1315]&lt;/a&gt;(Dagger2를 알아보자 &amp;ndash; Multibinding)&lt;br /&gt;&lt;a href=&quot;https://kotlinworld.com/106&quot;&gt;DAGGER2 @Binds&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Module
interface HeaterModule {
  @Binds Heater bindHeater(ElectricHeater impl);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관례에 따라 @Provides 메서드는 제공 접두사로 이름이 지정되고 @Binds 메서드는 bind 접두사로 이름이 지정되며 모듈 클래스는 Module 접미사로 이름이 지정됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Building the Graph&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Inject 및 @Provides-annotated 클래스는 종속성으로 연결된 개체의 그래프를 형성합니다. 애플리케이션의 기본 메서드나 Android 애플리케이션과 같은 코드를 호출하면 잘 정의된 루트 집합을 통해 해당 그래프에 액세스합니다. Dagger 2에서 해당 집합은 인수가 없고 원하는 유형을 반환하는 메서드가 있는 인터페이스로 정의됩니다. 이러한 인터페이스에 @Component 주석을 적용하고 모듈 유형을 모듈 매개변수에 전달함으로써 Dagger 2는 해당 계약의 구현을 완전히 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 Dagger 접두사가 붙은 인터페이스와 동일한 이름을 갖습니다. 해당 구현에서 builder() 메서드를 호출하여 인스턴스를 얻고 반환된 빌더를 사용하여 종속성을 설정하고 새 인스턴스를 build()합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: @Component가 최상위 유형이 아닌 경우 생성된 구성 요소의 이름에는 밑줄로 결합된 포함하는 유형의 이름이 포함됩니다. 예를 들어 이 코드는 다음과 같습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 DaggerFoo_Bar_BazComponent라는 컴포넌트를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액세스 가능한 기본 생성자가 있는 모든 모듈은 설정되지 않은 경우 빌더가 인스턴스를 자동으로 생성하므로 생략할 수 있습니다. 그리고 @Provides 메소드가 모두 정적인 모듈의 경우 구현에 인스턴스가 전혀 필요하지 않습니다. 사용자가 종속성 인스턴스를 생성하지 않고 모든 종속성을 구성할 수 있는 경우 생성된 구현에는 빌더를 처리할 필요 없이 새 인스턴스를 가져오는 데 사용할 수 있는 create() 메서드도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;CoffeeShop coffeeShop = DaggerCoffeeShop.create();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CoffeeApp은 Dagger가 생성한 CoffeeShop 구현을 사용하여 완전히 주입된 CoffeeMaker를 얻을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 그래프가 구성되고 진입점이 주입되었으므로 커피 메이커 앱을 실행합니다. 재미있군요.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=&amp;gt; =&amp;gt; pumping =&amp;gt; =&amp;gt;
 [_]P coffee! [_]P&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bindings in the graph&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예는 좀 더 일반적인 바인딩으로 구성 요소를 구성하는 방법을 보여주지만 그래프에 바인딩을 제공하는 다양한 메커니즘이 있습니다. 다음 목록을 종속성으로 사용할 수 있으며 잘 구성된 구성 요소를 생성하는 데 사용할 수 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Component.modules에 의해 직접 참조되거나 @Module.includes를 통해 전이적으로 참조되는 @Module 내 @Provides 메소드에 의해 선언된 것들&lt;/li&gt;
&lt;li&gt;범위가 지정되지 않았거나 구성 요소의 범위 중 하나와 일치하는 @Scope 주석이 있는 @Inject 생성자가 있는 모든 유형&lt;/li&gt;
&lt;li&gt;구성 요소 종속성(component dependencies)의 구성 요소 제공 메서드(component provision methods)&lt;/li&gt;
&lt;li&gt;구성 요소(component) 자체&lt;/li&gt;
&lt;li&gt;포함된 하위 구성 요소에 대한 자격이 없는 빌더&lt;/li&gt;
&lt;li&gt;위의 바인딩에 대한 공급자 또는 지연 래퍼&lt;/li&gt;
&lt;li&gt;위 바인딩의 Lazy 제공자(예: Provider&amp;lt;Lazy&amp;gt;)&lt;/li&gt;
&lt;li&gt;모든 유형에 대한 MembersInjector&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Singletons and Scoped Bindings&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Provides 메서드 또는 주입 가능한 클래스에 @Singleton으로 주석을 추가합니다. 그래프는 모든 클라이언트에 대해 값의 단일 인스턴스를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Provides @Singleton static Heater provideHeater() {
  return new ElectricHeater();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입 가능한 클래스의 @Singleton 주석도 문서 역할을 합니다. 잠재적인 유지 관리자에게 이 클래스가 여러 스레드에서 공유될 수 있음을 상기시킵니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Singleton
class CoffeeMaker {
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger 2는 그래프의 범위가 지정된 인스턴스를 &lt;code&gt;Component&lt;/code&gt; 구현의 인스턴스와 연결하므로 &lt;code&gt;Component&lt;/code&gt; 자체에서 표현하려는 범위를 선언해야 합니다. 예를 들어, @Singleton 바인딩과 @RequestScoped 바인딩을 동일한 &lt;code&gt;Component&lt;/code&gt;에 갖는 것은 의미가 없습니다. 이러한 범위는 수명 주기가 다르고 수명 주기가 다른 &lt;code&gt;Component&lt;/code&gt;에 있어야 하기 때문입니다. &lt;code&gt;Component&lt;/code&gt;가 주어진 범위와 연결되어 있음을 선언하려면 &lt;code&gt;Component&lt;/code&gt; 인터페이스에 범위 주석을 적용하기만 하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Component&lt;/code&gt;에는 여러 범위 주석이 적용될 수 있습니다. 이것은 그것들이 모두 동일한 범위에 대한 별칭임을 선언하므로 &lt;code&gt;Component&lt;/code&gt;는 선언하는 모든 범위와 범위가 지정된 바인딩을 포함할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reusable scope&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 @Inject 생성 클래스가 인스턴스화되거나 @Provides 메서드가 호출되는 횟수를 제한하고 싶지만 특정 구성 요소 또는 하위 구성 요소의 수명 동안 정확히 동일한 인스턴스가 사용된다고 보장할 필요는 없습니다. 이는 할당 비용이 많이 들 수 있는 Android와 같은 환경에서 유용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 바인딩의 경우 @Reusable 범위를 적용할 수 있습니다. @Reusable 범위 바인딩은 다른 범위와 달리 단일 &lt;code&gt;Component&lt;/code&gt;와 연결되지 않습니다. 대신 실제로 바인딩을 사용하는 각 &lt;code&gt;Component&lt;/code&gt;는 반환되거나 인스턴스화된 개체를 캐시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;Component&lt;/code&gt;에 @Reusable 바인딩이 있는 모듈을 설치했지만 하위 &lt;code&gt;Component&lt;/code&gt;만 실제로 바인딩을 사용하는 경우 해당 하위 &lt;code&gt;Component&lt;/code&gt;만 바인딩의 개체를 캐시합니다. 조상을 공유하지 않는 두 개의 하위 &lt;code&gt;Component&lt;/code&gt; 각각 바인딩을 사용하는 경우 각 &lt;code&gt;Component&lt;/code&gt;는 자체 개체를 캐시합니다. &lt;code&gt;Component&lt;/code&gt;의 조상이 이미 객체를 캐시했다면 하위 &lt;code&gt;Component&lt;/code&gt;는 이를 재사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Component&lt;/code&gt;가 바인딩을 한 번만 호출한다는 보장이 없으므로 변경 가능한 개체 또는 동일한 인스턴스를 참조하는 것이 중요한 개체를 반환하는 바인딩에 &lt;code&gt;@Reusable&lt;/code&gt;을 적용하는 것은 위험합니다. 할당된 횟수에 신경 쓰지 않는다면 범위를 지정하지 않은 불변 객체에 &lt;code&gt;@Reusable&lt;/code&gt;을 사용하는 것이 안전합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Reusable // It doesn't matter how many scoopers we use, but don't waste them.
class CoffeeScooper {
  @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
  @Provides
  @Reusable // DON'T DO THIS! You do care which register you put your cash in.
            // Use a specific scope instead.
  static CashRegister badIdeaCashRegister() {
    return new CashRegister();
  }
}

@Reusable // DON'T DO THIS! You really do want a new filter each time, so this
          // should be unscoped.
class CoffeeFilter {
  @Inject CoffeeFilter() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lazy injections&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 느리게 인스턴스화할 개체가 필요합니다. 모든 바인딩 T에 대해 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;의 get() 메서드에 대한 첫 번째 호출까지 인스턴스화를 연기하는 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;를 만들 수 있습니다. T가 싱글톤이면 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;는 ObjectGraph 내의 모든 주입에 대해 동일한 인스턴스가 됩니다. 그렇지 않으면 각 주입 사이트에 자체 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt; 인스턴스가 생깁니다. 그럼에도 불구하고 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;의 지정된 인스턴스에 대한 후속 호출은 T의 동일한 기본 인스턴스를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;class GrindingCoffeeMaker {
  @Inject Lazy&amp;lt;Grinder&amp;gt; lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Provider injections&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 단일 값을 주입하는 대신 여러 인스턴스를 반환해야 합니다. 여러 옵션(Factories, Builders 등)이 있지만 한 가지 옵션은 T 대신 &lt;code&gt;Provider&amp;lt;T&amp;gt;&lt;/code&gt;를 삽입하는 것입니다. &lt;code&gt;Provider&amp;lt;T&amp;gt;&lt;/code&gt;는 .get()이 호출될 때마다 T에 대한 바인딩 논리를 호출합니다. 해당 바인딩 논리가 &lt;code&gt;@Inject&lt;/code&gt; 생성자이면 새 인스턴스가 생성되지만 &lt;code&gt;@Provides&lt;/code&gt; 메서드에는 그러한 보장이 없습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class BigCoffeeMaker {
  @Inject Provider&amp;lt;Filter&amp;gt; filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p &amp;lt; numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //new filter every time.
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Qualifiers&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때때로 type만으로는 종속성을 식별하는 데 충분하지 않습니다. 예를 들어, 정교한 커피 메이커 앱은 물과 핫 플레이트를 위한 별도의 히터를 원할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 한정자 주석을 추가합니다. 이것은 자체적으로 @Qualifier 주석이 있는 모든 주석입니다. 다음은 javax.inject에 포함된 한정자 주석인 @Named의 선언입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고유한 한정자 주석을 만들거나 @Named를 사용할 수 있습니다. 관심 있는 필드 또는 매개변수에 주석을 달아 한정자를 적용합니다. 유형 및 한정자 주석은 모두 종속성을 식별하는 데 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;class ExpensiveCoffeeMaker {
  @Inject @Named(&quot;water&quot;) Heater waterHeater;
  @Inject @Named(&quot;hot plate&quot;) Heater hotPlateHeater;
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 @Provides 메서드에 주석을 달아 정규화된 값을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Provides @Named(&quot;hot plate&quot;) static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named(&quot;water&quot;) static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종속성에 여러 한정자 주석이 없을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Optional bindings&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;component&lt;/code&gt;에 일부 종속성이 바인딩되지 않은 경우에도 바인딩이 작동하도록 하려면 &lt;code&gt;@BindsOptionalOf&lt;/code&gt; 메서드를 모듈에 추가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@BindsOptionalOf abstract CoffeeCozy optionalCozy();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, @Inject 생성자 및 멤버 및 @Provides 메서드는 &lt;code&gt;Optional&amp;lt;CoffeeCozy&amp;gt;&lt;/code&gt; 개체에 종속될 수 있습니다. &lt;code&gt;component&lt;/code&gt;에 CoffeeCozy에 대한 바인딩이 있는 경우 Optional이 표시됩니다. CoffeeCozy에 대한 바인딩이 없으면 Optional이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 중 하나를 주입할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Optional (CoffeeCozy에 대한 @Nullable 바인딩이 없는 한; 아래 참조)&lt;/li&gt;
&lt;li&gt;Optional&amp;lt;Provider&amp;gt;&lt;/li&gt;
&lt;li&gt;Optional&amp;lt;Lazy&amp;gt;&lt;/li&gt;
&lt;li&gt;Optional&amp;lt;Provider&amp;lt;Lazy&amp;gt;&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 중 Provider나 Lazy 또는 Provider of Lazy를 주입할 수도 있지만 그다지 유용하지는 않습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoffeeCozy에 대한 바인딩이 있고 해당 바인딩이 @Nullable이면 Optional에 null을 포함할 수 없기 때문에 Optional를 삽입하는 것은 컴파일 타임 오류입니다. Provider와 Lazy는 항상 get() 메서드에서 null을 반환할 수 있기 때문에 항상 다른 형식을 삽입할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 &lt;code&gt;component&lt;/code&gt;에 없는 선택적 바인딩은 &lt;code&gt;subcomponent&lt;/code&gt;에 underlying type에 대한 바인딩이 포함되어 있는 경우 &lt;code&gt;subcomponent&lt;/code&gt;에 존재할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Guava의 Optional 또는 Java 8의 Optional을 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Binding Instances&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;component&lt;/code&gt;를 빌드할 때 사용할 수 있는 데이터가 있는 경우가 많습니다. 예를 들어 명령줄 인수를 사용하는 응용 프로그램이 있다고 가정합니다. &lt;code&gt;component&lt;/code&gt;에서 해당 인수를 바인딩할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마도 앱은 @UserName 문자열로 삽입하려는 사용자 이름을 나타내는 단일 인수를 취합니다. @BindsInstance 주석이 달린 메서드를 구성 요소 빌더에 추가하여 해당 인스턴스가 &lt;code&gt;component&lt;/code&gt;에 주입되도록 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component(modules = AppModule.class)
interface AppComponent {
  App app();

  @Component.Builder
  interface Builder {
    @BindsInstance Builder userName(@UserName String userName);
    AppComponent build();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 앱은 다음과 같이 보일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;public static void main(String[] args) {
  if (args.length &amp;gt; 1) { exit(1); }
  App app = DaggerAppComponent
      .builder()
      .userName(args[0])
      .build()
      .app();
  app.run();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예에서 구성 요소에 @UserName 문자열을 삽입하면 이 메서드를 호출할 때 빌더에 제공된 인스턴스가 사용됩니다. 구성 요소를 빌드하기 전에 모든 @BindsInstance 메서드를 호출하여 null이 아닌 값을 전달해야 합니다(아래 @Nullable 바인딩 제외).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@BindsInstance 메서드에 대한 매개변수가 @Nullable로 표시되면 @Provides 메서드가 nullable인 것과 같은 방식으로 바인딩이 &quot;nullable&quot;로 간주됩니다. 주입 사이트도 @Nullable로 표시해야 하며 null은 허용되는 값입니다. 바인딩. 또한 Builder 사용자는 메서드 호출을 생략할 수 있으며 구성 요소는 인스턴스를 null로 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@BindsInstance 메소드는 생성자 인수가 있는 @Module을 작성하고 해당 값을 즉시 제공하는 것보다 선호되어야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compile-time Validation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger 주석 프로세서는 엄격하며 바인딩이 유효하지 않거나 불완전하면 컴파일러 오류가 발생합니다. 예를 들어, 이 모듈은 Executor에 대한 바인딩이 없는 구성 요소에 설치됩니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger 주석 프로세서는 엄격하며 바인딩이 유효하지 않거나 불완전하면 컴파일러 오류가 발생합니다. 예를 들어, 이 모듈은 Executor에 대한 바인딩이 없는 &lt;code&gt;component&lt;/code&gt;에 설치됩니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;component&lt;/code&gt;의 모듈에 Executor용 @Provides-annotated 메소드를 추가하여 문제를 수정하십시오. @Inject, @Module 및 @Provides 주석은 개별적으로 유효성이 검사되지만 바인딩 간의 관계에 대한 모든 유효성 검사는 @Component 수준에서 발생합니다. Dagger 1은 @Module 수준 유효성 검사(런타임 동작을 반영하거나 반영하지 않을 수 있음)에 엄격하게 의존했지만 Dagger 2는 전체 그래프 유효성 검사를 위해 이러한 유효성 검사(및 @Module에 대한 동반 구성 매개변수)를 생략합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Compile-time Code Generation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger의 주석 처리기는 CoffeeMaker_Factory.java 또는 CoffeeMaker_MembersInjector.java와 같은 이름을 가진 소스 파일을 생성할 수도 있습니다. 이 파일은 Dagger 구현 세부 정보입니다. 직접 사용할 필요는 없지만 주입을 통해 단계적으로 디버깅할 때 편리할 수 있습니다. 코드에서 참조해야 하는 생성된 유형은 &lt;code&gt;component&lt;/code&gt;에 대해 Dagger가 접두사로 붙은 유형뿐입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Using Dagger In Your Build&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 런타임에 dagger-2.X.jar를 포함해야 합니다. 코드 생성을 활성화하려면 컴파일 시 빌드에 dagger-compiler-2.X.jar를 포함해야 합니다. 자세한 내용은 README를 참조하십시오.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;License&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Copyright 2012 The Dagger Authors

Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글 에서는 안드로이드 에서 Dagger2 를 사용하는 방법에 대해서 알아봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dagger.dev/dev-guide/&quot;&gt;https://dagger.dev/dev-guide/&lt;/a&gt;&lt;/p&gt;</description>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/638</guid>
      <comments>https://whyprogrammer.tistory.com/638#entry638comment</comments>
      <pubDate>Sun, 17 Apr 2022 23:06:21 +0900</pubDate>
    </item>
    <item>
      <title>Hello Dagger</title>
      <link>https://whyprogrammer.tistory.com/637</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hello Dagger&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 Java, Kotlin 및 Android를 위한 완전히 정적 컴파일 타임 종속성 주입 프레임워크입니다. Square에서 만들고 현재 Google에서 유지 관리하는 이전 버전을 개조한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The latest Dagger release is: Dagger 2.41 (2022-04-16 기점)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger는 리플렉션 기반 솔루션을 괴롭히는 많은 개발 및 성능 문제를 해결하는 것을 목표로 합니다. 자세한 내용은 Gregory Kick의 이 강연(슬라이드)에서 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>Android/Dagger2</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/637</guid>
      <comments>https://whyprogrammer.tistory.com/637#entry637comment</comments>
      <pubDate>Sun, 17 Apr 2022 22:05:07 +0900</pubDate>
    </item>
    <item>
      <title>[Android] RecyclerView + GridLayoutManager 으로 해상도별 item 뿌려주기</title>
      <link>https://whyprogrammer.tistory.com/636</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 안드로이드 리사이클러뷰와 그리드 레이아웃 매니저를 이용한 해상도별 item 그리기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하며 마주친 문제인데, 리사이클러 뷰 에서 여러 종류의 아이템(헤더, 아이템카드, 푸터)을 뿌려주고 있는 상황이며, 가이드로 나온 디자인은 특정 해상도에서 아이템 뷰를 다르게 보여주기를 원하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 디자인 가이드 예시 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2587&quot; data-origin-height=&quot;2341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lxOwK/btrqxTosyGm/VV73pCxo0SW9Du0dqiFhgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lxOwK/btrqxTosyGm/VV73pCxo0SW9Du0dqiFhgk/img.png&quot; data-alt=&quot;해상도별 디자인 가이드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lxOwK/btrqxTosyGm/VV73pCxo0SW9Du0dqiFhgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlxOwK%2FbtrqxTosyGm%2FVV73pCxo0SW9Du0dqiFhgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2587&quot; height=&quot;2341&quot; data-origin-width=&quot;2587&quot; data-origin-height=&quot;2341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해상도별 디자인 가이드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러 내부에 들어가는 아이템이 한 가지 종류가 아니라 위 디자인 가이드를 보시면 알겠지만 최 상단에는 타이틀 헤더(Header)가 최 하단에는 푸터(Footer)가 붙어있는 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 사용하고 있었던 &lt;code&gt;LinearLayoutManager&lt;/code&gt; 를 걷어내고 &lt;code&gt;GridLayoutManager&lt;/code&gt; 를 사용해야 함은 얼추 알고는 있었는데, 아이템 타입이 여러개 일 때는 어떻게 적용을 할지 몰라서 막막했었죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. Header 와 Footer 를 한 행으로 처리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 찾아본 것은 헤더와 푸터의 경우 해상도가 큰 화면에서 2열로 나뉘어 보여질 때도 한 행에서 보여지게 해야하는 문제였죠. 이 부분은 구글링을 통해서 쉽게 해답을 찾을 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;val mLayoutManager = GridLayoutManager(context, 2)
mLayoutManager.spanSizeLookup = object : SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return when (adapter.getItem(position)) {
            is HeaderItem, is FooterItem -&amp;gt; 2
            else -&amp;gt; 1
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GridLayoutManager&lt;/code&gt; 의 &lt;code&gt;spanSizeLookup&lt;/code&gt; 을 새로 확장하여 &lt;code&gt;getSpanSize&lt;/code&gt; 를 재정의해주는 방법이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러 뷰에 위치하는 각각 아이템의 타입을 가져와서 헤더와 푸터인 경우에는 spanSize 를 2로 반환하게 끔하여 두 개의 열을 다 차지하도록 해주었습니다. 그러면 실제 리사이클러 뷰에서 헤더와 푸터는 한 행에서 나타나게 되는 것이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 특정 해상도를 넘어가면 2개 열로 나누어 아이템을 보여주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 특정 해상도를 넘길 경우 2개 열로 나누어 아이템을 보여주는 부분 이었습니다. 이 부분은 사실 &lt;code&gt;GridLayoutManager&lt;/code&gt;를 인스턴스화 할 때 디바이스 너비를 얻어와 설정해 주면 되는 부분이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.1 에서 본 코드에서 특정 해상도 이상일 때만 &lt;code&gt;GridLayoutManager(context, 2)&lt;/code&gt; 으로 인스턴스화 하고 아닐 경우에는 &lt;code&gt;GridLayoutManager(context, 1)&lt;/code&gt; 또는 &lt;code&gt;LinearLayoutManager&lt;/code&gt; 를 사용하는 방법도 있겠죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 특정 해상도를 넘어가기 전에는 match_parent, 그 이후에는 가운데로 몰아서 보여주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 구글링도 많이하고 시간을 많이 할해한 부분은 특정 해상도 이상일 때 아이템 카드의 정렬과 너비 설정 부분이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 처음에는 &lt;a href=&quot;https://stackoverflow.com/q/70675860/12849780&quot;&gt;스택오버플로우&lt;/a&gt;에 질문도 남겼습니다. (현재는 자문자답 한 상태입니다  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 &lt;code&gt;RecyclerView.ItemDecoration&lt;/code&gt; 을 사용하는 방법입니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class ItemDecoration(context: Context, private val displayWidthPx: Int) :
    RecyclerView.ItemDecoration() {
    private val horizontalMargin =
        context.resources.getDimensionPixelSize(R.dimen.span_middle_margin)
    private val maxItemSize =
        context.resources.getDimensionPixelSize(R.dimen.item_max_width)

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        val spanIndex = (view.layoutParams as GridLayoutManager.LayoutParams).spanIndex

        outRect.run {
            if (parent.getChildViewHolder(view) is MyViewHolder) {
                when (displayWidthPx.pxToDp) {
                    in 1..767 -&amp;gt; Unit
                    else -&amp;gt; {
                        val displayStartEndMargin =
                            (displayWidthPx - (maxItemSize * 2) - (horizontalMargin * 2)) / 2
                        if (spanIndex % 2 == 0) {
                            right = horizontalMargin
                            left = displayStartEndMargin
                        } else {
                            left = horizontalMargin
                            right = displayStartEndMargin
                        }
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;code&gt;RecyclerView.ItemDecoration&lt;/code&gt; 를 확장하는 커스텀 데코레이션 클래스를 정의해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 살펴보면 &lt;code&gt;horizontalMargin&lt;/code&gt; 은 가운데로 아이템 카드를 몰아보여줄 때 각 1열 2열 아이템 사이에 마진을 위한 값입니다. &lt;code&gt;maxItemSize&lt;/code&gt; 는 해상도가 큰 화면에서 보여줄 아이템 카드의 최대 너비가 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;ItemDecoration&lt;/code&gt; 클래스 생성시 &lt;code&gt;context&lt;/code&gt; 와 더불어 &lt;code&gt;displayWidthPx&lt;/code&gt; 도 받도록 하였는데요? &lt;code&gt;displayWidthPx&lt;/code&gt; 는 유저가 사용하는 디바이스의 디스플레이 실제 너비이어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들은 모두 &lt;code&gt;getItemOffsets&lt;/code&gt; 에서 사용되어야 하고 &lt;code&gt;getItemOffsets&lt;/code&gt; 에서는 픽셀값을 사용하기 때문에 픽셀값을 받아왔습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;outRect&lt;/code&gt; 의 크기를 설정하여 그려질 영역을 결정하기 때문에 해당 객체에서 run 으로 적용해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;parent.getChildViewHolder(view) is MyViewHolder&lt;/code&gt; 으로 현재 &lt;code&gt;view&lt;/code&gt; 가 어떤 아이템 타입의 뷰홀더인지 파악하고 헤더와 푸터가 아닌 아이템 카드인 경우에만 적용하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 유저 디바이스가 특정 해상도를 넘어가는 경우에만 마진값을 재 계산하여 화면에 그려질 위치를 정해주었습니다.&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 원하는 리사이클러 뷰의 아이템 디스플레이 방식대로 구현을 할 수 있게 되었습니다. 처음에는 정말 이게 가능한가? 가능한지 팀원분께 질문을 해볼까? 라는 생각도 들었지만 (스택오버플로우에 질문을 올리기도 했지만 빠른 답변이 없없다..) 불가능 해보이진 않아서 계속 문제해결을 위한 답을 찾았고 결국 해결했다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞는 코드인지, 좀 더 효율적인 코드가 있는지 모르겠지만 그래도 뿌듯하다. 구글링 해봤을 때 관련된 티스토리나 국내 포스터가 없어서 혹시나 다른 분들에게 도움이 될까 해서 포스팅을 남기게 되었는데, 혹시나 다른 의견이나 궁금한 점이 있으시다면 덧글 남겨주시면 확인해 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움 되셨다면 공감 부탁 드려요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/635&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.01.02 - [Android/Kotlin] - [코틀린] Android KTX 을 탐구해보자  &lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/634&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.01.01 - [Android/Kotlin] - [코틀린] 더 좋은 Companion object 사용 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/633&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.12.18 - [Android/Jetpack Compose] - [Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>Kotlin</category>
      <category>안드로이드</category>
      <category>코틀린</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/636</guid>
      <comments>https://whyprogrammer.tistory.com/636#entry636comment</comments>
      <pubDate>Wed, 12 Jan 2022 21:24:17 +0900</pubDate>
    </item>
    <item>
      <title>[코틀린] Android KTX 을 탐구해보자  </title>
      <link>https://whyprogrammer.tistory.com/635</link>
      <description>&lt;h1&gt;First-party tooling  &lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exploring Android KTX&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Android KTX는 Android 프레임워크 및 Jetpack을 위한 다양한 확장 세트입니다.&lt;/li&gt;
&lt;li&gt;그는 Android KTX 확장 자체가 Jetpack의 일부이며 간단한 Gradle 종속성으로 프로젝트에 추가할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Android KTX에서 제공하는 기능은 확장 기능 및 고차 기능과 같은 기능을 활용하여 Android API를 보다 Kotlin 관용적으로 만드는 것을 목표로 합니다.&lt;/li&gt;
&lt;li&gt;종속성을 보다 가볍게 만들기 위해 Android KTX는 제공하는 기능에 따라 여러 개의 작은 종속성으로 나뉩니다. 그 중 일부는 다음과 같습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Core KTX&lt;/li&gt;
&lt;li&gt;Fragment KTX&lt;/li&gt;
&lt;li&gt;SQLite KTX&lt;/li&gt;
&lt;li&gt;ViewModel KTX&lt;/li&gt;
&lt;li&gt;Navigation KTX&lt;/li&gt;
&lt;li&gt;WorkManager KTX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Android KTX 전체는 많은 유용한 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;다음 섹션에서는 Core KTX 및 Fragment KTX라는 두 가지 특정 모듈에서 제공하는 기능을 살펴보겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Adding Core KTX to your project&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Core KTX를 사용하도록 프로젝트를 업데이트합니다. 이렇게 하려면 먼저 google() Maven 저장소가 프로젝트에 추가되었는지 확인해야 합니다. 추가하려면 다음 코드를 사용하세요.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;repositories {
    google()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 (이전에 만들어둔 buildSrc 폴더 안에 참고)&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;object Android {
    object Tools {
        const val androidGradle = &quot;com.android.tools.build:gradle:3.5.0-beta04&quot;
    }

    object Ktx {
        const val core = &quot;androidx.core:core-ktx:1.0.1&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱수준 build.gradle.kts 에 추가&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;dependencies {
    ...

    implementation(Deps.Android.Ktx.core)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Core KTX가 프로젝트에 추가되었으므로 개발자의 삶을 더 쉽게 만드는 몇 가지 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Using Core&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core KTX에는 다음을 포함하여 다양한 핵심 Android 프레임워크 라이브러리 및 API를 중심으로 구축된 패키지가 포함되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;androidx.core.animation&lt;/li&gt;
&lt;li&gt;androidx.core.preference&lt;/li&gt;
&lt;li&gt;androidx.core.transition&lt;/li&gt;
&lt;li&gt;androidx.core.view&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android KTX를 사용하여 플랫폼 API를 단순화하는 가장 좋은 예 중 하나는 SharedPreferences를 사용하는 것입니다. Kotlin의 기능을 사용할 수 있으므로 명시적으로 commit() 또는 apply()를 호출할 필요가 없는 매우 유창한 구문을 SharedPreferences 편집에 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val preferences = getPreferences(Context.MODE_PRIVATE)
preferences.edit { 
    putBoolean(&quot;key&quot;, false) 
    putString(&quot;key2&quot;, &quot;value&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android KTX에서 제공하는 유용한 기능의 또 다른 예는 View.onPreDraw() 확장 기능입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;button.doOnPreDraw {
    // Perform an action when view is about to be drawn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 새 리스너를 만들거나 해당 리스너를 등록 취소하지 않고도 View가 그려지려고 할 때 실행될 로직이 포함된 람다를 정의할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Using Fragment KTX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프래그먼트 작업을 위한 유틸리티 기능이 포함된 Fragment KTX 모듈을 살펴보겠습니다. 먼저 새로운 종속성 상수를 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object Android {
    ...
    object Ktx {
        const val core = &quot;androidx.core:core-ktx:1.0.1&quot;
        const val fragment = &quot;androidx.fragment:fragment-ktx:1.0.0&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 app/build.gradle.kts를 업데이트하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;dependencies {
    ...

    implementation(Deps.Android.Ktx.core)
    implementation(Deps.Android.Ktx.fragment)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종속성이 추가되면 다음과 같이 commit() 확장 기능을 사용하여 FragmentTransactions를 정의할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;supportFragmentManager.commit {
    addToBackStack(&quot;fragment name&quot;)
    add(SampleFragment(), &quot;tag&quot;)
    setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 새로운 FragmentTransaction을 추가할 때 Kotlin이 더 관용적으로 느껴지고 이러한 작업과 관련된 일부 상용구가 제거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core KTX 및 Fragment KTX에서 보았듯이 이러한 라이브러리는 Android 개발 경험을 실제로 향상시킬 수 있습니다. 이러한 유형의 확장 및 추가 기능은 Android KTX 종속성 전체에서 사용할 수 있으며 Android 개발을 위해 Kotlin을 활용하는 데 실제로 도움이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 섹션에서는 Kotlin Android Extensions 플러그인과 이 플러그인이 Kotlin을 사용하여 Android 개발을 더 쉽게 만드는 추가 기능을 제공하는 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Using Kotlin Android Extensions&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin Android Extensions 플러그인은 Kotlin 및 Android 작업을 위한 추가 기능 세트를 제공합니다. 이에 대한 가장 큰 두 가지 예는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findViewById() 없이 Android 보기 참조&lt;/li&gt;
&lt;li&gt;Parcelable 구현 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기능을 사용 설정하려면 app/build.gradle.kts 파일의 androidExtensions 블록 내에서 실험 기능을 사용 설정해야 합니다. 다음 코드 스니펫은 이 작업을 수행하는 방법을 보여줍니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;androidExtensions {
 isExperimental = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구성이 build.gradle.kts 파일에 추가되면 Android Extensions 플러그인에 포함된 기능이 활성화됩니다. 다음 섹션에서는 Android view를 구축하는 데 도움이 되는 이러한 기능 중 하나를 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Binding views with Kotlin Android Extensions&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 기능이 켜지면 합성 뷰 바인딩을 참조하여 뷰에 액세스할 수 있습니다. 이것은 Android Extensions 플러그인이 우리를 위해 뷰 바인딩을 생성한다는 것을 의미합니다. MainActivity에서 다음 가져오기를 추가하여 activity_main.xml 파일에 정의된 버튼을 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import kotlinx.android.synthetic.main.activity_main.button&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져오기를 포함하고 나면 findViewById() 또는 다른 변수 선언을 호출하지 않고도 해당 뷰를 직접 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;button.apply {
    text = &quot;Hello Kotlin&quot;
    gravity = Gravity.START
    setTextColor(resources.getColor(R.color.colorAccent))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 보기 이름은 XML의 android:id 속성에 따라 지정됩니다. 이 경우 버튼의 ID가 '@+id/button'이므로 생성된 바인딩의 이름은 버튼으로 지정되었습니다. 그러나 다른 변수 이름을 사용하거나 다른 이름과 충돌이 있는 경우 다음 구문을 사용하여 import 문을 업데이트하고 대체 이름을 제공할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;import kotlinx.android.synthetic.main.activity_main.button as theButton&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져오기를 업데이트한 후 이제 Button이라는 이름으로 버튼을 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;theButton.apply {
    text = &quot;Hello Kotlin&quot;
    gravity = Gravity.START
    setTextColor(resources.getColor(R.color.colorAccent))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합성 뷰 바인딩은 액티비티 및 프래그먼트와 함께 사용할 때 캐싱을 처리하고 사용자 지정 뷰에서도 작동하도록 만들 수 있습니다. Gradle 파일에서 androidExtensions 블록을 업데이트하여 요구 사항에 따라 캐싱 전략을 제어할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;androidExtensions {
   // HASH_MAP, SPARSE_ARRAY, NONE
   defaultCacheImplementation = &quot;HASH_MAP&quot; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기 참조에 Kotlin Android Extensions를 사용하면 Butterknife 또는 여러 findViewById() 호출과 같은 타사 라이브러리를 피할 수 있습니다. 이러한 합성 바인딩(DataBinding, ViewBinding 또는 findViewById())을 사용해야 하는지 여부는 프로젝트와 기본 설정에 따라 크게 달라지며 프로젝트별로 평가해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Generating Parcelable implementations&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parcelable은 보다 성능이 뛰어난 직렬화 API 제공을 목표로 하는 Android 개발의 공통 인터페이스입니다. Parcelable 인터페이스의 구현을 생성하는 것은 단순하고 반복적인 많은 코드를 생성하는 지루하고 상용구로 가득 찬 작업이 될 수 있습니다. 고맙게도 Kotlin Android Extensions는 Parcelable 구현을 생성할 수 있는 주석을 제공합니다. 이 기능을 사용하려면 Parcelable을 구현하는 모든 클래스에 @Parcelize 주석을 추가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Parcelize
data class Person(val firstName: String, val lastName: String): Parcelable&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Parcelize 주석을 모델 개체에 추가하면 플러그인이 필요한 Parcelable 구현을 생성합니다. 이렇게 하면 해당 Parcelable 구현의 구현 및 유지 관리를 건너뛸 수 있습니다. 이렇게 하면 클래스에 필요한 코드의 양이 줄어들고 클래스가 수정될 때마다 Parcelable 구현을 업데이트할 필요가 없습니다. 플러그인은 코드가 컴파일될 때 이를 수행합니다. 플러그인은 코드가 컴파일될 때 이를 수행합니다. 이는 속성 수정에서 일반적으로 발생할 수 있는 오류로부터 보호하는 데 도움이 되지만 Parcelable 구현을 업데이트하는 것을 잊어버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin Android Extensions 플러그인을 사용하면 작성 및 유지 관리해야 하는 코드의 양을 줄이고 플러그인이 일반적인 상용구 코드를 생성하도록 할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Summary&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 장에서는 Kotlin과 Android 개발 간의 관계를 살펴보았습니다. 우리는 초창기부터 최근 Google의 Kotlin First 발표까지 Kotlin이 Android에 채택된 역사를 살펴보았습니다. Kotlin을 지원하여 처음부터 새로운 Android 프로젝트를 만드는 방법과 Kotlin 관용적 Android 코드를 작성하는 방법을 자세히 설명했습니다. 마지막으로 Android-KTX, Kotlin Android Extensions 및 Android 아키텍처 구성 요소와 같은 몇 가지 특정 Kotlin 라이브러리 및 도구를 조사했습니다. 우리는 이러한 도구가 Kotlin을 활용하여 Android 개발을 더 쉽고 즐겁게 만드는 방법을 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 장에서는 웹 개발을 위해 Android 외부에서 Kotlin을 사용하는 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Referencs&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images-na.ssl-images-amazon.com/images/I/51H+ZH1ZqXL._SX404_BO1,204,203,200_.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mastering kotlin&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/634&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022.01.01 - [Android/Kotlin] - [코틀린] 더 좋은 Companion object 사용 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/633&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.12.18 - [Android/Jetpack Compose] - [Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/632&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.11.11 - [SELECT *] - 갤럭시 워치 4 (워치 시리즈) 롤렉스 페이스 다운로드 (Rolex face)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Kotlin</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/635</guid>
      <comments>https://whyprogrammer.tistory.com/635#entry635comment</comments>
      <pubDate>Sun, 2 Jan 2022 20:40:00 +0900</pubDate>
    </item>
    <item>
      <title>[코틀린] 더 좋은 Companion object 사용 방법</title>
      <link>https://whyprogrammer.tistory.com/634</link>
      <description>&lt;h1&gt;Better companion&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;companion object&lt;/code&gt; 는 바깥 클래스(enclsosing class) 에서 메서드나 프로퍼티를 접근할 수 있게하는 효율적인 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약간 Java 의 Static 느낌이라고 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &lt;code&gt;companion object&lt;/code&gt; 를 자바에서 참조할 떄는 살짝 복잡할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 어떻게 효율적으로 사용할 수 있는지 알아봅시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 &lt;code&gt;companion object&lt;/code&gt; 가 동작할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 클래스에 컴패니언 객체가 선언되면 컴파일러는 컴파일 때 명명된 내부 클래스를 생성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 빈 컴패니언 객체를 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Widget {
    companion object { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 코드에 의해 Java 코드가 컴파일러에 의해 생성되면 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public final class Widget {
   public static final Widget.Companion Companion = new Widget.Companion((DefaultConstructorMarker)null);

   ...
   public static final class Companion {
      private Companion() {
      }

      ...
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 바깥 클래스의 인스턴스가 인스턴스화 될 때 마다 내부 클래스에 있는 인스턴스도 인스턴스화 되게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 java 에서 &lt;code&gt;companion&lt;/code&gt; 인스턴스를 통해서 해당 클래스의 속성과 메서드에 접근할 수 있게됩니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// Main.java
public class Main {
    public static void main(String[] args) {
        Widget.Companion.goo();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴패니언 오브젝트를 사용하면 자바의 static 과 매우 유사한 구문을 사용할 수 있게 되는듯 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이러한 companion object 는 실제 사용을 위해 인스턴스화 해야하는 실제 개체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에서 컴패니언 오브젝트를 좀 더 잘 사용할 수 있는 방법을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴패니언 오브젝트는 도대체 언제 사용하면 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린으로 개발을 하다보면 상수를 정의할 때 주로 컴패니언 오브젝트를 사용해오곤 했는데. 그럼 언제 컴패니언 오브젝트를 사용하는 것이 좋을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 세 가지를 예로 들 수 있을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;private 생성자와 함께 팩토리 메서드를 작성할 때&lt;/li&gt;
&lt;li&gt;static constants 와 메서드 제공&lt;/li&gt;
&lt;li&gt;최상위 프로퍼티 및 메서드 범위 지정(Scoping top-level properties and functions)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Companion object 는 바깥 클래스의 private 속성, 메서드 및 생성자에 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이는 클래스에 private 생성자가 있을 때도 해당 Companion object 에서 이를 사용해서 클래스를 인스턴스화 할 수 있다는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 팩토리 메서드를 작성해서 클래스 생성 방법을 제어 가능합니다. (실제로 팩토리 메서드 경우 이런식으로 정의하는 것을 많이 보셨을 겁니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 일반적으로 최상위 메서드와 프로퍼티를 선호하지만, 컴패니언 객체를 통해 이러한 프로퍼티와 메서드를 둘러싸는(scoping) 클래스로 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 네임스페이스 오염을 방지할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Companion object 의 naming  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴패니언 개체에 이름을 수정할 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 에서 Kotlin 의 컴패니언 오브젝트를 참조하는 경우에 Companion 이라는 이름으로 참조하는데 이 이름을 바꿔 사용이 가능하다는 것.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Widget {
    companion object Factory {
        fun create() {}
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 컴패니언 오브젝트 네이밍을 통해 코드 가시성을 더욱 높여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 이름을 지정하는 경우 생성된 클래스에 새로 지정된 이름이 지정되게 되고, 해당 이름을 사용해 참조할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// Main.java
public class Main {
    public static void main(String[] args) {
        Widget.Factory.create();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해서 컴패니언 오브젝트에 보다 의미있는 이름을 제공할 수 있고, 위에서 처럼 팩토리 메서드 예시와 같은 보다 좋은(?) 이름 사용이 가능하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 자바에서 사용될 때 Companion 이라는 코틀린 표식이 없어지므로 보다 자연스레 사용이 가능해집니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;companion object&lt;/code&gt; 에 네이밍이 가능하다니! 그렇게 큰 기능은 아니지만 재미있는 기능을 알게된 것 같아 기분이 좋네요. 앞으로 유의미한 네이밍을 통해 코드 가시성을 높일 수 있을 것 같네요. 여러분도 적용해보세요!  &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Referencs&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images-na.ssl-images-amazon.com/images/I/51H+ZH1ZqXL._SX404_BO1,204,203,200_.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mastering kotlin&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/633&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.12.18 - [Android/Jetpack Compose] - [Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/632&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.11.11 - [SELECT *] - 갤럭시 워치 4 (워치 시리즈) 롤렉스 페이스 다운로드 (Rolex face)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/631&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.10.14 - [유용한 정보] - 네이버 피싱 메일 주의 ⚠ 새로운 환경에서 접속이 시도되었습니다&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android/Kotlin</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/634</guid>
      <comments>https://whyprogrammer.tistory.com/634#entry634comment</comments>
      <pubDate>Sat, 1 Jan 2022 23:38:12 +0900</pubDate>
    </item>
    <item>
      <title>[Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈)</title>
      <link>https://whyprogrammer.tistory.com/633</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Google Developers Jetpack Compose Codelab 2021&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 GDG Korea Android 에서 진행했던 행사인 DevFest 2021 Android Compose 코드랩 행사를 참여한 후기를 기록한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jetpack Compose 는 네이티브 UI 를 빌드하기 위한 Android 의 최신 툴킷으로, UI 개발을 간소화하고 가속화할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 처럼 compose 는 즉 UI 를 기존 보다 쉽게 디자인, 개발, 빌드 할 수 있는 최신 라이브러인데, 기존 성능문제를 해결하면서도 쉬운 개발이 가능한.. 그런 멋진 녀석이다. (여기서는 보다 자세한 설명은 생략한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 이직하게된 회사에서 안드로이드 앱 개발 직무를 맡고 있기도하고 평소에 관심이 많은지라 이번 행사의 소식을 듣자마자 등록했다. (사실 수료하면 Jetpack 을 타고있는 안드로이드가 그려진 맨투맨을 소정의 상품으로 받을 수 있다고 해서 더더욱 지원했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리 회사 앱에도 조만간은 아니지만 언젠가는 레거시를 제거하고 Jetpack compose 를 적용할 가능성이 충분히 있기 때문에 (실제로 내년 적용을 시작하지 않을까 예상을 해본다..) 미리 공부해 두어도 나쁘지 않다고 판단했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 그렇게 시작된 광란의 코드랩&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxz71w/btroibRhrJ6/KBCLaknKhAtto6Yf15KIpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxz71w/btroibRhrJ6/KBCLaknKhAtto6Yf15KIpK/img.png&quot; data-alt=&quot;열혈 커밋의 흔적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxz71w/btroibRhrJ6/KBCLaknKhAtto6Yf15KIpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdxz71w%2FbtroibRhrJ6%2FKBCLaknKhAtto6Yf15KIpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;460&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;열혈 커밋의 흔적&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드랩 진행방식은 &lt;a href=&quot;https://github.com/gdgand/ComposeFest2021&quot; data-hovercard-type=&quot;repository&quot; data-hovercard-url=&quot;/gdgand/ComposeFest2021/hovercard&quot;&gt;gdgand/ComposeFest2021&lt;/a&gt; 저장소에서 포크해서 해당 포크 브랜치에서 커밋을 날려서 진행하면 GDG 측에서 수료기간내에 모든 코드랩을 진행했는지 판단해서 수료가 됬다고 판단되면 사은품을 보내주게 되는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부러 나중에 참고하려고 열혈 커밋을 하면서 매우 세세한 커밋을 기록하려고 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 저의 브런치가 궁금하시다면 아래를 참고  &amp;zwj; ️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/NetLSS&quot; data-hovercard-type=&quot;user&quot; data-hovercard-url=&quot;/users/NetLSS/hovercard&quot; data-octo-click=&quot;hovercard-link-click&quot; data-octo-dimensions=&quot;link_type:self&quot;&gt;NetLSS&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-darkreader-inline-color=&quot;&quot;&gt;/&lt;/span&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/NetLSS/ComposeFest2021&quot; data-pjax=&quot;#repo-content-pjax-container&quot;&gt;ComposeFest2021&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;889&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chBTt1/btrogvJlkSw/srzvVh4KpJHjolebAdnbk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chBTt1/btrogvJlkSw/srzvVh4KpJHjolebAdnbk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chBTt1/btrogvJlkSw/srzvVh4KpJHjolebAdnbk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchBTt1%2FbtrogvJlkSw%2FsrzvVh4KpJHjolebAdnbk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;889&quot; height=&quot;869&quot; data-origin-width=&quot;889&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드랩은 구글 에서 제공하고 있는 코드랩이며 코드랩 특성상 나와있는 설명을 보면서 이해하고 그대로 코드를 타이핑해서 결과물을 보면서 진행하면 되기 때문에 큰 어려움은 없을 것이다. (라고 생각했다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFLCER/btrob8hQqNy/VnM1egrcjkGHAXr3QJRQD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFLCER/btrob8hQqNy/VnM1egrcjkGHAXr3QJRQD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFLCER/btrob8hQqNy/VnM1egrcjkGHAXr3QJRQD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFLCER%2Fbtrob8hQqNy%2FVnM1egrcjkGHAXr3QJRQD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;695&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 매주마다 영상이 공개되면 해당 주에 어떤 코드랩을 완수하면 수료하는데 도움이된다는 가이드와 설명을 GDG 측에서 올려주었다. 사실 강의처럼 코드랩을 같이 진행하는 줄 알았는데 그건 아니였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;GDG 코드랩 안내 링크&quot; href=&quot;https://sites.google.com/view/devfest-korea-2021/compose-codelab&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GDG 코드랩 안내 링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 주마다 보통 1~2개의 코드랩을 가이드 해주었고 가이드 대로 코드랩을 진행해도 사실 벅찼다.. 왜냐면 일하면서 동시에 해야했기 때문에 퇴근하고 저녁먹고 바로 코드랩을 켜서 진행하기를 매일 반복해서 하고 정 안되면 주말에도 코드랩을 수행해야 어느정도 일정을 따라갈 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LKuQI/btrodSE46bu/ZzmDrvIRMfqaDUIVrBeEKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LKuQI/btrodSE46bu/ZzmDrvIRMfqaDUIVrBeEKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LKuQI/btrodSE46bu/ZzmDrvIRMfqaDUIVrBeEKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLKuQI%2FbtrodSE46bu%2FZzmDrvIRMfqaDUIVrBeEKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;334&quot; height=&quot;243&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 코드랩이 영문으로 되어있던게 시간이 오래걸린 이유였던 것 같다. 해석해보면 큰 어려움 없는 내용이지만 100%로 까지 해석이 되지 않아 구글 번역을 사용해서 코드랩을 진행했다. 그래서 예상 코드랩 시간보다 거의 +30~40% 정도 걸렸던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참가하신 분들이 1,000명 이상 됬던 것으로 알고있는데 이후에 알고보니 수료자는 30% 정도 뿐 이라고 한다. 아무래도 적지 않은 코드랩 내용 + 영문 이었어서 수료가 어렵지 않았다 하는 생각이든다. 물론 한달간 꾸준히 성실하게 할 수 있느냐가 관건이지만..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Khq6d/btrohSYCdC8/MnN92TNCu6fTWNcDV6iMX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Khq6d/btrohSYCdC8/MnN92TNCu6fTWNcDV6iMX0/img.png&quot; data-alt=&quot;수료하고 받은 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Khq6d/btrohSYCdC8/MnN92TNCu6fTWNcDV6iMX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKhq6d%2FbtrohSYCdC8%2FMnN92TNCu6fTWNcDV6iMX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;203&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수료하고 받은 메일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 나는 수료했다 기쁘다!  &amp;nbsp; GDG 소속이자 현재 우아한 형제들 소속 이시기도한 경완님께서 보내준 메일..! 사실 안드로이드 취업준비를 하면서 실력을 쌓을 때 활용했던 강의가 하나가 있는데 그 강의 강사가 경완님이어서 이런 행사에서 또 경완님을 간접적으로 보게되니 반가웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 수료 기념 사은품&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어..! 코드랩 행사에 지원한 동기가 되었던 사은품을 받았다. 직접 다 택배로 GDG 측에서 쏴주는 지라 (맨투맨은 구글에서 제공한 것으로 알고 있다.) 받는데는 몇 주 정도 걸렸던 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctD2em/btrohTiVcfW/v05aXFUxGBhQFJ6GvDU6Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctD2em/btrohTiVcfW/v05aXFUxGBhQFJ6GvDU6Sk/img.png&quot; data-alt=&quot;기모 맨투맨과 스티커&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctD2em/btrohTiVcfW/v05aXFUxGBhQFJ6GvDU6Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctD2em%2FbtrohTiVcfW%2Fv05aXFUxGBhQFJ6GvDU6Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;480&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기모 맨투맨과 스티커&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후드인줄 알았으나 맨투맨 이었던..! 그래도 아주 마음에 들었다. 게다가 기모여서 요즘 입기도 딱이어서 너무 좋았따. (한달간 거의 야근 수준의 코드랩을 진행하면서 고생했던 거에 대한 보상을 받은기분!  )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y9m8n/btrodlHvskx/fAEvJCKDtl1c0WXkOY0v21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y9m8n/btrodlHvskx/fAEvJCKDtl1c0WXkOY0v21/img.jpg&quot; data-alt=&quot;굿즈 스티커&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y9m8n/btrodlHvskx/fAEvJCKDtl1c0WXkOY0v21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy9m8n%2FbtrodlHvskx%2FfAEvJCKDtl1c0WXkOY0v21%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;480&quot; data-filename=&quot;다운로드 (1).jpeg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;굿즈 스티커&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 대망의 개발자 전용(?) 스티커..! 벌써 부터 어디에 붙여야할지 고민이 된다. 스티커도 나무 잘 나온것 같아서 아주 아주 마음에 든다..! 역시 이런 개발자 컨퍼런스의 꽃은 이런 굿즈가 아닐까 싶다.. 하나 하나 모아 노트북에 붙이면 간지 + 만족감 up&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 회사 노트북에 붙이기엔 아까워서 최대한 고민해보고 어디에 붙일지 결정할 생각이다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 굿즈 자랑을 끝으로 이 글을 마치려고 한다. 다음에도 이런 행사가 있으면 또 참여 및 공유 해보도록 하겠음!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드랩을 해보고 싶으신 분들은 아래에 첨부해둔 링크를 참고하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/courses/pathways/compose&quot;&gt;컴포즈 코드랩 링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래는 저의 브런치 인데, 상세한 커밋이 궁금하신 분들은 살짝 구경하셔도 좋음 (깃헙 맞팔 해요!!  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/NetLSS&quot; data-hovercard-type=&quot;user&quot; data-hovercard-url=&quot;/users/NetLSS/hovercard&quot; data-octo-click=&quot;hovercard-link-click&quot; data-octo-dimensions=&quot;link_type:self&quot;&gt;NetLSS&lt;/a&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-darkreader-inline-color=&quot;&quot;&gt;/&lt;/span&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/NetLSS/ComposeFest2021&quot; data-pjax=&quot;#repo-content-pjax-container&quot;&gt;ComposeFest2021&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1639831219747&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - NetLSS/ComposeFest2021: DevFest 2021 ComposeFest 코드랩 Repo 입니다&quot; data-og-description=&quot;DevFest 2021 ComposeFest 코드랩 Repo 입니다 . Contribute to NetLSS/ComposeFest2021 development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/NetLSS/ComposeFest2021&quot; data-og-url=&quot;https://github.com/NetLSS/ComposeFest2021&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mqnbR/hyMKNSmZGp/Rh4rI3BY2Gd57lIIlKdw31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/NetLSS/ComposeFest2021&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/NetLSS/ComposeFest2021&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mqnbR/hyMKNSmZGp/Rh4rI3BY2Gd57lIIlKdw31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - NetLSS/ComposeFest2021: DevFest 2021 ComposeFest 코드랩 Repo 입니다&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;DevFest 2021 ComposeFest 코드랩 Repo 입니다 . Contribute to NetLSS/ComposeFest2021 development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/630&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.10.14 - [Android] - [Android Studio] 안드로이드 스튜디오 로딩바 꾸미기 : 슈퍼마리오&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/625&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.17 - [Android/App] - [Android] 구글맵 API를 사용한 어플 만들어보기 (위치 검색, 현위치)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Jetpack Compose</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/633</guid>
      <comments>https://whyprogrammer.tistory.com/633#entry633comment</comments>
      <pubDate>Sat, 18 Dec 2021 21:41:14 +0900</pubDate>
    </item>
    <item>
      <title>갤럭시 워치 4 (워치 시리즈) 롤렉스 페이스 다운로드 (Rolex face)</title>
      <link>https://whyprogrammer.tistory.com/pages/%EA%B0%A4%EB%9F%AD%EC%8B%9C-%EC%9B%8C%EC%B9%98-4-%EC%9B%8C%EC%B9%98-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EB%A1%A4%EB%A0%89%EC%8A%A4-%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-Rolex-face</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Galaxy Watch 4 Classic, 기본 모델 등 롤렉스 워치 페이스 다운로드 페이지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갤럭시 워치 4 롤렉스 워치 페이스에 대해서 많은 분들께서 관심을 가지고 계셔서 다운로드 페이지를 따로 만들게 되었습니다. 파일을 직접 드리기보다는 원본 창작자가 남겨놓은 드라이드 링크를 남겨드리니 참고바랍니다.  &lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;링크&quot; href=&quot;https://drive.google.com/drive/u/0/folders/16wuzKgttK9faYO6lur32Gspo0TosOKD6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 해당 드라이브는 언제든지 만료될 수 있으며, 페이지가 제대로 뜨지 않는다면 구글링을 통해 다른 갤럭시 워치페이스를 검색하시면 많이 나오니 참고하시면 좋겠습니다.  &lt;/p&gt;</description>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/pages/%EA%B0%A4%EB%9F%AD%EC%8B%9C-%EC%9B%8C%EC%B9%98-4-%EC%9B%8C%EC%B9%98-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EB%A1%A4%EB%A0%89%EC%8A%A4-%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-Rolex-face</guid>
      <pubDate>Thu, 11 Nov 2021 20:02:20 +0900</pubDate>
    </item>
    <item>
      <title>네이버 피싱 메일 주의 ⚠ 새로운 환경에서 접속이 시도되었습니다</title>
      <link>https://whyprogrammer.tistory.com/631</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 네이버 피싱 메일 주의 ⚠ 새로운 환경에서 접속이 시도되었습니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마전 부터 &quot;네이버 메일로 새로운 환경에서 접속이 시도되었습니다.&quot; 라는 메일이 한달에 한번 정도 주기적으로 오는 것을 발견했다. 네이버 메일 앱에서 알림이 떠서 확인을 해보면 아래와 같은 메일을 확인할 수 있는 경우다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;3068&quot; width=&quot;299&quot; height=&quot;944&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AfROB/btrhMIvaitr/Ck0ZXJAMmT4pOhWtILkMnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AfROB/btrhMIvaitr/Ck0ZXJAMmT4pOhWtILkMnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AfROB/btrhMIvaitr/Ck0ZXJAMmT4pOhWtILkMnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAfROB%2FbtrhMIvaitr%2FCk0ZXJAMmT4pOhWtILkMnK%2Fimg.png&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;3068&quot; width=&quot;299&quot; height=&quot;944&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 교묘한 것이 보낸사람 이름 자체가 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;네이버알림&lt;/b&gt;&lt;/span&gt;이다. 그리고 새로운 환경에서 네이버 로그인을 할 때마다 항상 위와 비슷한 메일을 받았기 때문에 아무 의심없이 진짜 네이버에서 알림을 보냈다고 생각했고, 해외에서 내 아이디가 유출되어 누군가 접속한 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 나는 2차 인증까지 해두었기 때문에 2차 인증이 오지 않은 것을 봐서는 로그인이 시도에서 그쳤다고 생각했는데 잘 생각 해보니까 사실 피싱 메일이라 2차 인증 알림까지 오지도 않은 것이 었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상한 점 두 번째는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;예와 아니오 버튼이 누를 수 없게 되어있다&lt;/span&gt;&lt;/b&gt;는 점이다. 이는 오늘 알고보니 네이버 측에서 자동으로 스팸메일로 분류하여 수상한 링크가 연결된 이미지와 링크를 모두 가려주었기 때문이다. 그래서 해당 피싱메일로 발생한 피해는 얼마없을 것으로 추정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 저런 메일을 받을 때 마다 아이디가 유출된 것이 아닌가 하는 보안 불안감이 생겼었다.. 너무 이상하게 주기적으로 와서 보낸 사람 주소와 연결된 도메인 주소를 검색해 봤을 때 정보도 없었고 해서 이렇게 나마 글로 남겨두려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보낸 사람의 메일 주소는 &lt;span style=&quot;color: #ee2323;&quot;&gt;nservice@naverteamcorp.site&lt;/span&gt; 이며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예와 아니오 버튼으로 이어지는 링크는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;38&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3zfO6/btrhHtTz7xC/x82i6WgWoHQfqUzlKzTa71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3zfO6/btrhHtTz7xC/x82i6WgWoHQfqUzlKzTa71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3zfO6/btrhHtTz7xC/x82i6WgWoHQfqUzlKzTa71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3zfO6%2FbtrhHtTz7xC%2Fx82i6WgWoHQfqUzlKzTa71%2Fimg.png&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;38&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfRw1/btrhMenU8iv/YQii9urHSmNYtkHH44KLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfRw1/btrhMenU8iv/YQii9urHSmNYtkHH44KLk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfRw1/btrhMenU8iv/YQii9urHSmNYtkHH44KLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnfRw1%2FbtrhMenU8iv%2FYQii9urHSmNYtkHH44KLk1%2Fimg.png&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱 봐도 이상한 링크.. &lt;span style=&quot;color: #ee2323;&quot;&gt;epizy.com&lt;/span&gt; 을 구글링 해봐도 수상한 점 밖에는 보이지 않는다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘엔 실제 사이트와 비슷하게 교묘하게 만들어 완전 비슷하게 보이도록 한뒤 사용자의 개인정보를 빼내는 것 같다.. 앞으로 어떤 더 심한게 나올지 모르기 때문에 항상 조심하자.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;046&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/046.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/046.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/630&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.10.14 - [Android] - [Android Studio] 안드로이드 스튜디오 로딩바 꾸미기 : 슈퍼마리오&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/629&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.26 - [Android] - [Android] Kotlin Retrofit (X-Auth-Token 과 함께 Post 하기)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/628&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.18 - [유용한 정보/IT&amp;middot;컴퓨터] - 갤럭시 워치4 롤렉스 페이스 적용기 (galaxy watch 4 classic Rolex face)&lt;/a&gt;&lt;/p&gt;</description>
      <category>유용한 정보</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/631</guid>
      <comments>https://whyprogrammer.tistory.com/631#entry631comment</comments>
      <pubDate>Thu, 14 Oct 2021 22:44:01 +0900</pubDate>
    </item>
    <item>
      <title>[Android Studio] 안드로이드 스튜디오 로딩바 꾸미기 : 슈퍼마리오</title>
      <link>https://whyprogrammer.tistory.com/630</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 안드로이드&amp;nbsp;스튜디오&amp;nbsp;로딩바&amp;nbsp;꾸미기&amp;nbsp;:&amp;nbsp;슈퍼마리오&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무미 건조한 개발 환경에 재미를 한줌이라도 끼얹기 위해 안드로이드 스튜디오의 프로그레스 바(Progress Bar)를 커스터마이징 해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Preferences 창을 열어주세요. 이후 Plugins 탭을 선택합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: right;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;#안드로이드스튜디오 #로딩창 #로딩바 #프로그레스바 #꾸미기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3G45i/btrhMuqmqpg/F1dHpyIeoyuXK1k2Zd3Dfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3G45i/btrhMuqmqpg/F1dHpyIeoyuXK1k2Zd3Dfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3G45i/btrhMuqmqpg/F1dHpyIeoyuXK1k2Zd3Dfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3G45i%2FbtrhMuqmqpg%2FF1dHpyIeoyuXK1k2Zd3Dfk%2Fimg.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plugins 의 Marketplace 에서 &lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;mario&lt;/span&gt; 로 검색해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfblwE/btrhNnRNzNi/3qKvXOfsYvhq6RsUC70lfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfblwE/btrhNnRNzNi/3qKvXOfsYvhq6RsUC70lfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfblwE/btrhNnRNzNi/3qKvXOfsYvhq6RsUC70lfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfblwE%2FbtrhNnRNzNi%2F3qKvXOfsYvhq6RsUC70lfK%2Fimg.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 현재(2021-10-14) 기준 36.9K 의 다운로드 수를 기록한 Mario Progress Bar 를 Install 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 설명에서 슈퍼 마리오 프로그레스 바 예시를 애니메이션으로 미리 체험해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wG7XI/btrhN4RZGWh/yQZ9Hmn6rp5OFHIDFwD5AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wG7XI/btrhN4RZGWh/yQZ9Hmn6rp5OFHIDFwD5AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wG7XI/btrhN4RZGWh/yQZ9Hmn6rp5OFHIDFwD5AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwG7XI%2FbtrhN4RZGWh%2FyQZ9Hmn6rp5OFHIDFwD5AK%2Fimg.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Third-Party Plugins Privacy Note 알림이 뜨면 Accept 로 수락해줍니다. (허용하지 않으면 설치 불가능)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;94&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oYXq3/btrhN3ZRNTP/zv9uJzXLVALf341o5rPZL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oYXq3/btrhN3ZRNTP/zv9uJzXLVALf341o5rPZL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oYXq3/btrhN3ZRNTP/zv9uJzXLVALf341o5rPZL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoYXq3%2FbtrhN3ZRNTP%2Fzv9uJzXLVALf341o5rPZL0%2Fimg.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;94&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Progress Bar 사용 시 슈퍼 마리오 애니메이션이 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 설치를 했는데도 애니메이션이 나타나지 않는다면 안드로이드 스튜디오를 종료 후 재실행 해주세요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 그 외 Progress Bar&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 검색에서 progress 로 검색해보면 다양한 프로그레스 바 커스텀 플러그인이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 인기가 많은 것은 Nyan Progress Bar 인데 저는 많이 사용했었기 때문에 다음으로 인기가 많은 Mario 로 적용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tt29E/btrhMftuQbZ/NWnGywtHdVJBNXaMqz4qZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tt29E/btrhMftuQbZ/NWnGywtHdVJBNXaMqz4qZK/img.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tt29E/btrhMftuQbZ/NWnGywtHdVJBNXaMqz4qZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTt29E%2FbtrhMftuQbZ%2FNWnGywtHdVJBNXaMqz4qZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2188&quot; height=&quot;1680&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c50qNP/btrhMfAh83p/4VWDyRMEAWnahnAobSEl5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c50qNP/btrhMfAh83p/4VWDyRMEAWnahnAobSEl5K/img.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c50qNP/btrhMfAh83p/4VWDyRMEAWnahnAobSEl5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc50qNP%2FbtrhMfAh83p%2F4VWDyRMEAWnahnAobSEl5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2188&quot; height=&quot;1680&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마음에드는 Progress Bar 를 설치해서 사용해 보세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/628&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.18 - [유용한 정보/IT&amp;middot;컴퓨터] - 갤럭시 워치4 롤렉스 페이스 적용기 (galaxy watch 4 classic Rolex face)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/629&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.26 - [Android] - [Android] Kotlin Retrofit (X-Auth-Token 과 함께 Post 하기)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/627&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.12 - [Android/Kotlin] - [Kotlin] 코틀린 인터페이스의 Default implementation 과 JAVA&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Android Studio Progress Bar custom</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/630</guid>
      <comments>https://whyprogrammer.tistory.com/630#entry630comment</comments>
      <pubDate>Thu, 14 Oct 2021 20:35:21 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Kotlin Retrofit (X-Auth-Token 과 함께 Post 하기)</title>
      <link>https://whyprogrammer.tistory.com/629</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Android Retrofit (X-Auth-Token 과 함께 Post 하기) (코틀린)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API 를 사용해서 문제를 해결해야할 경우 X-Auth-Token(토큰) 이 주어졌고 이를 사용해서 관리 키를 POST 메소드로 받아오려고 할때 필요한 코드를 기록해보려고합니다. (&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;삽질 방지용 글&lt;/span&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Field, FieldMap, Query 등을 사용해서 요청할 경우 되지 않아서 좀 삽질을 했습니다만 결론적으로는 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Body&lt;/span&gt;&lt;/b&gt; 를 사용해서 얻어와야 했습니다. (정확히 말하면 @Body 어노테이션)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. POST Example&lt;/h3&gt;
&lt;pre id=&quot;code_1632665098755&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X POST {BASE_URL}/start \
     -H 'X-Auth-Token: {X_AUTH_TOKEN}' \
     -H 'Content-Type: application/json' \
     -d '{
         &quot;problem&quot;: 1
     }'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST 메소드로 보낼 형식 예시는 위와 같았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. Data Class (DTO)&lt;/h3&gt;
&lt;pre id=&quot;code_1632664991050&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class StartDto(
    val auth_key: String,
    val problem: Int,
    val time: Int
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청으로 받아올 데이터 클래스는 위와 같고 이는 인증키, 문제번호, 시간 등으로 이루어져 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3. Service Interface&lt;/h3&gt;
&lt;pre id=&quot;code_1632665057190&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.lilcode.test.kakao2021challenges.dto.StartDto
import retrofit2.Call
import retrofit2.http.*

interface GameService {
    @Headers(
        &quot;X-Auth-Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,
        &quot;Content-Type: application/json&quot;)
    @POST(&quot;start&quot;)
    fun start(@Body params: HashMap&amp;lt;String, Int&amp;gt;): Call&amp;lt;StartDto&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시에서 요구하는 것과 마찬가지로 x auth token, content-type 을 헤더에 정의해주었고 POST 메소드로 start 주소에 Body에 파라미터 하나를 포함해서 요청하려고 위와 같이 정의해주었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Use Retrofit&lt;/h2&gt;
&lt;pre id=&quot;code_1632665247864&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.lilcode.test.kakao2021challenges.databinding.ActivityMainBinding
import com.lilcode.test.kakao2021challenges.dto.StartDto
import com.lilcode.test.kakao2021challenges.service.GameService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : AppCompatActivity() {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = requireNotNull(_binding)

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

        val retrofit = Retrofit.Builder()
            .baseUrl(&quot;https://xxx.xxx.xxx.amazonaws.com/prod/&quot;)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val params = HashMap&amp;lt;String, Int&amp;gt;()
        params[&quot;problem&quot;] = 1

        retrofit.create(GameService::class.java).also{
            it.start(params)
                .enqueue(object: Callback&amp;lt;StartDto&amp;gt;{
                    override fun onResponse(call: Call&amp;lt;StartDto&amp;gt;, response: Response&amp;lt;StartDto&amp;gt;) {
                        if (response.isSuccessful.not()) {
                            Log.e(&quot;@LSS&quot;, &quot;response fail&quot;)
                            Log.e(&quot;@LSS&quot;, response.message())
                            
                            return
                        }

                        response.body()?.let { startDto -&amp;gt;
                            Log.d(&quot;@LSS&quot;, &quot;auth_key: ${startDto.auth_key}, problem: ${startDto.problem}, time: ${startDto.time}&quot;)
                        }
                    }

                    override fun onFailure(call: Call&amp;lt;StartDto&amp;gt;, t: Throwable) {
                        Log.e(&quot;@LSS&quot;, &quot;connect fail&quot;)
                    }
                })
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit 빌더로 레트로핏 객체를 하나 생성하고 아까 정의한 서비스 인터페이스를 사용해서 서비스를 생성해서 연결해줍니다. 이후 enqueue 를 통해 응답 시 처리를 구현해주었습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. 결과&lt;/h3&gt;
&lt;pre id=&quot;code_1632665325590&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;D/@LSS: auth_key: xxxx-xxxx-xxxx-xxxx-xxxxxxxxxx, problem: 1, time: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 디버깅 메세지를 통해서 인증키를 정상적으로 받아온 것을 확인할 수 있습니다. (본문에 주소와 키는 xxx 를 사용해서 실제 주소와 키를 노출하지 않았습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Retrofit Version&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한 레트로핏 버전은 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1632665435340&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/628&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.18 - [유용한 정보/IT&amp;middot;컴퓨터] - 갤럭시 워치4 롤렉스 페이스 적용기 (galaxy watch 4 classic Rolex face)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/627&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.09.12 - [Android/Kotlin] - [Kotlin] 코틀린 인터페이스의 Default implementation 과 JAVA&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/626&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.18 - [Android/App] - [Android] K-MOOC 강좌정보 서비스 앱 (2021 app dev-matching)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>Kotlin</category>
      <category>retrofit</category>
      <category>x-auth-token</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/629</guid>
      <comments>https://whyprogrammer.tistory.com/629#entry629comment</comments>
      <pubDate>Sun, 26 Sep 2021 23:12:25 +0900</pubDate>
    </item>
    <item>
      <title>갤럭시 워치4 롤렉스 페이스 적용기 (galaxy watch 4 classic Rolex face)</title>
      <link>https://whyprogrammer.tistory.com/628</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 갤럭시 워치4 클래식 롤렉스 페이스 적용기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 워치류 기기가 없었으나 계속 구매해야겠다고 생각은 하고 있었는데 이번에 갤럭시 워치4가 공개 되었을 때 바로 사기로 마음먹었다. 일단 디자인도 너무 세련되게 잘 뽑혔고, 무엇보다 OS가 타이젠에서 구글 wear os 로 변경 됨에 따라 구매사유가 충분하다고 생각했다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;009&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/009.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/009.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직전 까지 쓰던 시계는 스무살 때 구매한 시계로 생활하는데 전혀 문제가 없지만.. 오래 쓰기도 했고 무엇보다 금방 질려하는 스타일이라 새로 구매를 하게된 이유도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pdhBJ/btrfpBMfMSf/byqv8nN96IrCNZt6KVPnbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pdhBJ/btrfpBMfMSf/byqv8nN96IrCNZt6KVPnbK/img.png&quot; data-alt=&quot;내가 적용한 워치 페이스 (갤럭시 워치4 클래식)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pdhBJ/btrfpBMfMSf/byqv8nN96IrCNZt6KVPnbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpdhBJ%2FbtrfpBMfMSf%2Fbyqv8nN96IrCNZt6KVPnbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;808&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 적용한 워치 페이스 (갤럭시 워치4 클래식)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;006&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/006.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/006.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. Rolex face&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MR Time, facer, WatchMaker 등 워치 페이스를 제공하는 시계앱들을 둘러보아도 괜찮은 스킨이 많긴 하지만, 롤렉스 스킨은 공식적으로 판매하거나 올려져있는 공개 페이스가 없었다. 그래서 직접 만들거나 이미지를 배경으로 할 수 있는 마이 포토+를 사용하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btHHV4/btrfpfo7jiS/KpkDd7YSkRP6KEcecANMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btHHV4/btrfpfo7jiS/KpkDd7YSkRP6KEcecANMK0/img.png&quot; data-alt=&quot;마이 포토+ 활용 (너무 별로)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btHHV4/btrfpfo7jiS/KpkDd7YSkRP6KEcecANMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtHHV4%2Fbtrfpfo7jiS%2FKpkDd7YSkRP6KEcecANMK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;208&quot; height=&quot;462&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;마이 포토+ 활용 (너무 별로)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이 포토+ 기본 제공 페이스를 사용해서 배경에 원하는 사진을 넣을 수 있는데 여기에 롤렉스 페이스를 적용시킬 수는 있으나 시계침이나 컴플리케이션 등을 롤렉스에 맞추어 커스텀하기에는 역부족이다. 자연스럽지 않고 커스텀 할 수 있는 영역이 한정적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. Samsung Developer : Watch Face Studio&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐색을 위해 google 에 검색을 해본다면 한국에서도 그렇고 외국에서도 갤럭시 워치에 롤렉스 페이스를 적용하기 위한 많은 시도가 있었다는 것을 알 수 있다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 하다보면 어느 능력자 분이 롤렉스 페이스를 다양하게 구현해둔 것을 찾을 수 있는데 이는 gwd 확장자를 갖는 파일이며. 삼성 개발자 페이지에서 제공하는 Watch face studio 를 통해 직접 워치페이스를 만들게 되면 생성되는 프로젝트 파일이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;1486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df9wB2/btrfoSVhxhI/pxkOBB4o8xxgRMlCXwS0s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df9wB2/btrfoSVhxhI/pxkOBB4o8xxgRMlCXwS0s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df9wB2/btrfoSVhxhI/pxkOBB4o8xxgRMlCXwS0s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf9wB2%2FbtrfoSVhxhI%2FpxkOBB4o8xxgRMlCXwS0s0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1438&quot; height=&quot;1486&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;1486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 목록을 전부 받은 모습인데 롤렉스 이외에도 오메가, 알마니 등의 다른 브랜드 페이스도 있다. 그런데 대부분이 롤렉스 페이스이다. 사실 2년전에 만들어진 페이스들이긴 한데 롤렉스라 그런지 그래도 꽤 태가난다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;gwd 파일을 공유하고 싶으나, 직접 공유하기는 그렇고 구글에 Rolex watchface for Galaxy Watch (links in description) 키워드로 검색하면 나오는 문서를 참고하면 되겠다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 gwd 파일을 이용해서 워치 페이스를 빌드하려면 Samsung Developers 페이지에서 &lt;a href=&quot;https://developer.samsung.com/watch-face-studio/download.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot; data-darkreader-inline-color=&quot;&quot;&gt;워치 페이스 스튜디오&lt;/span&gt;&lt;/a&gt;를 먼저 설치해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Win 및 Mac 에서 지원하며 Mac 의 경우에는 보안 허용을 해주어야 외부 패키지 설치가 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3308&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VDLrY/btrfoumORZY/EBwK4Nrc2UZ56hJM7aUEc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VDLrY/btrfoumORZY/EBwK4Nrc2UZ56hJM7aUEc1/img.png&quot; data-alt=&quot;설치 화면 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VDLrY/btrfoumORZY/EBwK4Nrc2UZ56hJM7aUEc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVDLrY%2FbtrfoumORZY%2FEBwK4Nrc2UZ56hJM7aUEc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3308&quot; height=&quot;1472&quot; data-origin-width=&quot;3308&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설치 화면 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Watch face studio 사용방법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2624&quot; data-origin-height=&quot;1824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXf2XU/btrfotg90Vy/kwrtSLZ1zThoH94BKACBuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXf2XU/btrfotg90Vy/kwrtSLZ1zThoH94BKACBuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXf2XU/btrfotg90Vy/kwrtSLZ1zThoH94BKACBuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXf2XU%2Fbtrfotg90Vy%2FkwrtSLZ1zThoH94BKACBuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2624&quot; height=&quot;1824&quot; data-origin-width=&quot;2624&quot; data-origin-height=&quot;1824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워치 페이스 스튜디오의 사용방법은 간단하다. 먼저 새로운 프로젝트를 생성해준다. (저의 워치 기준, 갤럭시 워치4 클래식 높이 너비 450)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvpjlE/btrfouG6g8U/aAsioT799UgPaacZ6K7fU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvpjlE/btrfouG6g8U/aAsioT799UgPaacZ6K7fU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvpjlE/btrfouG6g8U/aAsioT799UgPaacZ6K7fU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvpjlE%2FbtrfouG6g8U%2FaAsioT799UgPaacZ6K7fU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;556&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 새로 하나 생성했으면 메뉴-파일-import gwd resource 를 통해서 gwd 리소스를 불러오자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2624&quot; data-origin-height=&quot;1824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2wJ7/btrfuPCuLm9/HQoD6beYUOfvZx2CK7AteK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2wJ7/btrfuPCuLm9/HQoD6beYUOfvZx2CK7AteK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2wJ7/btrfuPCuLm9/HQoD6beYUOfvZx2CK7AteK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2wJ7%2FbtrfuPCuLm9%2FHQoD6beYUOfvZx2CK7AteK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2624&quot; height=&quot;1824&quot; data-origin-width=&quot;2624&quot; data-origin-height=&quot;1824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 프로젝트로 해당 워치 페이스 자원들이 불러와지는데 사실상 이것이 워치페이스 빌드를 위한 준비 완료입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 갤럭시 워치4 (클래식) 개발자 권한 얻기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gwd 파일을 사용하여 본인의 갤럭시 워치에 페이스를 적용하려면 개발자 권한이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbrNb5/btrfqCRrDMr/knbdxe2JMXZ0IOUehxSad0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbrNb5/btrfqCRrDMr/knbdxe2JMXZ0IOUehxSad0/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbrNb5/btrfqCRrDMr/knbdxe2JMXZ0IOUehxSad0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbrNb5%2FbtrfqCRrDMr%2Fknbdxe2JMXZ0IOUehxSad0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MPwzp/btrfsev1FQn/FKPvLkaQ5YFWBCwRdlu7Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MPwzp/btrfsev1FQn/FKPvLkaQ5YFWBCwRdlu7Bk/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MPwzp/btrfsev1FQn/FKPvLkaQ5YFWBCwRdlu7Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMPwzp%2Fbtrfsev1FQn%2FFKPvLkaQ5YFWBCwRdlu7Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JFm5O/btrfqEaGrL2/wH0AwcyFvX3tsqUYM0SSKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JFm5O/btrfqEaGrL2/wH0AwcyFvX3tsqUYM0SSKk/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JFm5O/btrfqEaGrL2/wH0AwcyFvX3tsqUYM0SSKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJFm5O%2FbtrfqEaGrL2%2FwH0AwcyFvX3tsqUYM0SSKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot; data-darkreader-inline-color=&quot;&quot;&gt;설정-워치 정보-소프트웨어&lt;/span&gt;&lt;/b&gt; 로 들어가신 후 &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot; data-darkreader-inline-color=&quot;&quot;&gt;소프트웨어 버전&lt;/span&gt;&lt;/b&gt;을 연타해주시면 토스트 메세지로 개발자 권한 얻음 이라는 메세지가 나타납니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ftwl2/btrfoRhH6Td/5KrBcb5sLlTE5H9pmttUCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ftwl2/btrfoRhH6Td/5KrBcb5sLlTE5H9pmttUCk/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ftwl2/btrfoRhH6Td/5KrBcb5sLlTE5H9pmttUCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFtwl2%2FbtrfoRhH6Td%2F5KrBcb5sLlTE5H9pmttUCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HrEzK/btrfseitFmO/nsNu9FaiY4eRzfs5mDUxj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HrEzK/btrfseitFmO/nsNu9FaiY4eRzfs5mDUxj1/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HrEzK/btrfseitFmO/nsNu9FaiY4eRzfs5mDUxj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHrEzK%2FbtrfseitFmO%2FnsNu9FaiY4eRzfs5mDUxj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDQM0c/btrfpCK9TUs/NyIGGK2kH4HkMS49gJvUSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDQM0c/btrfpCK9TUs/NyIGGK2kH4HkMS49gJvUSk/img.png&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;396&quot; style=&quot;width: 32.5581%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDQM0c/btrfpCK9TUs/NyIGGK2kH4HkMS49gJvUSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDQM0c%2FbtrfpCK9TUs%2FNyIGGK2kH4HkMS49gJvUSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 설정 화면을 다시 들어가보면 개발자 옵션이 추가된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 옵션에서 ADB 디버깅을 활성화 해주시고 Wi-Fi로 디버깅 옵션은 on 으로 바꿔주세요. (블루투스로 디버깅하셔도 됩니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 스마트폰 디버깅 모드 활성화 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 안드로이드 스마트폰도 개발자 권한과 디버깅하기 옵션이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce3fnV/btrfoRIOHMZ/W7Nf1OLmIntipzaYc1NBuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce3fnV/btrfoRIOHMZ/W7Nf1OLmIntipzaYc1NBuk/img.png&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce3fnV/btrfoRIOHMZ/W7Nf1OLmIntipzaYc1NBuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce3fnV%2FbtrfoRIOHMZ%2FW7Nf1OLmIntipzaYc1NBuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaC5QP/btrfolQWE8G/V2yKWMMCbo3Y2m0Y8bduL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaC5QP/btrfolQWE8G/V2yKWMMCbo3Y2m0Y8bduL1/img.png&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaC5QP/btrfolQWE8G/V2yKWMMCbo3Y2m0Y8bduL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaC5QP%2FbtrfolQWE8G%2FV2yKWMMCbo3Y2m0Y8bduL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정-개발자옵션 을 통해 USB 디머깅 모드 사용을 on으로 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 워치 페이스 빌드하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅 모드 활성화가 모두 완료되었다면 스마트폰과 워치, PC를 같은 wifi로 연결시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 스마트폰과 PC를 USB로 (유선으로) 연결해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EvsHY/btrfpBepwww/C5iO8zheYyyG27UTZsB9Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EvsHY/btrfpBepwww/C5iO8zheYyyG27UTZsB9Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EvsHY/btrfpBepwww/C5iO8zheYyyG27UTZsB9Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEvsHY%2FbtrfpBepwww%2FC5iO8zheYyyG27UTZsB9Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;274&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 워치 페이스 스튜디오의 우상단의 Run on Device 를 눌러 Device에 빌드하도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2l5rk/btrfpB6xWXg/HlDl5u5MtR7TCqbKKKUq6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2l5rk/btrfpB6xWXg/HlDl5u5MtR7TCqbKKKUq6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2l5rk/btrfpB6xWXg/HlDl5u5MtR7TCqbKKKUq6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2l5rk%2FbtrfpB6xWXg%2FHlDl5u5MtR7TCqbKKKUq6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;374&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Run on Device 시 분석을 통해 AOD 체크를 하게됩니다. 이때 빌드해서 적용 가능할 때만 빌드를 할 수 있게됩니다. 넘어갔다면 패스.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czePs0/btrfsdKCZ0i/Pk3oGRjUQ4rJzkVtcdQIY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czePs0/btrfsdKCZ0i/Pk3oGRjUQ4rJzkVtcdQIY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czePs0/btrfsdKCZ0i/Pk3oGRjUQ4rJzkVtcdQIY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczePs0%2FbtrfsdKCZ0i%2FPk3oGRjUQ4rJzkVtcdQIY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1196&quot; height=&quot;504&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 연결 목록에 만약에 스마튼폰 밖에 없거나 아예 기기목록이 뜨지 않는다면 위 주의 사항을 참고하셔 다시 설정을 해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zwGkj/btrfoHzrcOz/3u3r4KZoZxib6X5awsYLH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zwGkj/btrfoHzrcOz/3u3r4KZoZxib6X5awsYLH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zwGkj/btrfoHzrcOz/3u3r4KZoZxib6X5awsYLH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzwGkj%2FbtrfoHzrcOz%2F3u3r4KZoZxib6X5awsYLH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;490&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 모두 정상적으로 하였고 워치에서 디버깅을 허용하겠냐는 안내가 뜨면 OK를 눌러주면 연결 디바이스에 Galaxy Watch 가 정상적으로 표시되며 해당 기기를 눌러 워치에 적용하시면됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPGmTC/btrfseJx2De/10qLe3vMu6ZnRLtORVp2Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPGmTC/btrfseJx2De/10qLe3vMu6ZnRLtORVp2Uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPGmTC/btrfseJx2De/10qLe3vMu6ZnRLtORVp2Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPGmTC%2FbtrfseJx2De%2F10qLe3vMu6ZnRLtORVp2Uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1440&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 설명드린 내용을 통해 간단하게 커스텀 롤렉스 워치 페이스를 적용해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 상당히 만족스러워서 놀랐고요. 분침 또는 시침, 초침 동작도 매우 부드럽고 다른 컴플리케이션 들도 매끄럽게 동작하면서 더 고급적인 느낌을 줘서 놀랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스트랩만 정품 메탈 실버 스트랩으로 바꿔준다면 좀 더 느낌을 살릴 수 있을 것 같습니다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;030&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/030.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/030.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워치4 클래식을 구매해서 받아본지 얼마 안됬지만 사용감도 나쁘지 않고 이러한 커스터마이징 가능한 것들이 많아서 재미있네요. 오래동안 질리지 않고 사용할 수 있을 것 같다는 생각이 듭니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간혹가다 적용할만한 더 신박한 테크가 나온다면 또 포스팅 해보도록 할테니 자주 놀러와주시면 감사하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bddTaT/btrfrfho0Ve/As6WrwYWBy6Jf94EtUfU6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bddTaT/btrfrfho0Ve/As6WrwYWBy6Jf94EtUfU6K/img.png&quot; data-alt=&quot;워치에 설치해둔 Rolex 페이스들..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bddTaT/btrfrfho0Ve/As6WrwYWBy6Jf94EtUfU6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbddTaT%2Fbtrfrfho0Ve%2FAs6WrwYWBy6Jf94EtUfU6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1440&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;워치에 설치해둔 Rolex 페이스들..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한게 있으시거나 의견이 있으시다면 덧글로 남겨주세요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[2021-10-08 추가]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2021-10-08-19-16-14.jpeg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v1Etp/btrhdCvAqFi/ktNSZpTMAiIH6wIPAcDbuK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v1Etp/btrhdCvAqFi/ktNSZpTMAiIH6wIPAcDbuK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v1Etp/btrhdCvAqFi/ktNSZpTMAiIH6wIPAcDbuK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv1Etp%2FbtrhdCvAqFi%2FktNSZpTMAiIH6wIPAcDbuK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1440&quot; data-filename=&quot;KakaoTalk_Photo_2021-10-08-19-16-14.jpeg&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 메탈 스트랩을 장만했습니다. 갤럭시 워치4 클래식 42mm 에 맞는 날개형 메탈 실버 스트랩이 한동안 안보였는데 슬슬 보이기 시작하는 것 같아서 바로 구매했네요. 일반 스트랩 같은 경우에는 워치와 스트랩 사이에 공간이 남지만, 날개형 스트랩의 경우에는 공간을 메꿔주어 보기에 더 좋습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot; data-darkreader-inline-color=&quot;&quot;&gt;⚠️ 최근 메일로 워치 페이스를 보내달라고 하시는 분들이 많은데, 저작권으로 인하여 메일 공유는 어려우며, 덧글에 링크를 남겨드렸으니 참고하시기 바랍니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #8a3db6; --darkreader-inline-color: #ef3636;&quot; data-darkreader-inline-color=&quot;&quot;&gt;2021-11-11 내용추가&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #9f59c7;&quot;&gt;많은 분들께서 덧글로 링크를 요청드려서 직접 첨부파일을 올리기에는 여러 문제사항이 많아서 따로 링크를 탈 수 있는 페이지를 열었습니다. &lt;a title=&quot;롤렉스 워치 페이스 다운로드 페이지&quot; href=&quot;https://whyprogrammer.tistory.com/pages/%EA%B0%A4%EB%9F%AD%EC%8B%9C-%EC%9B%8C%EC%B9%98-4-%EC%9B%8C%EC%B9%98-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EB%A1%A4%EB%A0%89%EC%8A%A4-%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-Rolex-face&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt; 링크를 참고하셔서 꼭 원하시는 롤렉스 워치페이스 적용하시기를 기원합니다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;--darkreader-inline-color: #9f59c7;&quot;&gt;빠른 블로그 확인이 어렵습니다. 그래도 덧글 남겨주시면 최대한 확인해보려고 노력하도록 하겠습니다. 감사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/320&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;알약 잘 삼키는 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/315&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스트레칭으로 다크서클 없애는 방법&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/281&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;의외로 잘 모르는 니트 보관 방법&lt;/a&gt;&lt;/p&gt;</description>
      <category>유용한 정보/IT&amp;middot;컴퓨터</category>
      <category>갤럭시 워치4 롤렉스 페이스</category>
      <category>갤럭시워치4</category>
      <category>갤럭시워치4 클래식</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/628</guid>
      <comments>https://whyprogrammer.tistory.com/628#entry628comment</comments>
      <pubDate>Sat, 18 Sep 2021 20:43:45 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 코틀린 인터페이스의 Default implementation 과 JAVA</title>
      <link>https://whyprogrammer.tistory.com/627</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 코틀린&amp;nbsp;인터페이스의&amp;nbsp;Default&amp;nbsp;implementation&amp;nbsp;과&amp;nbsp;JAVA&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린의 인터페이스는 추상 메서드 뿐만이 아니라 구현이 있는 메서드 즉, Default 메서드를 정의할 수 있다. (하지만 필드(상태)는 불가능하다.) 클래스는 class 로 정의하듯 인터페이스는 interface 로 정의 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1631380021103&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Clickable {
    fun click()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 인터페이스는 click() 이라는 추상 메서드를 정의하며 이를 구현하는 클래스는 click()을 반드시 재정의할 필요가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1631380095642&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Button : Clickable {
    override fun click() = println(&quot;it was clicked&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바와 달리 콜론(:) 뒤에 확장할 클래스나 인터페이스를 붙여준다. 인터페이스 구현에는 개수 제한이 없지만 클래스의 경우 오직 하나만 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;override 변경자는 상위 클래스 또는 인터페이스의 프로퍼티 or 메서드를 재정의한다는 의미이며. 만약 상위 클래스와 겹치는 네임을 사용하고 override 를 쓰지 않는다면 겹치는 문제를 컴파일러가 미리 예방한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1. 코틀린 인터페이스에 디폴트 구현 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 인터페이스는 디폴트 구현이 사용가능하다. 예를 들어&lt;/p&gt;
&lt;pre id=&quot;code_1631380272345&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Clickable {
    fun click()
    fun showOff() = println(&quot;I'm clickable..!&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;showOff() 는 디폴트 구현 메서드이며 메서드 시그니처 뒤에 단지 본문을 추가하는 것으로 구현이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하는 클래스가 오버라이딩 한다면 새로 적용되지만, 그렇지 않은 경우 디폴트 구현을 따른다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2. 한 클래스에서 동시에 같은 메서드 명을 포함한 인터페이스를 구현한다면?&lt;/h3&gt;
&lt;pre id=&quot;code_1631380429534&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Focusable {
    fun setFocus(b: Boolean) = 
        println(&quot;I ${if (b) &quot;get&quot; else &quot;lost&quot;} focus.&quot;)
    
    fun showOff() = println(&quot;I'm focusable..!&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Clickable 외에 위와 같은 Focusable 같은 인터페이스가 존재할 때 두개를 같이 구현하는 클래스가 있다면 어떤일이 벌어질까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 둘다 구현한다고 명시하는 클래스가 showOff()를 재정의하지 않으면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;컴파일 오류&lt;/b&gt;&lt;/span&gt;가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1631380564041&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Button : Clickable, Focusable {
    override fun click() = println(&quot;I was clicked&quot;)
    override fun showOff() {
        super&amp;lt;Clickable&amp;gt;.showOff()
        super&amp;lt;Focusable&amp;gt;.showOff()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그와 같은 구현을 컴파일러가 강제하는 것이기 때문에 이를 구현해준다. 이때 super 와 꺽쇄(&amp;lt;&amp;gt;)를 사용해서 해당 인터페이스를 명시하여 호출을 명시하는 방법을 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JAVA 에서 코틀린 메서드가 있는 인터페이스를 구현해야할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린은 자바 6과 호환하도록 설계되었기 때문에 인터페이스의 디폴트 메서드를 지원하지 않는다. (자바 8부터 지원)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 코틀린은 디폴트 메서드가 있는 인터페이스를 &lt;span style=&quot;background-color: #c1bef9;&quot;&gt;일반 인터페이스 + 디폴트 메서드가 정적 메서드로 구현된 클래스&lt;/span&gt;를 조합해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트 메서드가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해서 사용해야 한다면, 코틀린에서 본문을 제공하는 메서드를 포함하는 모든 메서드에 대한 본문을 작성해야만한다. (자바에서 코틀린 디폴트 메서드 구현에 의존 불가능..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 코틀린 인터페이스와 디폴트 구현에 대해서 좀 깊게 알아보았는데, 언젠가 필요시 참고가 되길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/565&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2020.07.22 - [Android/Kotlin] - [코틀린] typealias 예제(examples)코드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/570&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.04.14 - [Android/Kotlin] - [Kotlin] 코틀린 프로젝트 IntelliJ IDE 에서 GitHub 연동하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/568&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.04.14 - [Android/Kotlin] - [Kotlin] 인텔리제이(IntelliJ) new package, class 없는 경우 해결&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/Kotlin</category>
      <category>java</category>
      <category>Kotlin</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/627</guid>
      <comments>https://whyprogrammer.tistory.com/627#entry627comment</comments>
      <pubDate>Sun, 12 Sep 2021 02:22:23 +0900</pubDate>
    </item>
    <item>
      <title>[Android] K-MOOC 강좌정보 서비스 앱 (2021 app dev-matching)</title>
      <link>https://whyprogrammer.tistory.com/626</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 프로그래머스 앱 데브 매칭 2021 : K-MOOC 강좌정보 서비스 앱 과제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스에서 금년도 6월 19일에 진행한 앱 데브 매칭에 대한 의도한 해답을 공개했다. 해당 과제를 내가 치룰 당시에는 안드로이드 뷰 모델에 대한 학습이 안되어있어서 당황하며 어찌저찌 거의 구현은 했는데 나 자신이 만족할만한 수준의 앱이 아니어서 상당히 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데브 매칭의 경우에는 다른 블로그 글들을 찾아보면 메일로 코드 리뷰 및 피드백을 주는 것을 본 적이 있는데, 이번에는 공개해답을 아예 공식 블로그에 공개를 해주었다. 안드로이드 신입 개발자를 준비하고 있는 나에게는 참 반가운 소식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 코드에는 정답이 없지만 어느 정도 통용되는 또는 안드로이드에서 적극 권장하는 아키텍처가 분명 존재하기 때문에 이러한 예시 앱이나 해설에 대한 갈증이 많았었다. 공식 문서는 깔끔하지만 친절하지 않은데 이번 출제자님의 해설은 그나마 친절한 편이라고 생각한다.  &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;앱 최종 결과 스크린샷&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgOHcG/btrcs80rORS/9onTDGJPsfkit5AvDzwYZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgOHcG/btrcs80rORS/9onTDGJPsfkit5AvDzwYZK/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629253800.png&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgOHcG/btrcs80rORS/9onTDGJPsfkit5AvDzwYZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgOHcG%2Fbtrcs80rORS%2F9onTDGJPsfkit5AvDzwYZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brSugo/btrctvnwbGu/cijqPqKhMspgkpvCPD0WVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brSugo/btrctvnwbGu/cijqPqKhMspgkpvCPD0WVK/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629253866.png&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brSugo/btrctvnwbGu/cijqPqKhMspgkpvCPD0WVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrSugo%2FbtrctvnwbGu%2FcijqPqKhMspgkpvCPD0WVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CuMdq/btrcolfdgaK/IrykO02y2oNZIWCLWLvK91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CuMdq/btrcolfdgaK/IrykO02y2oNZIWCLWLvK91/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629253893.png&quot; style=&quot;width: 32.5581%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CuMdq/btrcolfdgaK/IrykO02y2oNZIWCLWLvK91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCuMdq%2FbtrcolfdgaK%2FIrykO02y2oNZIWCLWLvK91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 기능은 이렇게만 보면 엄청 간단한 편이다. 공공 데이터 포털에서 제공하는 K-MOOK 강좌 공개 api를 사용해서 강좌 리스트를 리사이클러뷰에 뿌리고 클릭 시 해당 강좌에 디테일한 정보와 html 을 웹뷰에 뿌려주는 형식으로 앱 구현 요구사항이 주어진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;앱 아키택처&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPDMT4%2Fbtq91UxnV20%2FIAcFC8xWj72on5iy5mldKk%2Fimg.png&quot; alt=&quot;앱 아키텍처&quot; width=&quot;663&quot; height=&quot;497&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앱 아키텍쳐의 경우에는 안드로이드에서 권장하는 아키텍처를 따르며 유저 인터페이스는 &lt;b&gt;목록(KmookListActivity)&lt;/b&gt; 과&amp;nbsp;&lt;b&gt;상세&amp;nbsp;(KmookDetailActivity)&lt;/b&gt; 두 가지 액티비티로 이루어져 있는 구조입니다. (목록에서 강좌 클릭 시 상세로 넘어가는 방식)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 아키텍처를 모두 구현하지 않아도 기본적으로 베이스 코드가 주어졌습니다. ViewModel, Repository 등 기본적인 내용은 미리 구성되어 코드로 주어졌고 나머지 부분을 채워 과제를 완성해가는 내용이었지요. (아래)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;926&quot; width=&quot;379&quot; height=&quot;562&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2VpAa/btrcurebiwO/267kZKhWfl7zCZRIunFKt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2VpAa/btrcurebiwO/267kZKhWfl7zCZRIunFKt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2VpAa/btrcurebiwO/267kZKhWfl7zCZRIunFKt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2VpAa%2FbtrcurebiwO%2F267kZKhWfl7zCZRIunFKt0%2Fimg.png&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;926&quot; width=&quot;379&quot; height=&quot;562&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;api 서버를 사용하기 위해 http 통신을 위해 보통 retrofit 과 같은 외부 라이브러리에 의존하지만 이번 과제에서는 외부 라이브러리 사용이 불가했고 network 패키지에 이를 위한 HttpClient 등이 베이스 코드로 주어져서 이를 활용만 하면 됐습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;LiveData 나 DI 에 대해서는 구현자의 과제로 남겨두었고 필수는 아니었습니다. DI에 대한 구현 코드 해답은 따로 없기 때문에 이 부분에 대해서도 나중에 구현하여 이 글에 업데이트 하도록 하겠습니다. (규모가 작은 앱이라 DI가 필수는 아니지만 활용할 수 있는 모습을 보여주면 좋겠어서요.)&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요구 사항&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocListActivity&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetching된 데이터를 adapter를 통해 표시하는 부분&lt;/li&gt;
&lt;li&gt;progressBar 표시&lt;/li&gt;
&lt;li&gt;pull to refresh&lt;/li&gt;
&lt;li&gt;무한로딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocDetailActivity&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상세 데이터를 조회하여 기본정보 적용 및 &lt;span style=&quot;color: #ee2323;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;webview를 사용한 표시&lt;/span&gt; 기능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ImageLoader&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;*이미지를 로딩처리(async 한 처리 구현과 cache(이미지 캐싱))&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocRepository&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;json 을 파싱하여 Model 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 과제 당시 구현하지 못했던 기능&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 앱 구조를 전반적으로 살펴보기&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 공개 해설 전체 코드는 &lt;a href=&quot;https://prgms.tistory.com/m/87&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로그래머스 공식 블로그&lt;/a&gt;에서 확인 및 다운로드 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629255482284&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.programmers.kmooc&quot;&amp;gt;

    &amp;lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&amp;gt;
    &amp;lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot; /&amp;gt;

    &amp;lt;application
        android:name=&quot;.KmoocApplication&quot;
        ...&amp;gt;
        &amp;lt;activity android:name=&quot;.activities.detail.KmoocDetailActivity&quot;&amp;gt;&amp;lt;/activity&amp;gt;
        &amp;lt;activity android:name=&quot;.activities.list.KmoocListActivity&quot;&amp;gt;
            &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot; /&amp;gt;

                &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
        &amp;lt;/activity&amp;gt;
    &amp;lt;/application&amp;gt;

&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앱 매니페스트를 보면 애플리케이션은 KmoocApplication 이며, KmoocListActivity 가 런치시 기본 액티비티로 실행되는 것을 알 수 있고 KmoocDetailActivity 도 앱에서 사용되는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocApplication.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629255626234&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocApplication : Application() {
    val kmoocRepository = KmoocRepository()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KmoocApplication 은 KmoocRepository 를 가지고 있습니다. KmoocRepository 는 HttpClient 를 사용하여 서버에서 json 데이터를 받아와 처리하는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocRepository.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629255916833&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocRepository {
    private val httpClient = HttpClient(&quot;http://apis.data.go.kr/B552881/kmooc&quot;)
    private val serviceKey = ...
        
    fun list(completed: (LectureList) -&amp;gt; Unit) {
        httpClient.getJson(
            &quot;/courseList&quot;,
            mapOf(&quot;serviceKey&quot; to serviceKey, &quot;Mobile&quot; to 1)
        ) { result -&amp;gt;
            result.onSuccess {
                completed(parseLectureList(JSONObject(it)))
            }
        }
    }

    fun next(currentPage: LectureList, completed: (LectureList) -&amp;gt; Unit) {
        val nextPageUrl = currentPage.next
        httpClient.getJson(nextPageUrl, emptyMap()) { result -&amp;gt;
            result.onSuccess {
                completed(parseLectureList(JSONObject(it)))
            }
        }
    }

    fun detail(courseId: String, completed: (Lecture) -&amp;gt; Unit) {
        httpClient.getJson(
            &quot;/courseDetail&quot;,
            mapOf(&quot;CourseId&quot; to courseId, &quot;serviceKey&quot; to serviceKey)
        ) { result -&amp;gt;
            result.onSuccess {
                completed(parseLecture(JSONObject(it)))
            }
        }
    }

    private fun parseLectureList(jsonObject: JSONObject): LectureList {
        //TODO: JSONObject -&amp;gt; LectureList 를 구현하세요
        return LectureList.EMPTY
    }

    private fun parseLecture(jsonObject: JSONObject): Lecture {
        //TODO: JSONObject -&amp;gt; Lecture 를 구현하세요
        return Lecture.EMPTY
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 클라이언트에서는 json 데이터 요청시 내부에서 코루틴을 통해서 비동기로 데이터를 요청하여 완료시 지정한 콜백으로 데이터를 처리할 수 있게 해줍니다. 구현해야하는 부분은 TODO 주석이 매겨져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocListViewModel.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629256461935&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListViewModel(private val repository: KmoocRepository) : ViewModel() {

    fun list() {
        repository.list { lectureList -&amp;gt;
        }
    }

    fun next() {
        val currentLectureList = LectureList.EMPTY
        repository.next(currentLectureList) { lectureList -&amp;gt;
        }
    }
}

class KmoocListViewModelFactory(private val repository: KmoocRepository) :
    ViewModelProvider.Factory {
    override fun &amp;lt;T : ViewModel?&amp;gt; create(modelClass: Class&amp;lt;T&amp;gt;): T {
        if (modelClass.isAssignableFrom(KmoocListViewModel::class.java)) {
            return KmoocListViewModel(repository) as T
        }
        throw IllegalAccessException(&quot;Unkown Viewmodel Class&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 모델은 레포지토리를 기본적으로 넘겨받아 생성하게 되며 내부에서 list와 next 로 강좌 리스트와 다음 강좌 리스트 처리 구현을 해주게됩니다. KmoocListViewModel 은 ViewModelProvider.Factory 인터페이스를 구현한 팩토리 클래스의 생성 메서드로 인스턴스를 생성합니다. (이 부분에 대해 낯설게 느껴지신 다면 ViewModel 에 대한 기본 학습이 필요합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocListActivity.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629255777926&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListActivity : AppCompatActivity() {

    private lateinit var binding: ActivityKmookListBinding
    private lateinit var viewModel: KmoocListViewModel

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

        val kmoocRepository = (application as KmoocApplication).kmoocRepository
        viewModel = ViewModelProvider(this, KmoocListViewModelFactory(kmoocRepository)).get(
            KmoocListViewModel::class.java
        )

        binding = ActivityKmookListBinding.inflate(layoutInflater)
        setContentView(binding.root)

        ...

        viewModel.list()
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;메인 액티비티인 KmoocListActivity 에서는 위에서 정의한 뷰모델 인스턴스를 생성하여 사용하게 되며 onCreate 시 가장 마지막으로 viewModel 의 list()를 호출해서 강좌 리스트를 갱신하게 됩니다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;3. 서버로 부터 받아온 Json 파싱하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Json 을 파싱할 때는 보통 2가지 방식을 사용하는 데 첫 번째로는 Entitiy 객체를 만들어 사용하는 방식이 있고 두 번째로는 원하는 값을 직접 파싱하는 방법이 있습니다. 해설 코드에서는 후자를 사용합니다. 이때 안드로이드에서 json 을 다룰 수 있는 JSONObject 를 사용합니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 강좌 리스트의 json 데이터 샘플을 확인해보도록 합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1629257760029&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;pagination&quot;: {
    &quot;count&quot;: 1159,
    &quot;previous&quot;: null,
    &quot;num_pages&quot;: 116,
    &quot;next&quot;: &quot;http:\/\/www.kmooc.kr\/api\/courses\/v1\/course\/list\/?Mobile=1&amp;amp;SG_APIM=2ug8Dm9qNBfD32JLZGPN64f3EoTlkpD8kSOHWfXpyrY&amp;amp;page=2&amp;amp;serviceKey=9%2FCIHgZI1SKc5ppDSwmx0REDZtF61KNeVHqxA54N6MpyAwrf9v%2BOzBvfOxoQyh8%2F8a26oASfPpEFCmnuncdGGA%3D%3D&quot;
  },
  &quot;results&quot;: [
    {
      &quot;blocks_url&quot;: &quot;http:\/\/www.kmooc.kr\/api\/courses\/v1\/blocks\/?course_id=course-v1%3AACRCEDU%2BACRC01%2B2020_02&quot;,
      &quot;effort&quot;: &quot;00:15@07#01:40$07:00&quot;,
      &quot;end&quot;: &quot;2020-10-31T14:30:00Z&quot;,
      &quot;enrollment_start&quot;: &quot;2020-09-07T00:00:00Z&quot;,
      &quot;enrollment_end&quot;: &quot;2020-10-31T14:30:00Z&quot;,
      &quot;id&quot;: &quot;course-v1:ACRCEDU+ACRC01+2020_02&quot;,
      &quot;media&quot;: {
        &quot;course_image&quot;: {
          &quot;uri&quot;: &quot;\/asset-v1:ACRCEDU+ACRC01+2020_02+type@asset+block@1022_청렴_저용량__썸네일.png&quot;
        },
        &quot;course_video&quot;: {
          &quot;uri&quot;: null
        },
        &quot;image&quot;: {
          &quot;raw&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:ACRCEDU+ACRC01+2020_02+type@asset+block@1022_%EC%B2%AD%EB%A0%B4_%EC%A0%80%EC%9A%A9%EB%9F%89__%EC%8D%B8%EB%84%A4%EC%9D%BC.png&quot;,
          &quot;small&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:ACRCEDU+ACRC01+2020_02+type@asset+block@1022_%EC%B2%AD%EB%A0%B4_%EC%A0%80%EC%9A%A9%EB%9F%89__%EC%8D%B8%EB%84%A4%EC%9D%BC.png&quot;,
          &quot;large&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:ACRCEDU+ACRC01+2020_02+type@asset+block@1022_%EC%B2%AD%EB%A0%B4_%EC%A0%80%EC%9A%A9%EB%9F%89__%EC%8D%B8%EB%84%A4%EC%9D%BC.png&quot;
        }
      },
      &quot;name&quot;: &quot;문화와 생활 속 청렴&quot;,
      &quot;number&quot;: &quot;ACRC01&quot;,
      &quot;org&quot;: &quot;ACRCEDU&quot;,
      &quot;short_description&quot;: &quot;문화와 생활 속 청렴강좌는 인문학 속 역사 이야기와 생활 속 청렴 정보통으로 구성되어 있습니다\n인문학 속 역사 이야기에서는 역사 속 인물, 영화 속 주인공, 대중문화의 사례를 통해 쉽고 재미있게 청렴에 대해 다시 한 번 생각해볼 수 있는 시간을 드립니다\n또한 생활 속 청렴 정보통에서는 어렵게만 느껴졌던 청탁금지법, 부패 및 공익신고, 우리나라 청렴지수 등 국민들이 알아야 할 생활 속 반부패 법령, 제도의 정보를 쉽고 재미있게 풀어드리겠습니다\n&quot;,
      &quot;start&quot;: &quot;2020-09-07T00:00:00Z&quot;,
      &quot;start_display&quot;: &quot;Sept. 7, 2020&quot;,
      &quot;start_type&quot;: &quot;timestamp&quot;,
      &quot;pacing&quot;: &quot;instructor&quot;,
      &quot;mobile_available&quot;: true,
      &quot;hidden&quot;: false,
      &quot;invitation_only&quot;: false,
      &quot;teachers&quot;: &quot;신병주, 윤성은, 하재근, 오수진, 이정수&quot;,
      &quot;classfy&quot;: &quot;hum&quot;,
      &quot;middle_classfy&quot;: &quot;husc&quot;,
      &quot;classfy_plus&quot;: &quot;all&quot;,
      &quot;course_period&quot;: &quot;M&quot;,
      &quot;level&quot;: &quot;1&quot;,
      &quot;passing_grade&quot;: &quot;0.70&quot;,
      &quot;audit_yn&quot;: &quot;Y&quot;,
      &quot;fourth_industry_yn&quot;: &quot;N&quot;,
      &quot;home_course_yn&quot;: &quot;N&quot;,
      &quot;home_course_step&quot;: &quot;&quot;,
      &quot;ribbon_yn&quot;: &quot;N&quot;,
      &quot;job_edu_yn&quot;: &quot;N&quot;,
      &quot;linguistics&quot;: &quot;N&quot;,
      &quot;created&quot;: &quot;2020-09-03T11:25:18Z&quot;,
      &quot;modified&quot;: &quot;2021-06-09T06:51:09Z&quot;,
      &quot;ai_sec_yn&quot;: &quot;N&quot;,
      &quot;basic_science_sec_yn&quot;: &quot;N&quot;,
      &quot;org_name&quot;: &quot;국민권익위원회 청렴연수원&quot;,
      &quot;classfy_name&quot;: &quot;인문&quot;,
      &quot;middle_classfy_name&quot;: &quot;인문과학&quot;,
      &quot;language_name&quot;: &quot;한국어&quot;,
      &quot;effort_time&quot;: &quot;07:00&quot;,
      &quot;video_time&quot;: &quot;01:40&quot;,
      &quot;week&quot;: &quot;07&quot;,
      &quot;learning_time&quot;: &quot;00:15&quot;,
      &quot;preview_video&quot;: &quot;&quot;,
      &quot;course_id&quot;: &quot;course-v1:ACRCEDU+ACRC01+2020_02&quot;
    }, ...]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 확인해보면 json 상위에 pagination (페이지 매김 데이터)와 result (각 강좌의 리스트 데이터) 데이터가 있는 것을 알 수 있습니다. 이때 LectureList 를 생성하는 데 필요한 정보만 취사선택하여 파싱하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;parseLectureList&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;872&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dup959/btrcwxZgtte/YUymT23aaTuH1F89gkEouK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dup959/btrcwxZgtte/YUymT23aaTuH1F89gkEouK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dup959/btrcwxZgtte/YUymT23aaTuH1F89gkEouK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdup959%2FbtrcwxZgtte%2FYUymT23aaTuH1F89gkEouK%2Fimg.png&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;872&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;페이지 정보와 lectures: List&amp;lt;Lecture&amp;gt; 를 가지는 LectureList 를 파싱을 통해 생성해 반환합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이는 KmoocRepository 클래스의 list, next&amp;nbsp;의 complete 콜백의 인자를 넘길 때 parseLectureList 를 통해 파싱 후 LectureList 를 넘기게 되어 complete 콜백에서 알맞은 처리가 이루어지게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 앱 최초 실행 시 메인 액티비티(리스트 액티비티)에서 viewModel.list() 를 호출하게되고 이때 lectureList 를 갱신할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;3.1. 리사이클러 뷰에 강좌 리스트 갱신하기&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocListViewModel : list()&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629263442093&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListViewModel(private val repository: KmoocRepository) : ViewModel() {

    var progressVisible = MutableLiveData&amp;lt;Boolean&amp;gt;()
    var lectureList = MutableLiveData&amp;lt;LectureList&amp;gt;()

    fun list() {
        progressVisible.postValue(true)
        repository.list {
            this.lectureList.postValue(it)
            progressVisible.postValue(false)
        }
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Kmooc 리스트 뷰모델에서 list() 메서드는 콜백으로 위와 같이 LectureList(it) 를 받아 LiveData 에 post 해주는 것을 알 수 있습니다. 그러면 이후 해당 라이브 데이터의 변경을 리스트 액티비티에서 정의한 옵저버가 감지하여 리사이클러 어뎁터에 해당 강좌 리스트를 갱신합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629263699191&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListActivity : AppCompatActivity() {

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

        ...
        binding.lectureList.adapter = adapter
        viewModel.lectureList.observe(this) { lectureList -&amp;gt;
            adapter.updateLectures(lectureList.lectures)
            binding.pullToRefresh.isRefreshing = false
        }
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;4. 무한 스크롤 기능 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 스크롤 기능은 리사이클러 뷰에 스크롤 리스너를 추가해서 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629263894282&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        binding.lectureList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)

                val layoutManager = binding.lectureList.layoutManager

                // hasNextPage() -&amp;gt; 다음 페이지가 있는 경우
                if (viewModel.progressVisible.value != true) {
                    val lastVisibleItem = (layoutManager as LinearLayoutManager)
                        .findLastCompletelyVisibleItemPosition()

                    // 마지막으로 보여진 아이템 position 이
                    // 전체 아이템 개수보다 5개 모자란 경우, 데이터를 loadMore 한다
                    if (layoutManager.itemCount &amp;lt;= lastVisibleItem + 5) {
                        viewModel.next()
                    }
                }
            }
        })

        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리사이클러 뷰의 레이아웃 매니저를 통해서 가장 마지막으로 보여지고 있는 아이템 위치를 가져와서 해당 위치가 전체 아이템 개수보다 5개 전에 위치한 경우에 데이터를 추가로 로드해서 무한 스크롤 기능을 구현합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이로써 유저가 만약 스크롤을 거의 마지막(5개 아이템 전)까지 하게 되면 자동으로 다음 페이지를 로드해서 볼 수 있도록 해줍니다. 한꺼번에 전체 페이지를 로드하는 것이 아닌 필요할 때 필요한 만큼의 페이지를 로드하게 되는 것이지요.&lt;/p&gt;
&lt;pre id=&quot;code_1629264280057&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListViewModel(private val repository: KmoocRepository) : ViewModel() {

    var progressVisible = MutableLiveData&amp;lt;Boolean&amp;gt;()
    var lectureList = MutableLiveData&amp;lt;LectureList&amp;gt;()

    ...

    fun next() {
        progressVisible.postValue(true)
        val currentLectureList = this.lectureList.value ?: return
        repository.next(currentLectureList) { lectureList -&amp;gt;
            val currentLectures = currentLectureList.lectures
            val mergedLectures = currentLectures.toMutableList()
                .apply { addAll(lectureList.lectures) }
            lectureList.lectures = mergedLectures // 새로 받아온 페이징 정보 + 머지 데이터 대입
            this.lectureList.postValue(lectureList)
            progressVisible.postValue(false)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리스트 뷰 모델에서는 next() 에서 이러한 처리를 하게됩니다. 이때 기존 페이지에 있던 강좌 리스트 뒤에 새로 받아온 페이지의 강좌 리스트를 합칩니다. 여기서 중요한 점은 LectureList 객체 자체는 기존 것이 아닌 새로 얻어온 페이지의 페이징 정보를 가진 객체라는 점 입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 다음번 next() 호출 시에도 그 다음 페이지를 이어서 받아올 수 있게됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;또한 깨알같이 프로그레스바의 보여짐 여부 또한 LiveData로 관리해서 적절하게 보여주거나 가리도록 하고 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;4.2. Pull to refreshing (아래로 당겨서 새로고침) 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1629264769989&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListActivity : AppCompatActivity() {
	...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ...

        binding.pullToRefresh.setOnRefreshListener {
            viewModel.list()
        }
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;binding.pullToRefresh 는 SwipeRefreshLayout 으로 해당 컴포넌트는 setOnRefreshListener 로 pull to refresh 를 쉽게 구현할 수 있게 해줍니다. 아래로 당겨 새로고침하는 액션에 뷰모델이 가지고 있는 list()를 호출해주면 구현 완료입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서는 아래로 당겨 새로고침의 경우 일반적으로 유저가 생각할 수 있는 새로고침 즉, 초기 리스트를 다시 새로고침 (로딩 되었던 다른 페이지는 새로고침 대상이 아님) 하는 방식의 새로고침을 말합니다. (유튜브 같은 경우에는 새로고침시 새로운 영상이 상위에 업데이트된다.)&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;5. 비동기 이미지 로드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 로드 작업은 오래걸리는 네트워크 작업에 속하므로 IO 스레드에서 비동기로 처리하여 UI가 끊김없이 유저에게 경험될 수 있도록 해주는 것이 바람직합니다. 보통 외부 라이브러리인 Glide 가 이를 대신 해주었는데 코루틴을 사용해서 직접 구현해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 로드 까지는 어찌저찌 하였지만 이미지 캐싱에 대해서는 구현하지 못했어서 어떻게 구현 하라는 걸까 궁금했는데 해설 코드를 보니 약간 허무했는데.. 이미지 캐싱의 경우에는 맵을 사용해서 이전에 해당 url 의 비트맵을 로드한 적이 있다면 캐싱된 비트맵을 사용하는 방법을 사용하는것이었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629265097102&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object ImageLoader {
    private val imageCache = mutableMapOf&amp;lt;String, Bitmap&amp;gt;()

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

        if (imageCache.containsKey(url)) {
            completed(imageCache[url])
            return
        }

        GlobalScope.launch(Dispatchers.IO) {
            try {
                val bitmap = BitmapFactory.decodeStream(URL(url).openStream())
                imageCache[url] = bitmap

                withContext(Dispatchers.Main) {
                    completed(bitmap)
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    completed(null)
                }
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 url 이 비어있으면 이미지가 없는 것으로 이미지 처리를 하지 않고 반환처리합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로는 이전에 이미 해당 url을 처리한 적이 있는 경우 맵에서 해당 url을 키로 전달하여 bitmap을 받아올 수 있습니다. 이를 콜백함수로 지정한 completed 에 전달해서 이미지를 로드합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로 최초 이미지 로드시 코루틴을 사용해서 이미지를 로드합니다. BitmapFactory.decodeStream(URL(url).openStream()) 코드를 통해서 이미지를 URL로부터 Bitmap 을 생성할 수 있고 이는 IO 스레드에서 처리됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Bitmap을 성공적으로 받아온 경우 Main 스레드에서 UI 작업(completed 콜백)을 수행합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LectureViewHolder&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629265633962&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class LectureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val binding = ViewKmookListItemBinding.bind(itemView)
    private val thumbnail = binding.lectureImage
    ...

    fun bind(lecture: Lecture) {
        ...
        ImageLoader.loadImage(lecture.courseImage) { bitmap -&amp;gt;
            thumbnail.setImageBitmap(bitmap)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러뷰 어뎁터의 뷰 홀더 클래스로 사용되는 LectureViewHolder 의 bind 메서드에서 강좌의 url 주소를 ImageLoader.loadImage 에 넘기면서 람다로 completed 시 처리할 작업(Bitmap 이미지 설정)을 지정합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 상세 정보를 웹뷰에 표시하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629266728.png&quot; width=&quot;258&quot; height=&quot;458&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7PJPr/btrcxKdJkTB/uSRGcJLkbG1GV0iY7cwxO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7PJPr/btrcxKdJkTB/uSRGcJLkbG1GV0iY7cwxO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7PJPr/btrcxKdJkTB/uSRGcJLkbG1GV0iY7cwxO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7PJPr%2FbtrcxKdJkTB%2FuSRGcJLkbG1GV0iY7cwxO0%2Fimg.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629266728.png&quot; width=&quot;258&quot; height=&quot;458&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;detail activity 의 강좌 상세 정보의 하단 부분은 web view를 통해서 강좌의 자세한 정보를 표시하게됩니다. api 를 통해서 detail 정보를 받아올 때 html 형식 string 을 받아오는데 이를 활용해서 웹뷰에 보여주면 되는데 json 형식은 아래와 같이 주어지게됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Detail json sample&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629266917233&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;blocks_url&quot;: &quot;http:\/\/www.kmooc.kr\/api\/courses\/v1\/blocks\/?course_id=course-v1%3AAIIA%2BAIIA01%2B2021_T2_AIIA01&quot;,
  &quot;effort&quot;: &quot;00:40@15#10:00$10:00&quot;,
  &quot;end&quot;: &quot;2021-07-31T23:30:00Z&quot;,
  ...,
  &quot;media&quot;: {
    &quot;course_image&quot;: {
      &quot;uri&quot;: &quot;\/asset-v1:AIIA+AIIA01+2021_T2_AIIA01+type@asset+block@2021-04-07_14_20_44f.PNG&quot;
    },
    &quot;course_video&quot;: {
      &quot;uri&quot;: &quot;http:\/\/www.youtube.com\/watch?v=JWggZMvQNus&quot;
    },
    &quot;image&quot;: {
      &quot;raw&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:AIIA+AIIA01+2021_T2_AIIA01+type@asset+block@2021-04-07_14_20_44f.PNG&quot;,
      &quot;small&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:AIIA+AIIA01+2021_T2_AIIA01+type@asset+block@2021-04-07_14_20_44f.PNG&quot;,
      &quot;large&quot;: &quot;http:\/\/www.kmooc.kr\/asset-v1:AIIA+AIIA01+2021_T2_AIIA01+type@asset+block@2021-04-07_14_20_44f.PNG&quot;
    }
  },
  &quot;name&quot;: &quot;확률적 그래픽 모델&quot;,
  ...,
  &quot;overview&quot;: &quot;&amp;lt;div id=\&quot;course-info\&quot;&amp;gt;\n&amp;lt;section class=\&quot;about\&quot;&amp;gt;\n&amp;lt;h2&amp;gt;&amp;lt;i class=\&quot;fa fa-university\&quot;&amp;gt;&amp;lt;\/i&amp;gt;강좌 소개&amp;lt;\/h2&amp;gt;\n&amp;lt;article&amp;gt;\n&amp;lt;h3&amp;gt;&amp;lt;i class=\&quot;fa fa-pencil-square-o\&quot;&amp;gt;&amp;lt;\/i&amp;gt;수업내용\/목표&amp;lt;\/h3&amp;gt;\n&amp;lt;div class=\&quot;article_contents goal\&quot;&amp;gt;본 강좌의목적은확률적그래픽모델의개요를이해하고,이를응용한각종...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 json 데이터 중에서 overview 데이터에 html 형식이 담겨져 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;KmoocDetailActivity&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629267058951&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocDetailActivity : AppCompatActivity() {

    companion object {
        const val INTENT_PARAM_COURSE_ID = &quot;param_course_id&quot;
    }

    private lateinit var binding: ActivityKmookDetailBinding
    private lateinit var viewModel: KmoocDetailViewModel

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

        ...
        val courseId = intent.getStringExtra(INTENT_PARAM_COURSE_ID)
        if (courseId == null || courseId.isEmpty()) {
            finish()
            return
        }

        ...
    }

    private fun setDetailInfo(lecture: Lecture) {
        ...

        binding.webView.loadData(lecture.overview ?: &quot;&quot;, &quot;text/html&quot;, &quot;UTF-8&quot;)
        binding.webView.visibility = (lecture.overview?.isEmpty() == false).toVisibility()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KmoocDetailActivity 에서는 overview 를 확인해서 웹뷰에 로드합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;148&quot; width=&quot;629&quot; height=&quot;90&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BxWxb/btrcp1g3Bb8/kTed1kwPOMkmUwEXsb86l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BxWxb/btrcp1g3Bb8/kTed1kwPOMkmUwEXsb86l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BxWxb/btrcp1g3Bb8/kTed1kwPOMkmUwEXsb86l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBxWxb%2Fbtrcp1g3Bb8%2FkTed1kwPOMkmUwEXsb86l1%2Fimg.png&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;148&quot; width=&quot;629&quot; height=&quot;90&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹뷰의 loadData 로 해당 데이터 문자열을 넘겨주면서, 형식으로 html, 인코딩 UTF-8 을 지정해서 넘겨주기만 하면 웹뷰에 디테일 정보 로드하기 구현 완료입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Detail activity 뷰 업데이트 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KmoocDetailActivity 의 setDetailInfo(lecture: Lecture) 메서드를 통해 뷰에 데이터가 뿌려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KmoocDetailActivity 가 onCreate 시 ViewModelProvider 를 통해 뷰 모델을 받아오며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewModel.lecture.observe(this, this::setDetailInfo) 를 통해서 lecture 에 대한 옵저버를 붙여준다. (setDetailInfo 가 Lecture 를 받아 이를 처리하기에 옵저버로 사용 가능한 것)&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 viewModel.detail(courseId) 이 이루어질 겨우 강좌 id로 부터 lecture 정보가 업데이트 되므로 해당 LiveData 는 옵저버에게 이를 알려 detatil activity 에서 뷰를 갱신할 수 있게된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;깨알 같은 ProgressBar 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1629267717703&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        viewModel.progressVisible.observe(this) { visible -&amp;gt;
            binding.progressBar.visibility = visible.toVisibility()
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;detail 뷰모델이 가지고 있는 progressVisible 이라는 LiveData 를 바탕으로 옵저버를 등록하여 프로그레스바의 표시유무를 셋팅한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 상단 툴바 (뒤로가기) 클릭시 detail activity 종료하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppBarLayout 아래 Toolbar 툴바를 누르면 detail activity 를 종료하기 위해 아래와 같이 구현해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629267297011&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocDetailActivity : AppCompatActivity() {

    ...

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

        ...
        binding.toolbar.setNavigationOnClickListener { finish() }
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 강좌 item 클릭 시 detail activity 실행 시키기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 제공되는 코드이다. 이해하고 알아두면 좋을 것으로 보여 이또한 복습해두도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629267946371&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocListActivity : AppCompatActivity() {

    ...

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

        ...

        val adapter = LecturesAdapter()
            .apply { onClick = this@KmoocListActivity::startDetailActivity }

        binding.lectureList.adapter = adapter
        ...
    }

    private fun startDetailActivity(lecture: Lecture) {
        startActivity(
            Intent(this, KmoocDetailActivity::class.java)
                .apply { putExtra(KmoocDetailActivity.INTENT_PARAM_COURSE_ID, lecture.id) }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러뷰의 아이템을 클릭하는 경우에 해당 강좌의 detail 화면으로 넘어가야하는데 이를 정해주는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뎁터 인스턴스를 생성할 때 apply 를 통해서 onClick 프로퍼티를 할당하게됩니다. 이때 startDetailActivity 의 참조를 넘겨 할당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startDetailActivity 에서는 또한 Intent 에 Extra 데이터로 해당 lecture 의 강좌 ID를 넣어서 전달하게됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629268025650&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class LecturesAdapter : RecyclerView.Adapter&amp;lt;LectureViewHolder&amp;gt;() {

    
    var onClick: (Lecture) -&amp;gt; Unit = {}

    ...
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LecturesAdapter 를 확인해보면 onClick 은 위와 같이 정의된 람다입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629268447747&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KmoocDetailActivity : AppCompatActivity() {

    ...
    companion object {
        const val INTENT_PARAM_COURSE_ID = &quot;param_course_id&quot;
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ...

        val courseId = intent.getStringExtra(INTENT_PARAM_COURSE_ID)
        if (courseId == null || courseId.isEmpty()) {
            finish()
            return
        }
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Detail Activity 가 실행될 때 Intent의 Extra 를 확인해서 강좌 ID를 가져옵니다. 없을 경우 처리 불가하므로 그대로 종료해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. Date &amp;harr; String 변환 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json 데이터로 받아오는 것 중 하나인 Date 정보의 경우 String 형식으로 받아오기 때문에 Date 형식으로 변환 하여 이를 나타내고 싶은 일자 형식으로 다시 뿌려주기 위해 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629268660611&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object DateUtil {

    fun parseDate(dateString: String): Date {
        try {
            val format = SimpleDateFormat(&quot;yyyy-MM-dd'T'HH:mm:ss'Z'&quot;)
            return format.parse(dateString)
        } catch (e: Exception) {
            return Date()
        }
    }

    fun formatDate(date: Date): String {
        val format = SimpleDateFormat(&quot;yyyy/MM/dd&quot;)
        return format.format(date)
    }

    fun dueString(start: Date, end: Date): String {
        return &quot;${formatDate(start)} ~ ${formatDate(end)}&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 또한 기본적으로 주어지는 코드이며, 해설에는 dueString 이 추가되어 시작일 ~ 종료일을 나타내는 문자열 형식으로 변환해주는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String 문자열의 경우 SimpleDateFormat 을 활용해서 Date 형식으로 바꿔줄 수 있으며 이때는 parse 를 사용합니다. 반대로 Date 를 String 형식으로 변환할 때는 format() 에 date 를 넘겨 문자열 형식으로 바꿔줄 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 전체적으로 살펴보면 Repository 에서는 서버에 저장되어있는 강좌들을 요청해서 데이터를 ViewModel 에서 사용할 수 있게 하고 ViewModel 에서는 이를 받아와 LiveData 형식으로 갖고 있어서 Activity 에서는 이를 관찰해서 적절하게 UI 처리를 해줄 수 있는 것을 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 이미지를 비동기적으로 처리해서 뷰에 보여주는 것과 캐싱을 통해서 같은 일을 두번 하지 않도록 하는 방법도 알게되었습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 무한 스크롤, JSON 파싱, html 데이터 웹뷰에 뿌려주기, Date 형식 처리 등을 해볼 수 있는 좋은 경험이 되는 값진 과제였던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 개발자로 일하게되는 그날 까지. 물론 그리고 그 이후에도 즐겁게 개발할 수 있는 제가 되길 기원하며 글을 마무리 하도록 하겠습니다. 도움 되셨다면 공감버튼 한번 부탁드릴게요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/625&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.17 - [Android/App] - [Android] 구글맵 API를 사용한 어플 만들어보기 (위치 검색, 현위치)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/619&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.02 - [Android/App] - [Android] 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/603&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.13 - [Android/App] - [Android] Youtube 앱 만들어보기 (ExoPlayer, MotionLayout)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/App</category>
      <category>프로그래머스 앱 데브매칭</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/626</guid>
      <comments>https://whyprogrammer.tistory.com/626#entry626comment</comments>
      <pubDate>Wed, 18 Aug 2021 15:56:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 구글맵 API를 사용한 어플 만들어보기 (위치 검색, 현위치)</title>
      <link>https://whyprogrammer.tistory.com/625</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 안드로이드 google map api를 활용한 어플 만들어보기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 구글 맵, sk (t map) pai 를 활용해서 위치 검색 및 현위치를 지도상에서 마커로 나타내주는 안드로이드 앱 입니다. 상호명이나 건물명 등으로 검색을 한 결과를 리스트로 보여주고 유저가 이를 클릭하면 지도 화면으로 이동해서 해당 위치를 마커로 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅은 구현 코드 전부를 설명하지는 않고  &amp;zwj;♂️ 핵심 코드를 정리합니다. 구현 코드 전부가 궁금하신 분들은 글 말미에 첨부드리는 저의 깃허브 저장소를 참고하시기 바랍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치 (건물, 상호 명 등) 검색 기능&lt;/li&gt;
&lt;li&gt;검색한 위치 클릭 시 지도 상에 핀(마커)으로 표시해줌&lt;/li&gt;
&lt;li&gt;현재 위치 버튼을 클릭하면 현재 자신의 위치를 보여줌&lt;/li&gt;
&lt;li&gt;+ 무한 스크롤 기능 추가&lt;/li&gt;
&lt;li&gt;+ 위치 권한 거절 시 교육용 팝업 보여줌&lt;/li&gt;
&lt;li&gt;+ 키보드 엔터 시 바로 검색 등.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용 기술&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코루틴(Coroutines)&lt;/li&gt;
&lt;li&gt;okhttp3, retrofit2&lt;/li&gt;
&lt;li&gt;google map&lt;/li&gt;
&lt;li&gt;intent&lt;/li&gt;
&lt;li&gt;POI Geo Reverse (현재 내위치 받아오기)&lt;/li&gt;
&lt;li&gt;Tmap 라이브러리&lt;/li&gt;
&lt;li&gt;등.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결과 화면&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pCbMs/btrb8TQSx66/pi4Eea8aZgkd4MqA7DAba0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pCbMs/btrb8TQSx66/pi4Eea8aZgkd4MqA7DAba0/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629178826.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pCbMs/btrb8TQSx66/pi4Eea8aZgkd4MqA7DAba0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpCbMs%2Fbtrb8TQSx66%2Fpi4Eea8aZgkd4MqA7DAba0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oHYW8/btrcj178g9K/oba8AJpnUhvPIkAmE8qATk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oHYW8/btrcj178g9K/oba8AJpnUhvPIkAmE8qATk/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629178833.png&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oHYW8/btrcj178g9K/oba8AJpnUhvPIkAmE8qATk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoHYW8%2Fbtrcj178g9K%2Foba8AJpnUhvPIkAmE8qATk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 런치시 간단 텍스트뷰와 검색 버튼이 있습니다. 여기에 주소를 입력하고 검색해주면 검색 목록이 아래 뜨고 이를 클릭하면 클릭한 주소지를 구글 맵에서 보여주게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfk1uC/btrctaiS18V/R5sBAZGZHaAGW0jKb8kWL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfk1uC/btrctaiS18V/R5sBAZGZHaAGW0jKb8kWL1/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629178840.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfk1uC/btrctaiS18V/R5sBAZGZHaAGW0jKb8kWL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfk1uC%2FbtrctaiS18V%2FR5sBAZGZHaAGW0jKb8kWL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tjHSZ/btrcrOtC0XB/VKTzDvomW5HpdF8PsZjuMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tjHSZ/btrcrOtC0XB/VKTzDvomW5HpdF8PsZjuMk/img.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot; data-filename=&quot;Screenshot_1629179368.png&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tjHSZ/btrcrOtC0XB/VKTzDvomW5HpdF8PsZjuMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtjHSZ%2FbtrcrOtC0XB%2FVKTzDvomW5HpdF8PsZjuMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 유저의 위치는 민감정보로 이용자 동의가 반드시 필요하며 동의할 경우 현재 위치로 이동하여 현위치를 보여줍니다. 앱 실행 및 테스트는 AVD에서 진행하였으므로 위치를 임의로 지정해서 테스트 해볼 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 레이아웃 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 레이아웃은 activity_main.xml 와 activity_map.xml 으로 구성되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lvtNH/btrcjakg80H/t0Nr11DUFU5lvCKB8R0LX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lvtNH/btrcjakg80H/t0Nr11DUFU5lvCKB8R0LX0/img.png&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;1856&quot; style=&quot;width: 49.03%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lvtNH/btrcjakg80H/t0Nr11DUFU5lvCKB8R0LX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlvtNH%2Fbtrcjakg80H%2Ft0Nr11DUFU5lvCKB8R0LX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1046&quot; height=&quot;1856&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zcL9V/btrcs61QrGb/mBfCKpDxhhOryPUe6wrk71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zcL9V/btrcs61QrGb/mBfCKpDxhhOryPUe6wrk71/img.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1848&quot; style=&quot;width: 49.8072%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zcL9V/btrcs61QrGb/mBfCKpDxhhOryPUe6wrk71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzcL9V%2Fbtrcs61QrGb%2FmBfCKpDxhhOryPUe6wrk71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1058&quot; height=&quot;1848&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629180148243&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
	...
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;EditText
        android:id=&quot;@+id/search_bar_input_view&quot;
        android:inputType=&quot;text&quot;
        android:maxLines=&quot;1&quot;
        .../&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/search_button&quot;
        .../&amp;gt;

    &amp;lt;androidx.recyclerview.widget.RecyclerView
        android:id=&quot;@+id/recycler_view&quot;
      	...
        android:scrollbars=&quot;vertical&quot;
        app:layoutManager=&quot;androidx.recyclerview.widget.LinearLayoutManager&quot;
        /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/empty_result_text_view&quot;
        ... /&amp;gt;

    &amp;lt;ProgressBar
        android:id=&quot;@+id/progress_circular&quot;
        ... /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 엑티비티는 검색어 입력 텍스트, 검색 버튼, 리사이클러 뷰 등으로 이루어져 있습니다. EditText 의 경우에는 인풋 타입을 텍스트, 최대 라인을 1로 잡아주었고 리사이클러뷰 레이아웃 매니저를 xml 상에서 지정해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;activity_map.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629180308341&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
	...&amp;gt;

    &amp;lt;androidx.fragment.app.FragmentContainerView
        android:id=&quot;@+id/map_fragment&quot;
        android:name=&quot;com.google.android.gms.maps.SupportMapFragment&quot;
        ... /&amp;gt;

    &amp;lt;com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id=&quot;@+id/current_location_button&quot;
        ...
        android:src=&quot;@drawable/ic_baseline_my_location_24&quot;
        ... /&amp;gt;

    &amp;lt;ProgressBar
        android:id=&quot;@+id/progress_circular&quot;
        ... /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵 엑티비티 에서는 구글맵을 담아줄 FragmentContainerView 를 추가했고 name으로는 구글맵을 지원하는 프레그먼트 명을 지정해주어야합니다. 현재 위치를 나타내는 버튼은 drawable 자원을 하나 추가해 아이콘을 지정해서 넣어주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 액티비티 모두 로딩 상태를 표시할 프로그레스 바를 추가해주었습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 리사이클러 뷰 사용 하기&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러뷰 에서는 검색한 위치 내역이 리스트로 표시되게 할겁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. 뷰 홀더 아이템 레이아웃 정의&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;292&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A9kmS/btrcrN2AHHE/0028OVKgXTeSOZ6iFuWg7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A9kmS/btrcrN2AHHE/0028OVKgXTeSOZ6iFuWg7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A9kmS/btrcrN2AHHE/0028OVKgXTeSOZ6iFuWg7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA9kmS%2FbtrcrN2AHHE%2F0028OVKgXTeSOZ6iFuWg7K%2Fimg.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;292&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1629181554892&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
	...
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/title_text_view&quot;
        ...
        tools:text=&quot;제목&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/subtitle_text_view&quot;
        ...
        tools:text=&quot;부제목&quot; /&amp;gt;

    &amp;lt;View
        android:id=&quot;@+id/divider_view&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;1dp&quot;
        android:background=&quot;@color/black&quot;
        ... /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 레이아웃의 경우에는 제목, 부제목 형식으로 위치 명칭, 상세 주소를 담아서 보여줄 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. 데이터 클래스 정의 (Entitiy)&lt;/h3&gt;
&lt;pre id=&quot;code_1629181656998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Parcelize
data class SearchResultEntity(
    val fullAddress: String,
    val name: String,
    val locationLatLng: LocationLatLngEntity
) : Parcelable&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 아이템은 데이터 클래스인 SearchResultEntity 를 바탕으로 구성되게 할 것이며 이는 주소, 주소 명칭 그리고 위도 경도를 Float 형식으로 갖고 있는 LocationLatLngEntity 데이터 클래스를 포함합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. RecyclerView Adapter 정의 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 중요한 어뎁터를 구현해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629181772989&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SearchRecyclerAdapter : RecyclerView.Adapter&amp;lt;SearchRecyclerAdapter.SearchResultViewHolder&amp;gt;() {

    private var searchResultList: List&amp;lt;SearchResultEntity&amp;gt; = listOf()
    var currentPage = 1
    var currentSearchString = &quot;&quot;

    private lateinit var searchResultClickListener: (SearchResultEntity) -&amp;gt; Unit

    inner class SearchResultViewHolder(
        private val binding: ViewholderSearchResultItemBinding,
        private val searchResultClickListener: (SearchResultEntity) -&amp;gt; Unit
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bindData(data: SearchResultEntity) = with(binding) {
            titleTextView.text = data.name
            subtitleTextView.text = data.fullAddress
        }

        fun bindViews(data: SearchResultEntity) {
            binding.root.setOnClickListener {
                searchResultClickListener(data)
            }
        }
    }
 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SearchResultEntity 를 리스트로 갖는 searchResultList를 정의해주고 내부에 inner 클래스로 뷰홀더를 정의해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 바인딩을 사용해서 뷰홀더를 생성하며 메서드로 뷰와 데이터를 바인딩 시키는 bindData 와 아이템 클릭 시 발생하는 이벤트 리스너를 지정해줍니다.(bindViews)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;searchResultClickListener 를 어뎁터 생성시 지정해 줄 것이며 이를 사용하여 아이템 클릭 이벤트를 처리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629181991831&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchResultViewHolder {
        val binding = ViewholderSearchResultItemBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return SearchResultViewHolder(binding, searchResultClickListener)
    }

    override fun onBindViewHolder(holder: SearchResultViewHolder, position: Int) {
        holder.bindData(searchResultList[position])
        holder.bindViews(searchResultList[position])
    }

    override fun getItemCount(): Int {
        return searchResultList.size
    }
 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onCreateViewHolder 에서 뷰 홀더를 생성하는 매커니즘을 정의하고 리스너도 전달해서 넘겨주도록 합니다. searchResultClickListener 를 지정하는 부분은 잠시후 아래에서 설명드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 재정의 해주어야하는 onBindViewHolder와 getItemCount 메서드도 정의해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629182170942&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
    @SuppressLint(&quot;NotifyDataSetChanged&quot;)
    fun setSearchResultList(
        searchResultList: List&amp;lt;SearchResultEntity&amp;gt;,
        searchResultClickListener: (SearchResultEntity) -&amp;gt; Unit
    ) {
        this.searchResultList = this.searchResultList + searchResultList
        this.searchResultClickListener = searchResultClickListener
        notifyDataSetChanged()
    }

    fun clearList(){
        searchResultList = listOf()
    }
 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 결과를 담을 리스트는 setSearchResultList 를 통해 지정하며 이때 searchResultClickListener (리스너) 또한 받아와 지정해줍니다. 리스트의 경우에는 무한 스크롤 기능을 위해서 기존 리스트에 더해주도록 설정하였고 clearList 메서드를 따로 두어 리스트 초기화에 사용하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 변경 시 notifyDataSetChanged 으로 데이터 변경을 알려 뷰를 업데이트 할 수 있도록 해줍시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Retrofit 사용하기 (T-map api 사용하기)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. service interface 정의하기&lt;/h3&gt;
&lt;pre id=&quot;code_1629182530358&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ApiService {

    companion object {
        const val MAX_PAGE_CONTENT_SIZE = 30
        const val VERSION = 1
        const val START_PAGE = 1
    }

    @GET(Url.GET_TMAP_LOCATION)
    suspend fun getSearchLocation(
        @Header(&quot;appKey&quot;) appKey: String = Key.TMAP_API,
        @Query(&quot;version&quot;) version: Int = VERSION,
        @Query(&quot;callback&quot;) callback: String? = null,
        @Query(&quot;page&quot;) page: Int = START_PAGE,
        @Query(&quot;count&quot;) count: Int = MAX_PAGE_CONTENT_SIZE, // 한페이지에 얼마나 나타낼 지
        @Query(&quot;searchKeyword&quot;) keyword: String, // 검색어
        ...
    ): Response&amp;lt;SearchResponse&amp;gt;

    @GET(Url.GET_TMAP_REVERSE_GEO_CODE)
    suspend fun getReverseGeoCode(
        @Header(&quot;appKey&quot;) appKey: String = Key.TMAP_API,
        @Query(&quot;version&quot;) version: Int = VERSION,
        @Query(&quot;callback&quot;) callback: String? = null,
        @Query(&quot;lat&quot;) lat: Double,
        @Query(&quot;lon&quot;) lon: Double,
        ...
    ): Response&amp;lt;AddressInfoResponse&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1629182577197&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object Url {
    const val TMAP_URL = &quot;https://apis.openapi.sk.com&quot;

    const val GET_TMAP_LOCATION = &quot;/tmap/pois&quot;

    const val GET_TMAP_REVERSE_GEO_CODE = &quot;/tmap/geo/reversegeocoding&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레트로핏에 사용할 API service 인터페이스를 정의합니다. base 주소로 부터 요청할 주소를 @GET 키워드로 정의해서 각각 필요한 헤더와 쿼리를 넣어 서버에 요청하게됩니다. 이때 API 키, 검색어 등이 필요합니다. 저는 요청 페이지 컨텐츠를 30개로 제한해서 요청했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 함수를 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;suspend 함수&lt;/b&gt;&lt;/span&gt;로 지정해서 코루틴에서 비동기로 처리할 수 있도록 해줍니다. (비동기 처리는 enqueue 를 통해서도 가능함)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sir0D/btrci9eCdJP/K4EgAKgBM5qwn5UyXWuLRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sir0D/btrci9eCdJP/K4EgAKgBM5qwn5UyXWuLRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sir0D/btrci9eCdJP/K4EgAKgBM5qwn5UyXWuLRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsir0D%2Fbtrci9eCdJP%2FK4EgAKgBM5qwn5UyXWuLRK%2Fimg.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 응답에 해당하는 데이터를 받아올 data class 또한 따로 정의 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. Retrofit 유틸 정의하기&lt;/h3&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1629182724069&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object RetrofitUtil {

    val apiService: ApiService by lazy { getRetrofit().create(ApiService::class.java) }

    private fun getRetrofit(): Retrofit {

        return Retrofit.Builder()
            .baseUrl(Url.TMAP_URL)
            .addConverterFactory(GsonConverterFactory.create()) // gson으로 파싱
            .client(buildOkHttpClient()) // OkHttp 사용
            .build()
    }
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object 로 RetrofitUtil 을 정의해줍니다. getRetrofit 에서는 (인스턴스 생성 코드 별도로 필요 없이 바로 호출 사용 가능) Tmap 베이스 주소, gson 컨버터, OkHttp 를 사용해서 레트로핏을 빌드해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apiService 를 사용할때는 아까 위에서 정의해준 서비스 인터페이스인 ApiService를 사용해서 API 구현체를 사용하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629183560382&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun buildOkHttpClient(): OkHttpClient {
        val interceptor = HttpLoggingInterceptor() // 매번 api 호출 시 마다 로그 확인 할것
        if (BuildConfig.DEBUG) {
            interceptor.level = HttpLoggingInterceptor.Level.BODY
        } else {
            interceptor.level = HttpLoggingInterceptor.Level.NONE
        }
        return OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS) // 5초 동안 응답 없으면 에러
            .addInterceptor(interceptor)
            .build()
    }
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OkHttp 를 사용해서 api 호출 결과를 매번 로그를 통해 확인할 수 있도록 해줍니다. 이때 5초 동안 응답이 없으면 에러가 발생하도록 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3. 코루틴을 사용한 비동기 요청 처리&lt;/h3&gt;
&lt;pre id=&quot;code_1629184109107&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity(), CoroutineScope {
    ...
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 엑티비티에서 CoroutineScope 구현 코루틴 컨텍스트 프로퍼티를 재정의&lt;/p&gt;
&lt;pre id=&quot;code_1629184022808&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun searchWithPage(keywordString: String, page: Int) {
        launch(coroutineContext) {
            try {
                ...
                // IO 스레드 사용
                withContext(Dispatchers.IO) {
                    val response = RetrofitUtil.apiService.getSearchLocation(
                        keyword = keywordString,
                        page = page
                    )
                    if (response.isSuccessful) {
                        val body = response.body()
                        // Main (UI) 스레드 사용
                        withContext(Dispatchers.Main) {
                            ...
                            body?.let { searchResponse -&amp;gt;
                                setData(searchResponse.searchPoiInfo, keywordString)
                            }
                        }
                    }
                }
            } catch (e: Exception) {
                ...
            } finally {
                ...
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 버튼이 눌리면 입력한 검색어를 바탕으로 검색을 실시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 정의 해준 coroutineContext 로 launch 해서 코루틴을 시작합니다. 네트워크 작업을 할때는 withContext(Dispatchers.IO) 를 통해 IO 스레드를 사용해서 처리해줍니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레트로핏을 사용해서 티맵 api 를 통해 지도 검색을 합니다. 요청 body가 응답 성공이라면 이를 통해서 데이터를 파싱하여 리사이클러 뷰의 어뎁터에 데이터를 등록하게됩니다. 이때는 UI 스레드 즉, 메인 스레드를 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어뎁터에 데이터 리스트 갱신&lt;/h4&gt;
&lt;pre id=&quot;code_1629184436561&quot; class=&quot;kotlin&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun setData(searchInfo: SearchPoiInfo, keywordString: String) {

        ...
        adapter.setSearchResultList(dataList) {
            ...

            // map 액티비티 시작
            startActivity(Intent(this, MapActivity::class.java).apply {
                putExtra(SEARCH_RESULT_EXTRA_KEY, it)
            })
        }
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뎁터에 데이터 리스트를 갱신(등록) 해줄 때는 해당 아이템의 클릭 리스너를 같이 지정하는데 이때 아이템 클릭 시 해당 데이터에 맞는 지도가 Map Activity 에서 보여지도록 해당 위치 데이터 entitiy 를 인텐트의 Extra에 넣어서 실행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 구글 맵 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메인 엑티비티&lt;/b&gt;에서 검색 결과 아이템을 클릭하면 해당 데이터를 바탕으로 &lt;b&gt;맵 엑티비티&lt;/b&gt;에서 구글맵을 표시합니다. 검색 위치를 표시하는 과정은 아래와 같이 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵 엑티비티 실행 -&amp;gt; getParcelableExtra 로 SearchResultEntity 를 받아옴 -&amp;gt; OnMapReadyCallback 을 구현한 맵 엑티비티에서 onMapReady 를 처리 -&amp;gt; 이때 마커를 만들고 지도에서 이를 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색 결과 entitiy 가져오기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629184991251&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        if (::searchResult.isInitialized.not()) {
            intent?.let {
                searchResult = it.getParcelableExtra&amp;lt;SearchResultEntity&amp;gt;(SEARCH_RESULT_EXTRA_KEY)
                    ?: throw Exception(&quot;데이터가 존재하지 않습니다.&quot;)
                setupGoogleMap()
            }
        }

        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SupportMapFragment 가져와서 callback 전달&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629185049152&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun setupGoogleMap() {
        val mapFragment =
            supportFragmentManager.findFragmentById(binding.mapFragment.id) as SupportMapFragment
        
        mapFragment.getMapAsync(this) // callback 구현 (onMapReady)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;onMapReady 콜백을 재정의&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629185079290&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    override fun onMapReady(map: GoogleMap) {
        this.map = map
        currentSelectMarker = setupMarker(searchResult)

        currentSelectMarker?.showInfoWindow()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 searchResult 를 사용해서 마커를 설정하고 이를 보여줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구글맵 마커 만들기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1629185127059&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun setupMarker(searchResult: SearchResultEntity): Marker {

        // 구글맵 전용 위도/경도 객체
        val positionLatLng = LatLng(
            searchResult.locationLatLng.latitude.toDouble(),
            searchResult.locationLatLng.longitude.toDouble()
        )

        // 구글맵 마커 객체 설정
        val markerOptions = MarkerOptions().apply {
            position(positionLatLng)
            title(searchResult.name)
            snippet(searchResult.fullAddress)
        }

        // 카메라 줌 설정
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(positionLatLng, CAMERA_ZOOM_LEVEL))

        return map.addMarker(markerOptions)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위도와 경도 정보를 가지고 LatLng 객체를 하나 만들어주고 마커 객체 옵션을 MarkerOptions() 를 통해 생성해줍니다. 위치, 제목, 스니펫 등을 설정하고 맵의 moveCamera 를 통해 줌을 설정, 위치를 지정해준뒤 마커를 추가해서 반환해줍니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. LocationManager 를 사용한 내위치 현위치 가져오기&lt;/h2&gt;
&lt;pre id=&quot;code_1629186090511&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private lateinit var locationManager: LocationManager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 매니저 프로퍼티를 하나 정의합니다. 위치 매니저는 앱 프레임워크에 해당하는데 앱이 위치 변경 정보를 수신할 수 있게 해준다. (수신할 수 있는 서비스를 사용할 수 있게 한다.)&lt;/p&gt;
&lt;pre id=&quot;code_1629186289541&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun getMyLocation() {
        // 위치 매니저 초기화
        if (::locationManager.isInitialized.not()) {
            locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        }

        // GPS 이용 가능한지
        val isGpsEnable = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getMyLocation 메서드를 정의한다. 이 메서드는 현위치 버튼의 onClick 시 호출되게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 위치 매니저의 초기화가 안되었다면 (lateinit 이라 초기화 필요) 초기화를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 GPS 이용이 가능한지 확인하고 GPS 이용이 불가능하다면 권한을 얻어야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1629186446815&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        // 권한 얻기
        if (isGpsEnable) {
            when {
                shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) &amp;amp;&amp;amp; shouldShowRequestPermissionRationale(
                    Manifest.permission.ACCESS_COARSE_LOCATION
                ) -&amp;gt; {
                    showPermissionContextPop()
                }

                ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.ACCESS_FINE_LOCATION
                ) != PackageManager.PERMISSION_GRANTED &amp;amp;&amp;amp; ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                ) != PackageManager.PERMISSION_GRANTED -&amp;gt; {
                    makeRequestAsync()
                }

                else -&amp;gt; {
                    setMyLocationListener()
                }
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 전에 한번 거절 되었다면 권한이 왜 필요한지 설명할 팝업을 띄우고 권한이 부여되지 않은 경우에는 퍼미션 요청 작업을 진행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629186587613&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun makeRequestAsync() {
        // 퍼미션 요청 작업. 아래 작업은 비동기로 이루어짐
        ActivityCompat.requestPermissions(
            this,
            arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ),
            PERMISSION_REQUEST_CODE
        )
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 모두 주어졌다면 이제 위치 정보를 받아옵시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 현재 위치가 변경되는 이벤트를 받아올 리스너 클래스를 하나 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629186687672&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    inner class MyLocationListener : LocationListener {
        override fun onLocationChanged(location: Location) {
            // 현재 위치 콜백
            val locationLatLngEntity = LocationLatLngEntity(
                location.latitude.toFloat(),
                location.longitude.toFloat()
            )

            onCurrentLocationChanged(locationLatLngEntity)
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 onLocationChanged 를 재정의 해주어 위치가 변경되는 경우에 처리를 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재 위치를 요청하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629186726015&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun setMyLocationListener() {
        val minTime = 3000L // 현재 위치를 불러오는데 기다릴 최소 시간
        val minDistance = 100f // 최소 거리 허용

        // 로케이션 리스너 초기화
        if (::myLocationListener.isInitialized.not()) {
            myLocationListener = MyLocationListener()
        }

        // 현재 위치 업데이트 요청
        with(locationManager) {
            requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                minTime,
                minDistance,
                myLocationListener
            )
            requestLocationUpdates(
                LocationManager.NETWORK_PROVIDER,
                minTime,
                minDistance,
                myLocationListener
            )
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 위치를 요청할때 myLocationListener 를 리스너로 전달해서 현재 위치를 요청하고 현위치가 변경되는 경우 onCurrentLocationChanged 가 호출되서 위치를 업데이트 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629186956902&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun onCurrentLocationChanged(locationLatLngEntity: LocationLatLngEntity) {
        map.moveCamera(
            CameraUpdateFactory.newLatLngZoom(
                LatLng(
                    locationLatLngEntity.latitude.toDouble(),
                    locationLatLngEntity.longitude.toDouble()
                ), CAMERA_ZOOM_LEVEL
            )
        )

        loadReverseGeoInformation(locationLatLngEntity)
        removeLocationListener() // 위치 불러온 경우 더이상 리스너가 필요 없으므로 제거
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onCurrentLocationChanged 에서는 위치를 현재 위치로 이동시키고 loadReverseGeoInformation 으로 위도 경도 정보로 해당 위치의 지역명을 T-map 서버에서 가져오도록 요청해서 지도에 마커로 표시합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629187033194&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun removeLocationListener() {
        if (::locationManager.isInitialized &amp;amp;&amp;amp; ::myLocationListener.isInitialized) {
            locationManager.removeUpdates(myLocationListener)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 위치를 불러온 경우에는 myLocationListener 를 업데이트 대상에서 지워줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타. 리사이클러뷰 무한 스크롤 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 부터 검색 결과를 모두 나타내기에는 과부하가 걸릴 수 있으니 30개만 보여주고 유저가 스크롤할 경우에 계속해서 +30 개씩 무한 로드해서 보여줄 수 있도록 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1629187917046&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private fun initViews() = with(binding) {
        ...

        // 무한 스크롤 기능 구현
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                recyclerView.adapter ?: return

                val lastVisibleItemPosition =
                    (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
                val totalItemCount = recyclerView.adapter!!.itemCount - 1

                // 페이지 끝에 도달한 경우
                if (!recyclerView.canScrollVertically(1) &amp;amp;&amp;amp; lastVisibleItemPosition == totalItemCount) {
                    loadNext()

                }
            }
        })
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 리사이클러뷰에 스크롤 리스너를 달아서 페이지 끝에 도달한 경우에 다음 페이지를 로딩하도록 구현해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 리사이클러뷰 스크롤 영역이 끝에 도달한 경우 loadNext()를 호출합니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/619&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.02 - [Android/App] - [Android] 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/603&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.13 - [Android/App] - [Android] Youtube 앱 만들어보기 (ExoPlayer, MotionLayout)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/600&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.06.30 - [Android/App] - [Android] 에어비앤비 앱 만들어 보기 (네이버 지도 api, mocky 등)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 앱 프로젝트 전체 코드는 저의 &lt;a href=&quot;https://whyprogrammer.tistory.com/pages/%EB%82%98%EC%9D%98-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%93%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃허브 저장소&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>Android/App</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/625</guid>
      <comments>https://whyprogrammer.tistory.com/625#entry625comment</comments>
      <pubDate>Tue, 17 Aug 2021 17:32:43 +0900</pubDate>
    </item>
    <item>
      <title>[Android] LayoutInflater attachToParent 파라미터 의미를 알아보자</title>
      <link>https://whyprogrammer.tistory.com/624</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Android] LayoutInflater attachToParent parameter mean?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 프로젝트를 여러개 진행해보면서 레이아웃을 inflate 할 때 항상 inflate 메서드의 마지막 파라미터인 attachToParent/Root 를 거의 무조건 false 로 한다 로만 알고 있었지 정확한 의미가 항상 궁금했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;attachToRoot: whether the inflated hierarchy should be attached to the root parameter? If false, root is only used to create the correct subclass of LayoutParams for the root view in the XML.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서를 보면 위와 같이 루트에 대하여 계층적으로 연결할지를 결정하는데 만약 false 인 경우 루트는 XML의 루트 뷰에 대한 올바른 서브 클래스를 만드는데만 사용된다. 라고 하는데 상당히 난해하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지금 or 지금 말고의 문제다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;attachToParent 파라미터의 주요 차이점은 사실 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;true&lt;/b&gt; : child 뷰를 parent 뷰에 지금 즉시 붙이겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;false&lt;/b&gt; : child 뷰를 parent 뷰에 지금 붙이지 않겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그럼 false 인 경우에는 언제 parent 뷰에 붙이겠다는 거지?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 붙이겠다는 의미는 코드상으로 아래 등과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1628723515316&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;parent.addView(childView)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 하는 오해로는 attachToParent 의 인자를 false 로 주면 부모 뷰 아래 추가되지 않을 것이라는 오해인데 사실 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;틀렸다&lt;/b&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;true 던 false 던 parent 뷰 아래 childe 뷰로 추가된다. 단지 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;그 시간이 문제일 뿐&lt;/span&gt;&lt;/b&gt;이다. (언제 붙이느냐의 문제라는 것)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면,&lt;/p&gt;
&lt;pre id=&quot;code_1628723674433&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;inflater.inflate(child,parent,false);
parent.addView(child);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 아래와 동일한 코드라고 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1628723695934&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;inflater.inflate(child,parent,true);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대 해서는 안되는 것&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 현재 parent 뷰에 child 뷰를 추가할 의무가 없는 경우에는 attachToParnet 파라미터를 true 로 두면 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;절대&lt;/span&gt;&lt;/b&gt; 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프레그먼트를 사용한다고 했을 때,&lt;/p&gt;
&lt;pre id=&quot;code_1628723821287&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public View onCreateView(LayoutInflater inflater,ViewGroup parent,Bundle bundle)
  {
        super.onCreateView(inflater,parent,bundle);
        View view = inflater.inflate(R.layout.image_fragment,parent,false);
        .....
        return view;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 세 번째 파라미터(attachToParnet)를 true 로 한다면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;IllegalStateException 예외가 발생&lt;/b&gt;&lt;/span&gt;하게되기 때문입니다. (아래 때문)&lt;/p&gt;
&lt;pre id=&quot;code_1628723958559&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;getSupportFragmentManager()
      .beginTransaction()
      .add(parent, childFragment)
      .commit();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드에서는 하위뷰로 붙일 필요(의무)가 없는데 true를 넘겨주면서 즉시 붙여주었기 때문에 이후에 FragmentManager 가 해당 의무를 다하려고 add 하려는 찰나! 이미 붙였기 때문에 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;IllegalStateException 예외가 발생&lt;/span&gt;&lt;/b&gt;하는 것 이지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 예시에서는 인플레이트 시 Child 뷰에 추가해줄 의무가 없으므로 false 로 해주는 것이 맞겠지요. 그래서 무조건 false 를 주라는 말이 있었나 봅니다. (&lt;s&gt;강의를 하는 강사면 이런것도 알려주면 좋을텐데... 하지말고 궁금하면 내가 찾아봐야지머&lt;/s&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 attachToParnet 가 false 인 경우 parent 뷰는 child 뷰의 터치 이벤트를 가져오지 않는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움 되셨다면 공감 버튼 부탁드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/623&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.06 - [Android] - 안드로이드 아키텍처 개요 : 전체적인 구조를 알아보자 (Android Architecture)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/622&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.04 - [Android/클린 아키텍처] - [Clean Architecture] 15-안드로이드에서의 Dagger2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/619&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.02 - [Android/App] - [Android] 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Reference&lt;/b&gt;&lt;br /&gt;1. https://stackoverflow.com/questions/12567578/what-does-the-layoutinflater-attachtoroot-parameter-mean&lt;/blockquote&gt;</description>
      <category>Android</category>
      <category>attachToParent</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/624</guid>
      <comments>https://whyprogrammer.tistory.com/624#entry624comment</comments>
      <pubDate>Thu, 12 Aug 2021 08:26:10 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 아키텍처 개요 : 전체적인 구조를 알아보자 (Android Architecture)</title>
      <link>https://whyprogrammer.tistory.com/623</link>
      <description>&lt;h1&gt;&lt;b&gt;안드로이드&amp;nbsp;아키텍처&amp;nbsp;개요&amp;nbsp;:&amp;nbsp;전체적인&amp;nbsp;구조를&amp;nbsp;알아보자&amp;nbsp;(Android&amp;nbsp;Architecture)&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 시스템은 소프트웨어 stack 구조로 구성&lt;/li&gt;
&lt;li&gt;앱, 운영체제, 런타임, 미들웨어, 서비스, 라이브러리 등을 레고처럼 쌓듯이 구성&lt;/li&gt;
&lt;li&gt;모바일 장치를 위한 최적의 앱 개발, 실행 환경을 제공하기 위해 긴밀하게 구성 되어있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;2154&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSxRd3/btrblRMFGV1/z7FkbPnn4hlYL3osLdU1u1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSxRd3/btrblRMFGV1/z7FkbPnn4hlYL3osLdU1u1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSxRd3/btrblRMFGV1/z7FkbPnn4hlYL3osLdU1u1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSxRd3%2FbtrblRMFGV1%2Fz7FkbPnn4hlYL3osLdU1u1%2Fimg.jpg&quot; width=&quot;100%&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;2154&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스 커널&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 소프트웨어 스택 맨 아래 위치&lt;/li&gt;
&lt;li&gt;하드웨어 기반 운영체제의 역할 수행&lt;/li&gt;
&lt;li&gt;멀티 테스킹, 메모리 관리, 프로세스 및 전원관리 등&lt;/li&gt;
&lt;li&gt;하드웨어(장치) 드라이버 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 런타임 ART&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 스튜디오에서 앱을 빌드하면 바이트 코드 형태로(Dalvik Executable, DEX) 컴파일 되며&lt;/li&gt;
&lt;li&gt;이를 장치에 설치할 때 안드로이드 런타임이 Ahead-Of-Time 컴파일을 하여 해당 프로레서가 필요로하는 네이티브 명령어(기계어)로 변환해준다&lt;/li&gt;
&lt;li&gt;네이티브 명령어로 변환된 형태를 ELF(Executable and Linkable Format) 이라고 함&lt;/li&gt;
&lt;li&gt;앱이 론칭 될 때마다 ELF 버전으로 실행되기 때문에 빠른 실행 속도, 배터리 수명 향상 효과 있음&lt;/li&gt;
&lt;li&gt;5.x(롤리팝) 이전 버전은 JIT(Just-in-Time) 방식 컴파일 사용 달빅VM 에서 바이트 코드를 하나씩 기계어로 변환하면서 실행했음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 라이브러리&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 개발환경에는 표준 자바 라이브러리 + 안드로이드 라이브러리 포함&lt;/li&gt;
&lt;li&gt;앱 프레임워크 라이브러리 포함, UI 생성, 그래픽 드로잉, 데이터베이스 사용 등&lt;/li&gt;
&lt;li&gt;문자열 처리, 네트워킹, 파일 처리 등 라이브러리 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 라이브러리들&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;android.app&lt;/code&gt; - 앱 모델을 사용할 수 있게 해줌, 모든 안드로이드 앱의 초석이 되는 라이브러리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.content&lt;/code&gt; - 앱과 앱 컴포넌트 간의 콘텐트 사용, 메세징을 가능하게 함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.database&lt;/code&gt; - Content Provier가 제공하는 데이터를 사용할 수 있게 해줌. SQLite 데이터베이스 관리 클래스도 포함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.graphics&lt;/code&gt; - 저수준 2D 그래픽 드로잉 API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.hardware&lt;/code&gt; - 센서 등의 하드웨어의 API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.opengl&lt;/code&gt; - OpenGL ES 3D 그래픽 렌더링 API 의 자바 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.os&lt;/code&gt; - 메세지, 서비스, 프로세스 간 통신을 포함. 표준 운영체제 시스템 서비스를 앱에서 사용할 수 있게함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.media&lt;/code&gt; - 오디오, 비디오 재생 클래스 제공&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.net&lt;/code&gt; - 네트워크 스택을 사용하게 해주는 API. 무선 네트워크 wifi 사용 포함 (&lt;code&gt;android.net.wifi&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.print&lt;/code&gt; - 앱에서 프린터로 콘텐츠 전송을 도와주는 클래스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.provider&lt;/code&gt; - 연락처 등 표준 안드로이드 콘텐츠 제공자 데이터베이스를 사용할 수 있게 해주는 클래스가 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.text&lt;/code&gt; - 화면에 텍스트를 나타내고 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.util&lt;/code&gt; - 문자열 숫자 변환, xml 다루기, 날짜 시간 처리 등 수행 하는 유틸 클래스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.view&lt;/code&gt; - 사용자 인터페이스를 구성하는 뷰 클래스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.widget&lt;/code&gt; - 사전 제작된 사용자 인터페이스 컴포넌트 포함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.webkit&lt;/code&gt; - 웹 콘텐츠를 보여주는 웹뷰 객체 관리하는 API 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C/C++ 라이브러리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 라이브러리의 경우 C/C++ 기반 코드를 감싸고 있는 자바 wrapper임&lt;/li&gt;
&lt;li&gt;2D, 3D 그래픽 처리, 오디오 비디오 재생, 비트맵, 표준 C 시스템 라이브러리(libc) 등&lt;/li&gt;
&lt;li&gt;직접 사용해야할 경우 NDK 사용 (Native Development kit)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱 프레임워크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 앱이 실행되고 관리되는 환경을 구성하는 서비스의 집합체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 서비스&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Activity Manager - 앱의 라이프사이클, 액티비티 스택 제어&lt;/li&gt;
&lt;li&gt;Content Provider - 앱이 다른 앱과 데이터를 공유할 수 있게 함&lt;/li&gt;
&lt;li&gt;Resource Manager - 코드 외에 리소스(그림, 오디오, 사용자 레이아웃 등)을 사용할 수 있게 함&lt;/li&gt;
&lt;li&gt;Notification Manager - 앱이 사용자에게 경고나 알림을 표할 수 있음&lt;/li&gt;
&lt;li&gt;View System - 앱의 UI 생성에 사용되는 확장 가능한 뷰 집합&lt;/li&gt;
&lt;li&gt;Package Manager - 기기에 설치된 다른 앱에 대한 정보 확인 가능&lt;/li&gt;
&lt;li&gt;Window Manager - 기기에 나타나는 화면에 모든 앱의 창을 관리&lt;/li&gt;
&lt;li&gt;Location Manager - 앱이 위치 변경 정보를 수신할 수 있게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이티브 앱 : 기본 웹 브라우저, 이메일 앱 등&lt;/li&gt;
&lt;li&gt;사용자 앱 : 사용자가 설치한 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 아키텍처의 최종 목표는 앱의 성능과 효율을 극대화 시키는 것임&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;본 글은 안드로이드 스튜디오 Arctic Fox 프로그래밍 도서로 학습한 내용임&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/623</guid>
      <comments>https://whyprogrammer.tistory.com/623#entry623comment</comments>
      <pubDate>Fri, 6 Aug 2021 18:53:31 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 15-안드로이드에서의 Dagger2</title>
      <link>https://whyprogrammer.tistory.com/622</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;14. 안드로이드와 Dagger2&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안드로이드를 위한 기본적인 접근 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 특성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 애플리케이션 내에서 액티비티 또는 서비스 와 같은 생명 주기를 갖는 컴포넌트로 구성&lt;/li&gt;
&lt;li&gt;프래그먼트는 단독 존재 불가하며 반드시 액티비티 내에 존재해야함&lt;/li&gt;
&lt;li&gt;애플리케이션을 포함한 액티비티, 서비스 등 컴포넌트는 시스템에 의해서만 인스턴스화 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 특징을 통해 아래와 같은 컴포넌트 그래프를 그릴 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/NetLSS/AndroidCleanArchitecture/blob/master/img/android_app_component.jpeg?raw=true&quot; alt=&quot;안드로이드 앱 컴포넌트 예시&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱은 생명주기 동안 다양한 화면(액티비티) 및 서비스가 생성과 소멸을 반복&lt;/li&gt;
&lt;li&gt;하나의 액티비티 내에서는 여러 프래그먼트가 생성 소멸 반복 가능&lt;/li&gt;
&lt;li&gt;앱 생명주기와 Dagger 컴포넌트의 생명 주기를 같이 하는 앱 컴포넌트 구현&lt;/li&gt;
&lt;li&gt;액티비티나 서비스를 위한 컴포넌트는 앱 컴포넌트의 서브 컴포넌트로 구성한다&lt;/li&gt;
&lt;li&gt;프래그먼트는 액티비티 컴포넌트의 서브 컴포넌트로 다시 지정 한다. (서브의 서브 컴포넌트가 되겠지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[애플리케이션 컴포넌트 생성을 위한 AppComponent]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton

@Component(modules = [AppModule::class])
@Singleton
interface AppComponent {
    fun mainActivityComponentBuilder(): MainActivityComponent.Builder
    fun inject(app: App)

    /*
    팩토리를 통해 생성
    create() 매개 변수로 애플리케이션 컴포넌트의 모듈로 AppModule,
    애플리케이션 클래스인 App을 받음
     */
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance app: App, appModule: AppModule): AppComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module(subcomponents = [MainActivityComponent::class]) // 액티비티를 위한 컴포넌트는 서브 컴포넌트로 구성
class AppModule {

    @Provides
    @Singleton
    fun provideSharedPreferences(app: App) = app.getSharedPreferences(
        &quot;default&quot;,
        Context.MODE_PRIVATE
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.app.Application

class App : Application() {

    private lateinit var appComponent: AppComponent

    @Override
    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent.factory()
            .create(this, AppModule())
    }

    fun getAppComponent() = appComponent
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;App&lt;/code&gt;클래스를 매니페스트에 등록&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt; &amp;lt;application
        ...
        android:name=&quot;.androidComponent.App&quot;
        &amp;gt;
       ...
    &amp;lt;/application&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 인스턴스는 시스템에 의해서만 생성가능&lt;/li&gt;
&lt;li&gt;애플리케이션이 생성된 후에 팩토리의 &lt;code&gt;@BindsInstance&lt;/code&gt; 메서드를 통해 오브젝트 그래프에 바인딩&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AppModule&lt;/code&gt;에서는 앱 생명 주기 동안 싱글턴으로 취급할 SharedPreference 제공&lt;/li&gt;
&lt;li&gt;싱글턴이 아닌 매번 생성을 원하면 &lt;code&gt;@Singletone&lt;/code&gt; 제거 할것.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.BindsInstance
import dagger.Subcomponent

@Subcomponent(modules = [MainActivityModule::class])
@ActivityScope
interface MainActivityComponent {
    fun mainFragmentComponentBuilder(): MainFragmentComponent.Builder

    fun inject(activity: MainActivity)

    @Subcomponent.Builder
    interface Builder{
        fun setModule(module: MainActivityModule): Builder

        /*
         엑티비티 인스턴스도 시스템에 의해 생성되므로
         액티비티 생명주기 콜백 내에서 서브 컴포넌트 빌드 시 바인딩할 수 있도록 @BindsInstance 사용
         setter 메서드로 액티비티 인스턴스를 바인딩
         */
        @BindsInstance
        fun setActivity(activity: MainActivity): Builder
        fun build():MainActivityComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import com.lilcode.hellodagger.MainActivity
import dagger.Module
import dagger.Provides
import dagger.Subcomponent

@Module(Subcomponent = [MainFragmentComponent::class])
class MainActivityModule {
    @Provides
    @ActivityScope // 해당 스코프를 사용하여 액티비티 생명주기 동안 동일한 인스턴스 제공
    fun provideActivityName() = MainActivity::class.simpleName ?: &quot;Failed get Activity simpleName&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스코프 정의&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import javax.inject.Scope

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import javax.inject.Scope

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;FrameLayout
        android:layout_width=&quot;0dp&quot;
        android:id=&quot;@+id/container&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        android:layout_height=&quot;0dp&quot;/&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.lilcode.hellodagger.R
import javax.inject.Inject

class MainActivity: AppCompatActivity() {

    @Inject
    lateinit var sharedPreferences: SharedPreferences

    @Inject
    lateinit var activityName: String

    lateinit var component: MainActivityComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        component = (application as App).getAppComponent() // 매니페스트에 App을 등록했었기에.
            .mainActivityComponentBuilder() // 빌더를 제공 받아
            .setModule(MainActivityModule()) // 모듈 바인딩
            .setActivity(this) // 인스턴스 바인딩
            .build()

        component.inject(this) // 의존성 주입

        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragment())
            .commit()
    }

    @JvmName(&quot;getComponent1&quot;)
    fun getComponent(): MainActivityComponent{
        return component
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션으로 부터 AppComponent 인스턴스를 가져와서 MainActivityComponent.Builder를 제공 받아 액티비티 모듈과 인스턴스를 바인딩&lt;/li&gt;
&lt;li&gt;MainActivityComponent를 생성 한 뒤 의존성 주입&lt;/li&gt;
&lt;li&gt;AppComponent 로 부터 SharedPreference를 주입 받고, 액티비티 컴포넌트에서 String 객체 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.BindsInstance
import dagger.Subcomponent

@FragmentScope
@Subcomponent(modules = [MainFragmentModule::class])
interface MainFragmentComponent {
    fun inject(mainFragment: MainFragment)

    @Subcomponent.Builder
    interface Builder {
        fun setModule(module: MainFragmentModule): MainFragmentComponent.Builder

        @BindsInstance
        fun setFragment(fragment: MainFragment): MainFragmentComponent.Builder
        fun build(): MainFragmentComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import kotlin.random.Random

@Module
class MainFragmentModule {
    @Provides
    @FragmentScope
    fun provideInt() = Random.nextInt()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.fragment.app.Fragment
import javax.inject.Inject
import javax.inject.Named

class MainFragment: Fragment() {

    @Inject
    lateinit var sharedPreferences: SharedPreferences

    @Inject
    lateinit var activityName: String

    @set: [Inject Named(&quot;randomNumber&quot;)]
    var randomNumber: Int? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (activity is MainActivity){
            (activity as MainActivity).getComponent()
                .mainFragmentComponentBuilder()
                .setModule(MainFragmentModule())
                .setFragment(this)
                .build()
                .inject(this)
        }

        Log.d(&quot;MainFragment&quot;, activityName)
        Log.d(&quot;MainFragment&quot;, &quot;randomNUmber = $randomNumber&quot;)
        /*
        실행2
        2021-08-02 18:39:34.958 2853-2853/com.lilcode.hellodagger D/MainFragment: MainActivity
        2021-08-02 18:39:34.958 2853-2853/com.lilcode.hellodagger D/MainFragment: randomNUmber = -1329834136

        실행1
        2021-08-02 18:40:33.910 3342-3342/com.lilcode.hellodagger D/MainFragment: MainActivity
        2021-08-02 18:40:33.910 3342-3342/com.lilcode.hellodagger D/MainFragment: randomNUmber = -709593616
         */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매니패스트 참고&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    package=&quot;com.lilcode.hellodagger&quot;&amp;gt;
&amp;lt;!--
        android:name=&quot;.androidComponent.App&quot; 추가
--&amp;gt;
    &amp;lt;application
        android:allowBackup=&quot;true&quot;
        android:icon=&quot;@mipmap/ic_launcher&quot;
        android:label=&quot;@string/app_name&quot;
        android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
        android:supportsRtl=&quot;true&quot;
        android:name=&quot;.androidComponent.App&quot;
        android:theme=&quot;@style/Theme.HelloDagger&quot;&amp;gt;
        &amp;lt;activity
            android:name=&quot;.androidComponent.MainActivity&quot;
            android:exported=&quot;true&quot;&amp;gt;
            &amp;lt;intent-filter&amp;gt;
                &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot; /&amp;gt;

                &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&amp;gt;
            &amp;lt;/intent-filter&amp;gt;
        &amp;lt;/activity&amp;gt;
    &amp;lt;/application&amp;gt;


&amp;lt;/manifest&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보일러 플레이트 코드 제거&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;android.dagger.*&lt;/code&gt; 패키지 활용하기&lt;/h3&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에서 구현한 안드로이드를 위한 오브젝트 그래프는 아래와 같은 문제점이 있음&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비슷한 형태로 반복되는 보일러 플레이트 코드 생성&lt;/li&gt;
&lt;li&gt;리팩토링의 어려움&lt;/li&gt;
&lt;li&gt;멤버 주입 메서드의 매개변수로 정확한 타입을 알아야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Dagger는 &lt;code&gt;dagger.android&lt;/code&gt; 패키지를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액티비티에 의존성을 주입한다고 가정. 다시 기존 코드를 수정해보자&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component
import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector
import javax.inject.Singleton
/*
안드로이드 프레임워크 관련 클래스에 의존성 주입을 위임할 AndroidInjector&amp;lt;?&amp;gt; 팩토리를 멀티 바인딩으로 관리
 */
@Component(modules = [AndroidInjectionModule::class, AppModule::class])
@Singleton
interface AppComponent: AndroidInjector&amp;lt;App&amp;gt; {

    @Component.Factory
    interface Factory : AndroidInjector.Factory&amp;lt;App&amp;gt;{
        // App 인스턴스를 그래프에 바인딩 하고 Component 를 반환하는 create() 메서드가 이미 포함
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.AndroidInjector
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Named
import javax.inject.Singleton

// MainActivitySubcomponent: MainActivity의 인스턴스에 멤버 인젝션을 담당
@Module(subcomponents = [MainActivitySubcomponent::class])
abstract class AppModule {

    companion object{
        @Named(&quot;app&quot;)
        @Provides
        @Singleton
        fun provideString() = &quot;String from AppModule&quot; // 의존성 주입하는지 확인용
    }

    @Binds
    @IntoMap
    @ClassKey(MainActivity::class)
    abstract fun bindAndroidInjectorFactory(factory: MainActivitySubcomponent.Factory): AndroidInjector.Factory&amp;lt;*&amp;gt;
    // AndroidInjectionModule 내부에 있는 Map에 AndroidInjector.Factory를 멀티 바인딩 한다
    // 서브 컴포넌트들이 편하게 멤버 인젝션을 할 수있게 인젝터 팩토리들을 멀티 바인딩으로 관리
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.app.Application
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject

class App : Application(), HasAndroidInjector{

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector&amp;lt;Any&amp;gt;

    override fun onCreate() {
        super.onCreate()
        DaggerAppComponent.factory()
            .create(this)
            .inject(this)
    }

    override fun androidInjector(): AndroidInjector&amp;lt;Any&amp;gt; {
        return dispatchingAndroidInjector
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Subcomponent
import dagger.android.AndroidInjector

@ActivityScope
@Subcomponent(modules = [MainActivityModule::class])
interface MainActivitySubcomponent: AndroidInjector&amp;lt;MainActivity&amp;gt; {

    @Subcomponent.Factory
    interface Factory: AndroidInjector.Factory&amp;lt;MainActivity&amp;gt;{
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.AndroidInjector
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Named

@Module(subcomponents = [MainFragmentSubcomponent::class]) // MainFragment 멤버 인젝션을 위해 서브 연결
abstract class MainActivityModule {
    companion object{
        @Named(&quot;activity&quot;)
        @Provides
        @ActivityScope
        fun provideString() = &quot;String from MainActivityModule&quot;
    }

    @Binds
    @IntoMap
    @ClassKey(MainFragment::class)
    abstract fun bindInjectorFactory(factory: MainFragmentSubcomponent.Factory): AndroidInjector.Factory&amp;lt;*&amp;gt;
    // MainFragment 를 위한 인젝터 펙토리
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.lilcode.hellodagger.R
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
import javax.inject.Named

class MainActivity: AppCompatActivity(), HasAndroidInjector {

    @Inject
    lateinit var androidInjector: DispatchingAndroidInjector&amp;lt;Any&amp;gt;

    @Inject
    @Named(&quot;app&quot;)
    lateinit var appString: String

    @Inject
    @Named(&quot;activity&quot;)
    lateinit var activityString: String

    override fun onCreate(savedInstanceState: Bundle?) {

        AndroidInjection.inject(this) // 호출 시 App 으로 부터 DispatchingAndroidInjector&amp;lt;Any&amp;gt; 를 얻고
        // 이를 통해 MainActivity에 맞는 AndroidInjector.Factory 클래스 이름을 통해 찾는다
        // 팩토리를 통해서 생성된 MainActivitySubComponent는 액티비티에서 호출한 inject()를 통해 의존성 주입 완료

        Log.e(&quot;MainActivity&quot;, appString)
        Log.e(&quot;MainActivity&quot;, activityString)

        /* 결과
        2021-08-03 11:46:35.979 28214-28214/com.lilcode.hellodagger E/MainActivity: String from AppModule
        2021-08-03 11:46:35.979 28214-28214/com.lilcode.hellodagger E/MainActivity: String from MainActivityModule
         */

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragment())
            .commit()
    }

    override fun androidInjector(): AndroidInjector&amp;lt;Any&amp;gt; {
        return androidInjector
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프래그먼트도 액티비티와 동일한 방식으로 주입&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Subcomponent
import dagger.android.AndroidInjector

@FragmentScope
@Subcomponent(modules = [MainFragmentModule::class])
interface MainFragmentSubcomponent : AndroidInjector&amp;lt;MainFragment&amp;gt;{
    @Subcomponent.Factory
    interface Factory : AndroidInjector.Factory&amp;lt;MainFragment&amp;gt;{
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
class MainFragmentModule {
    @Named(&quot;fragment&quot;)
    @Provides
    @FragmentScope
    fun provideString() = &quot;String from fragment&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import android.content.Context
import android.util.Log
import androidx.fragment.app.Fragment
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import javax.inject.Named

class MainFragment: Fragment() {

    @Inject
    @Named(&quot;app&quot;)
    lateinit var appString: String

    @Inject
    @Named(&quot;activity&quot;)
    lateinit var activityString: String

    @Inject
    @Named(&quot;fragment&quot;)
    lateinit var fragmentString: String

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)

        Log.e(&quot;MainFragment&quot;, appString)
        Log.e(&quot;MainFragment&quot;, activityString)
        Log.e(&quot;MainFragment&quot;, fragmentString)

        /* 결과
        2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from AppModule
        2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from MainActivityModule
        2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from fragment
         */
        super.onAttach(context)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;@ContributesAndroidInjector&lt;/code&gt; 어노테이션 활용하기&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브 컴포넌트의 팩토리가 다른 메서드나 클래스를 상속하지 않으면&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ContributesAndroidInjector&lt;/code&gt; 활용&lt;/li&gt;
&lt;li&gt;서브 컴포넌트 정의 코드 대체&lt;/li&gt;
&lt;li&gt;보일러 플레이트 코드 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[AppComponent.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component
import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector
import javax.inject.Singleton

@Singleton
@Component(modules = [AndroidInjectionModule::class, AppModule::class])
interface AppComponent : AndroidInjector&amp;lt;App&amp;gt; {

    @Component.Factory
    interface Factory : AndroidInjector.Factory&amp;lt;App&amp;gt; {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import javax.inject.Named
import javax.inject.Singleton

@Module
abstract class AppModule {
    companion object {
        @Named(&quot;app&quot;)
        @Provides
        @Singleton
        fun provideString() = &quot;String from AppModule&quot;
    }

    @ActivityScope
    @ContributesAndroidInjector(modules = [MainActivityModule::class])
    abstract fun mainActivity(): MainActivity

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class App : DaggerApplication() {
    override fun applicationInjector(): AndroidInjector&amp;lt;out DaggerApplication&amp;gt; {
        return DaggerAppComponent.factory()
            .create(this)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import javax.inject.Named

@Module
abstract class MainActivityModule {

    companion object {
        @Named(&quot;activity&quot;)
        @Provides
        @ActivityScope
        fun provideString() = &quot;String from MainActivityModule&quot;
    }

    @FragmentScope
    @ContributesAndroidInjector(modules = [MainFragmentModule::class])
    abstract fun mainFragment(): MainFragment
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import android.os.Bundle
import android.util.Log
import com.lilcode.hellodagger.R
import dagger.android.AndroidInjection
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject
import javax.inject.Named

class MainActivity : DaggerAppCompatActivity() {
    @Inject
    @Named(&quot;app&quot;)
    lateinit var appString: String

    @Inject
    @Named(&quot;activity&quot;)
    lateinit var activityString: String

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)

        Log.e(&quot;MainActivity&quot;, appString)
        Log.e(&quot;MainActivity&quot;, activityString)

        /*
        2021-08-03 21:24:44.733 8407-8407/com.lilcode.hellodagger E/MainActivity: String from AppModule
        2021-08-03 21:24:44.733 8407-8407/com.lilcode.hellodagger E/MainActivity: String from MainActivityModule
         */

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragment())
            .commit()

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
class MainFragmentModule {
    @Named(&quot;fragment&quot;)
    @Provides
    @FragmentScope
    fun provideString() = &quot;String from fragment&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import android.content.Context
import android.util.Log
import dagger.android.support.AndroidSupportInjection
import dagger.android.support.DaggerFragment
import javax.inject.Inject
import javax.inject.Named

class MainFragment : DaggerFragment() {
    @Inject
    @Named(&quot;app&quot;)
    lateinit var appString: String

    @Inject
    @Named(&quot;activity&quot;)
    lateinit var activityString: String

    @Inject
    @Named(&quot;fragment&quot;)
    lateinit var fragmentString: String

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)

        Log.e(&quot;MainFragment&quot;, appString)
        Log.e(&quot;MainFragment&quot;, activityString)
        Log.e(&quot;MainFragment&quot;, fragmentString)

        /*
        2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from AppModule
        2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from MainActivityModule
        2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from fragment
         */

        super.onAttach(context)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행시 log가 잘 뜨는 것을 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Application 대신 DaggerApplicataion&lt;/li&gt;
&lt;li&gt;AppCompatActivity 대신 DaggerAppCompatActivity&lt;/li&gt;
&lt;li&gt;Fragement 대신 DaggerFragment&lt;/li&gt;
&lt;li&gt;베이스 클래스 참조가 불가하다면 베이스 클래스 내부 참조하여 HasAndroidInjector 인터페이스 직접 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dagger 베이스 클래스&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DispatchingAndroidInjector는 AndroidInjector.Factory를 런타임에 찾게 HasAndroidInjector를 구현 하게 됨&lt;/li&gt;
&lt;li&gt;매번 AndroidInjection.inject() 호출 == 보일러 플레이트 코드&lt;/li&gt;
&lt;li&gt;이를 구현할 Base 클래스를 작성할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.dagger&lt;/code&gt; 패키지 에서 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.android.AndroidInjector
import dagger.android.DaggerApplication

class App : DaggerApplication() {

    // 애플리케이션 컴포넌트를 반환. 이로써 기존 코드 대체 가능
    override fun applicationInjector(): AndroidInjector&amp;lt;out DaggerApplication&amp;gt; {
        return DaggerAppComponent.factory().create(this)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import android.content.SharedPreferences
import android.os.Bundle
import com.lilcode.hellodagger.R
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var sharedPreferences: SharedPreferences

    @Inject
    lateinit var activityName: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction()
            .replace(R.id.container, MainFragment())
            .commit()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dagger가 지원하는 기본 프레임워크 타입&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DaggerApplication&lt;/li&gt;
&lt;li&gt;DaggerActivity&lt;/li&gt;
&lt;li&gt;DaggerFragment&lt;/li&gt;
&lt;li&gt;DaggerService&lt;/li&gt;
&lt;li&gt;DaggerIntentService&lt;/li&gt;
&lt;li&gt;DaggerBroadcastReceiver&lt;/li&gt;
&lt;li&gt;DaggerContentProvider&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DaggerBroadcastReceiver 사용 시 매니페스트에 브로드캐스트 리시버가 등록되어 있어야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 리시버 인스턴스 생성하는 경우라면 생성자 주입을 사용해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/621&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.08.03 - [Android/클린 아키텍처] - [Clean Architecture] 14-Dagger 서브 컴포넌트, 상속&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/618&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.27 - [Android/클린 아키텍처] - [Clean Architecture] 13-Dagger 멀티 바인딩 하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/617&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.26 - [Android/클린 아키텍처] - [Clean Architecture] 12-바인딩의 종류 (Dagger)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>dagger2</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/622</guid>
      <comments>https://whyprogrammer.tistory.com/622#entry622comment</comments>
      <pubDate>Wed, 4 Aug 2021 00:12:39 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 14-Dagger 서브 컴포넌트, 상속</title>
      <link>https://whyprogrammer.tistory.com/621</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;13. 컴포넌트 간의 의존 관계&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다수의 컴포넌트를 생성 및 상호 의존 관계를 맺을 수도 있음&lt;/li&gt;
&lt;li&gt;방법
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브 컴포넌트&lt;/li&gt;
&lt;li&gt;컴포넌트 상속&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브 컴포넌트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브 컴포넌트 내에 다른 서브컴포넌트 구현&lt;/li&gt;
&lt;li&gt;두 컴포넌트를 연관 짓는 쉬운 방법&lt;/li&gt;
&lt;li&gt;서브 컴포넌트는 상위 컴포넌트에 바인딩된 모든 의존성을 제공 받음 (그 반대는 불가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/NetLSS/AndroidCleanArchitecture/raw/master/img/component.jpeg&quot; alt=&quot;component&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SubComponentB 는 SubComponentA, Component가 가진 모듈로 부터 의존성을 제공 받을 수 있음&lt;/li&gt;
&lt;li&gt;SubComponentA 는 Component 모듈로 부터 의존성을 제공 받을 수 있음&lt;/li&gt;
&lt;li&gt;Component 는 하위 컴포넌트 들이 가진 의존성을 제공 받을 수 없음 ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브 컴포넌트의 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추상 클래스 또는 인터페이스에 &lt;code&gt;@Component&lt;/code&gt; 대신 &lt;code&gt;@Subcomponent&lt;/code&gt; 어노테이션을 붙임&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Subcomponent&lt;/code&gt; 속성 modules에 바인딩 하려는 모듈 클래스들을 추가&lt;/li&gt;
&lt;li&gt;서브 컴포넌트를 만들려면 빌더 또는 팩토리를 반드시 정의 할것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카페에가면.. 커피. 커피를 만들려면.. 에스프레소 머신(커피콩 + 물)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;카페 = 컴포넌트&lt;/li&gt;
&lt;li&gt;에스프레소 머신 = 서브 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Subcomponent

@Subcomponent(modules = [MachineModule::class])
interface MachineComponent {
    fun getCoffee():Coffee
    fun inject(machine: Machine)

    @Subcomponent.Builder
    interface Builder{
        fun setMachineModule(coffeeMachineModule: MachineModule):Builder
        fun build():MachineComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Machine {
    private lateinit var component: MachineComponent

    constructor(builder: MachineComponent.Builder){ // 매개변수로 서브 컴포넌트로부터 빌더를 제공 받음
        component = builder.setMachineModule(MachineModule())
            .build()
        component.inject(this)
    }

    fun extract() = component.getCoffee()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class MachineModule {
    @Provides
    fun provideCoffeeBean() = CoffeeBean()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import javax.inject.Inject

class Coffee @Inject constructor(coffeeBean: CoffeeBean, water: Water) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class CoffeeBean {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Water {
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브 컴포넌트 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브 컴포넌트(MachineComponent)를 정의 했고&lt;/li&gt;
&lt;li&gt;이제 컴포넌트에 서브 컴포넌트로 추가 해야함&lt;/li&gt;
&lt;li&gt;컴포넌트가 가진 &lt;code&gt;@Module&lt;/code&gt;의 멤버인 subcomponent에 서브 컴포넌트 클래스를 추가&lt;/li&gt;
&lt;li&gt;서브 컴포넌트가 연결됬다면, 연결된 컴포넌트 모듈로부터 서브 컴포넌트의 빌더를 요청할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import javax.inject.Inject

class Cafe {
    @Inject
    lateinit var coffeeMachine: Machine

    init{
        DaggerCafeComponent.create().inject(this)
    }

    fun orderCoffee() = coffeeMachine.extract()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [CafeModule::class]) // CafeModule로부터 Machine을 제공 받음
interface CafeComponent {
    fun inject(cafe: Cafe)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module(subcomponents = [MachineComponent::class])
class CafeModule {
    @Provides
    fun provideWater() = Water()

    @Provides
    fun provideMachine(builder: MachineComponent.Builder) = Machine(builder)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커피 주문 테스트 코드&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CoffeeUnitTest {
    @Test
    fun test_cafe(){
        val cafe = Cafe()
        println(cafe.orderCoffee())
        println(cafe.orderCoffee())
        println(cafe.orderCoffee())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;com.lilcode.hellodagger.subComponent.Coffee@72a9989
com.lilcode.hellodagger.subComponent.Coffee@4a0fd396
com.lilcode.hellodagger.subComponent.Coffee@32cd3972&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브 컴포넌트의 특징&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/NetLSS/AndroidCleanArchitecture/raw/master/img/cafe_component.jpeg&quot; alt=&quot;cafe component&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;머신-component 는 카페-component의 일부분&lt;/li&gt;
&lt;li&gt;머신-component는 카페-component 가 가진 카페-module 로부터 모듈이 가진 의존성을 제공 받을 수 있음&lt;/li&gt;
&lt;li&gt;하지만, 카페-component는 머신-component.builder를 제외하고는 서브 컴포넌트가 가진 모듈의 의존성을 제공 받을 수 없음.&lt;/li&gt;
&lt;li&gt;서브 컴포넌트는 독립적인 생명주기를 가짐&lt;/li&gt;
&lt;li&gt;컴포넌트가 존재하는 동안 서브 컴포넌트는 생성과 소멸을 반복할 수 있음.&lt;/li&gt;
&lt;li&gt;컴포넌트 소멸시 서브 컴포넌트도 같이 소멸&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트의 상속&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오브젝트 그래프 내의 하위 그래프를 작성하는 게 가장 간단하지만,&lt;/li&gt;
&lt;li&gt;서브 컴포넌트는 부모 컴포넌트와 밀접하여 분리가 어려움&lt;/li&gt;
&lt;li&gt;그 대안으로 컴포넌트의 상속이 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt; 의 &lt;code&gt;dependencies&lt;/code&gt; 속성에 상속 컴포넌트 클래스를 추가&lt;/li&gt;
&lt;li&gt;상속한 컴포넌트의 의존성 사용을 위해 상속한 컴포넌트는 해당 의존성을 프로비전 메서드로 제공해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 예시를 구현해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ComponentA]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [ModuleA::class])
interface ComponentA {
    fun provideString(): String // 프로비전 메서드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ModuleA]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class ModuleA {
    @Provides
    fun provideString() = &quot;String from ModuleA&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ComponentB]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(
    modules = [ModuleB::class],
    dependencies = [ComponentA::class]
)
interface ComponentB {
    fun inject(foo: Foo)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ModuleB]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class ModuleB {
    @Provides
    fun provideInteger() = 100
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Foo]&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import javax.inject.Inject
import javax.inject.Named

class Foo {
    @Inject
    lateinit var str: String

    @set: [Inject Named(&quot;int&quot;)]
    var int: Int? = null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import org.junit.Test

class SubComponentInheritanceTest {
    @Test
    fun test_subComponentInheritance(){
        val foo = Foo()

        val componentA = DaggerComponentA.create()
        val componentB = DaggerComponentB.builder()
            .componentA(componentA)
            .build()
        componentB.inject(foo)

        println(foo.str) // String from ModuleA
        println(foo.int) // 100
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;컴포넌트B&lt;/code&gt; 가 &lt;code&gt;컴포넌트A&lt;/code&gt; 를 상속하여 &lt;code&gt;컴포넌트A&lt;/code&gt;의 프로비전 메서드로 부터 String 타입 의존성을 제공 받을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/618&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.27 - [Android/클린 아키텍처] - [Clean Architecture] 13-Dagger 멀티 바인딩 하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/617&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.26 - [Android/클린 아키텍처] - [Clean Architecture] 12-바인딩의 종류 (Dagger)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/615&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.25 - [Android/클린 아키텍처] - [Clean Architecture] 11-범위 지정하기 (@Scope)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/621</guid>
      <comments>https://whyprogrammer.tistory.com/621#entry621comment</comments>
      <pubDate>Tue, 3 Aug 2021 15:45:06 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin</title>
      <link>https://whyprogrammer.tistory.com/619</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 안드로이드 뮤직 플레이어 앱 만들기 with ExoPlayer, Kotlin&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 멜론 같은 안드로이드 뮤직 재생 앱을 만들어 보는 프로젝트를 진행해보았습니다. 서버 에서 음악 정보들을 받아와 재생 목록을 구성하고 재생 및 일시정지, 다음 곡, 이전 곡, 곡 위치 탐색 등의 기본적인 기능을 수행할 수 있도록 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음악 서버에서 음악 받아와 재생 목록 구성하기&lt;/li&gt;
&lt;li&gt;현재 재생 중인 음악 재생 목록에서 표시 (회색 배경)&lt;/li&gt;
&lt;li&gt;재생/일시정지, 이전 곡/다음 곡 재생 기능&lt;/li&gt;
&lt;li&gt;seekBar 를 통한 음악 탐색 기능&lt;/li&gt;
&lt;li&gt;재생 목록 &amp;lt;-&amp;gt; 음악 뷰 전환 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용 기술&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exoplayer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커스텀 컨트롤러&lt;/li&gt;
&lt;li&gt;playlist&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;androidx.constraintlayout.widget.Group&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SeekBar Custom&lt;/li&gt;
&lt;li&gt;postDelayed, TimeUnit&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과 화면&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brmKcd/btraNyfKC30/IyV5AzOI6PzyqhPtJMZBW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brmKcd/btraNyfKC30/IyV5AzOI6PzyqhPtJMZBW0/img.png&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;2296&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brmKcd/btraNyfKC30/IyV5AzOI6PzyqhPtJMZBW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrmKcd%2FbtraNyfKC30%2FIyV5AzOI6PzyqhPtJMZBW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1368&quot; height=&quot;2296&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muUVN/btraVTpxsGJ/1IM7rUSKD3Y46ZvFl5a7Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muUVN/btraVTpxsGJ/1IM7rUSKD3Y46ZvFl5a7Bk/img.png&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;2296&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muUVN/btraVTpxsGJ/1IM7rUSKD3Y46ZvFl5a7Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuUVN%2FbtraVTpxsGJ%2F1IM7rUSKD3Y46ZvFl5a7Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1368&quot; height=&quot;2296&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;최종 앱 스크린샷&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 레이아웃 구성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;189&quot; data-origin-height=&quot;85&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doobke/btraWvaTQmE/sTdFveXuBczqnpn4iYVxXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doobke/btraWvaTQmE/sTdFveXuBczqnpn4iYVxXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doobke/btraWvaTQmE/sTdFveXuBczqnpn4iYVxXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdoobke%2FbtraWvaTQmE%2FsTdFveXuBczqnpn4iYVxXk%2Fimg.png&quot; data-origin-width=&quot;189&quot; data-origin-height=&quot;85&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용된 레이아웃 자원은 메인 화면, 플레이어 프레그먼트 그리고 리사이클러 뷰 홀더에 사용 될 음악 아이템 레이아웃 이렇게 3개로 정의해두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[activity_main.xml]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
...&amp;gt;

    &amp;lt;FrameLayout
        android:id=&quot;@+id/fragment_container&quot;
        ... /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 레이아웃은 player_fragment 를 담을 FrameLayout 으로 가득차게 구성을 했는데 미래에 재사용성을 위해 이런 구성을 사용했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;753&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNrJO5/btraMqCCI9h/FT40MXJMhAhy6jDuK4NBDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNrJO5/btraMqCCI9h/FT40MXJMhAhy6jDuK4NBDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNrJO5/btraMqCCI9h/FT40MXJMhAhy6jDuK4NBDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNrJO5%2FbtraMqCCI9h%2FFT40MXJMhAhy6jDuK4NBDK%2Fimg.png&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;753&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[fragment_player.xml : 뒷 배경 구성 하기]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;    &amp;lt;View
        android:id=&quot;@+id/top_background_view&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;0dp&quot;
        android:background=&quot;@color/background&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/bottom_background_view&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintVertical_weight=&quot;3&quot; /&amp;gt;

    &amp;lt;View
        android:id=&quot;@+id/bottom_background_view&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;0dp&quot;
        android:background=&quot;@color/white&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/top_background_view&quot;
        app:layout_constraintVertical_weight=&quot;2&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 플레이어 프래그먼트입니다. 나눠진 배경의 경우 view 2개로 구성하여 위에 회색, 아래 흰색으로 &lt;code&gt;layout_constraintVertical_weight&lt;/code&gt; 속성을 사용해 3:2 비율로 수직으로 채워지도록 설정해주어 위와 같이 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[fragment_player.xml : Group 사용 해보기]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;    &amp;lt;androidx.constraintlayout.widget.Group
        android:id=&quot;@+id/player_view_group&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:constraint_referenced_ids=&quot;track_text_view, artist_text_view,
        cover_image_card_view, bottom_background_view,
        player_seek_bar, play_time_text_view, total_time_text_view&quot;
        tools:visibility=&quot;visible&quot;
        android:visibility=&quot;gone&quot;/&amp;gt;


    &amp;lt;androidx.constraintlayout.widget.Group
        android:id=&quot;@+id/play_list_group&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:constraint_referenced_ids=&quot;play_list_recycler_view, title_text_view, play_list_seek_bar&quot;
        android:visibility=&quot;visible&quot;
        /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재생화면과 플레이어 뷰를 따로 나누지 않고 한 프레그먼트 안에서 그룹으로 묶어 플레이 리스트 전환 아이콘을 터치 할 때 관련된 뷰들만 보여주도록 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[fragment_player.xml : 앨범 커버 이미지 CardView]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;    &amp;lt;androidx.cardview.widget.CardView
        android:id=&quot;@+id/cover_image_card_view&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;0dp&quot;
        android:layout_marginStart=&quot;36dp&quot;
        android:layout_marginEnd=&quot;36dp&quot;
        android:translationY=&quot;30dp&quot;
        app:cardCornerRadius=&quot;5dp&quot;
        app:cardElevation=&quot;10dp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@id/top_background_view&quot;
        app:layout_constraintDimensionRatio=&quot;H,1:1&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;&amp;gt;

        &amp;lt;ImageView
            android:id=&quot;@+id/cover_image_view&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            tools:background=&quot;@color/purple_200&quot; /&amp;gt;
    &amp;lt;/androidx.cardview.widget.CardView&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앨범 커버 이미지 영역을 위해 카드뷰를 사용하여 둥근 모양, 바닥에서 띄워진 그림자 효과 등을 이용할 수 있습니다. 다음 속성으로 &lt;code&gt;cardElevation&lt;/code&gt; 바닥에서 띄워서 그림자 생성, &lt;code&gt;cardCornerRadius&lt;/code&gt; 둥근 모양 형성이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[fragment_player.xml : exoplayer2]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;    &amp;lt;com.google.android.exoplayer2.ui.PlayerView
        android:id=&quot;@+id/player_view&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;100dp&quot;
        android:alpha=&quot;0&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:use_controller=&quot;false&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑소 플레이어의 경우에는 컨트롤러를 사용할 수 있지만, 해당 프로젝트에서는 따로 커스텀 버튼을 만들어서 사용하도록 해주었습니다. (사실 컨트롤러를 사용하면 더욱 쉽게 구현이 가능할 수 있습니다.)&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외로는 seekBar, 버튼(재생, 다음/이전 곡), 리사이클러 뷰에 사용될 아이템 레이아웃 등을 구현을 해주었습니다. (전체 코드는 저의 &lt;a href=&quot;https://github.com/NetLSS/aop-p4c02-MusicStreaming/tree/master/app/src/main/res/layout&quot;&gt;깃허브 저장소&lt;/a&gt;에서 확인가능합니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. Main activity&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment를 상속한 PlayerFragment를 구현한 뒤 메인 엑티비티의 프래그먼트 컨테이너에 할당해주어 뮤직 플레이어 프레그먼트가 최초 실행 시 보이도록 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity() {

    private var _viewBinding: ActivityMainBinding? = null
    private val viewBinding: ActivityMainBinding get() = requireNotNull(_viewBinding)

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

        supportFragmentManager.beginTransaction()
            .replace(viewBinding.fragmentContainer.id, PlayerFragment.newInstance())
            .commit()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;requireNotNull()&lt;/code&gt;을 사용해서 nullable이 제거된 바인딩을 사용할 수 있도록 get()을 가진 프로퍼티를 하나 정의해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[PlayerFragment.kt]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class PlayerFragment : Fragment(R.layout.fragment_player) {

    // 코드 생략...

  companion object {
        fun newInstance(): PlayerFragment {
            return PlayerFragment()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlayerFragment를 직접 생성해서 사용할 수도 있지만, companion object로 newInstance 를 만드는 정적 매서드를 하나 추가해서 새로운 인스턴스를 공급 받는 형태로 구현해주었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. RecyclerView (item_layout, Adapter, Model)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리사이클러뷰는 현재 까지 제 블로그에 올려진 앱들을 만들어 보면서 활용을 많이 하였기에 넘어갈 수도 있지만, 그래도 한 번 간단하게 집고 가도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;item_music.xml&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰홀더에 사용될 아이템의 경우 사용 뮤직앱들과 비슷하게 앨범 커버, 타이틀, 아티스트로 이루어진 레이아웃으로 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;116&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBaiDe/btraP815OC7/zXg138flTmFGIRrfCmDsL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBaiDe/btraP815OC7/zXg138flTmFGIRrfCmDsL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBaiDe/btraP815OC7/zXg138flTmFGIRrfCmDsL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBaiDe%2FbtraP815OC7%2FzXg138flTmFGIRrfCmDsL0%2Fimg.png&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;116&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Model (data class)&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class MusicModel (
    val id: Long,
    val track: String,
    val streamUrl: String,
    val artist: String,
    val coverUrl: String,
    val isPlaying: Boolean = false
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델으로는 곡의 정보를 담고 있는 data class를 하나 정의해주었습니다. isPlaying 이라는 불 변수를 하나 추가해 현재 재생되고 있는지 상태를 나타내주었습니다. (이후 해당 상태로 아이템 뷰의 배경색을 결정합니다.)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RecyclerView Adapter&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MusicAdapter(private val callback: (MusicModel) -&amp;gt; Unit): ListAdapter&amp;lt;MusicModel, MusicAdapter.ViewHolder&amp;gt;(diffUtil) {

    inner class ViewHolder(private val itemMusicBinding: ItemMusicBinding):RecyclerView.ViewHolder(itemMusicBinding.root){
        fun bind(music: MusicModel){
            itemMusicBinding.itemArtistTextView.text = music.artist
            itemMusicBinding.itemTrackTextView.text = music.track

            Glide.with(itemMusicBinding.itemCoverImageView.context)
                .load(music.coverUrl)
                .into(itemMusicBinding.itemCoverImageView)

            // 재생 중에 따라
            if(music.isPlaying){
                // itemView 를 사용했는데 이건 리사이클러 뷰에서 뷰홀더(아이템 하나) 현재 아이템에 해당
                itemView.setBackgroundColor(Color.GRAY) // 재생중이면 배경 색을 회색
            }else{
                itemView.setBackgroundColor(Color.TRANSPARENT)
            }

            itemView.setOnClickListener {
                callback(music)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemMusicBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

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

    companion object{
        val diffUtil = object : DiffUtil.ItemCallback&amp;lt;MusicModel&amp;gt;(){
            override fun areItemsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean { // id 값만 비교
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean { // 내부 내용 비교
                return oldItem == newItem
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뎁터는 리스트 어뎁터를 상속해 구현해주었고 생성자로 콜백함수를 받아 아이템 클릭시 해당 콜백을 실행하도록 구현해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앨범 커버 이미지 로드는 Glide 라이브러리를 활용해 구현해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 홀더의 경우 inner class로 정의해주었고 안드로이드에서 권장하는 뷰 바인딩을 사용해주었습니다. &lt;code&gt;RecyclerView.ViewHolder&lt;/code&gt;를 상속해 처음 생성 시 사용 뷰를 넘겨주어야 하기 때문에 바인딩의 루트 뷰를 넘겨주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;diffUtil을 통해서 아이템의 동일 여부 및 내용의 동일 여부를 판단하는데 해당 프로젝트에서는 이를 잘 활용하는데 이후 재생 되는 곡이 변경 될 때 isPlaying 이 변경된 리스트를 갱신하는데 새로운 객체를 할당해 옳바르게 재생 중인 음악이 표시되도록 &lt;code&gt;data class&lt;/code&gt;의 &lt;code&gt;copy()&lt;/code&gt;를 활용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. SeekBar customizing&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FuWXL/btraRwIFeAE/Z2lMDGSF9Vr5p3SUXlolvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FuWXL/btraRwIFeAE/Z2lMDGSF9Vr5p3SUXlolvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FuWXL/btraRwIFeAE/Z2lMDGSF9Vr5p3SUXlolvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFuWXL%2FbtraRwIFeAE%2FZ2lMDGSF9Vr5p3SUXlolvK%2Fimg.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;64&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;    &amp;lt;SeekBar
        android:id=&quot;@+id/player_seek_bar&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginStart=&quot;50dp&quot;
        android:layout_marginEnd=&quot;50dp&quot;
        android:layout_marginBottom=&quot;30dp&quot;
        android:maxHeight=&quot;4dp&quot;
        android:minHeight=&quot;4dp&quot;
        android:paddingStart=&quot;0dp&quot;
        android:paddingEnd=&quot;0dp&quot;
        android:progressDrawable=&quot;@drawable/player_seek_background&quot;
        android:thumb=&quot;@drawable/player_seek_thumb&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/player_view&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        tools:progress=&quot;40&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;progressDrawable&lt;/code&gt;으로 프로그레스 표시를 커스터마이징 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;thumb&lt;/code&gt;으로 thumb를 커스터마이징 할 수 있습니다. (thumb는 음악 탐색 시 끌거나 당기거나 하는 그 부분(?) 입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[player_seek_background.xml]&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;layer-list xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&amp;gt;
    &amp;lt;item android:id=&quot;@android:id/background&quot;&amp;gt; &amp;lt;!-- 재정의 --&amp;gt;
        &amp;lt;shape&amp;gt;
            &amp;lt;corners android:radius=&quot;2dp&quot; /&amp;gt;
            &amp;lt;solid android:color=&quot;@color/seek_background&quot; /&amp;gt;
        &amp;lt;/shape&amp;gt;
    &amp;lt;/item&amp;gt;

    &amp;lt;item android:id=&quot;@android:id/progress&quot;&amp;gt;
        &amp;lt;clip&amp;gt;
            &amp;lt;shape&amp;gt;
                &amp;lt;corners android:radius=&quot;2dp&quot; /&amp;gt;
                &amp;lt;stroke
                    android:width=&quot;2dp&quot;
                    android:color=&quot;@color/purple_200&quot; /&amp;gt;
                &amp;lt;solid android:color=&quot;@color/purple_200&quot; /&amp;gt;
            &amp;lt;/shape&amp;gt;
        &amp;lt;/clip&amp;gt;
    &amp;lt;/item&amp;gt;
&amp;lt;/layer-list&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;background, progress를 재정의 해준 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[player_seek_thumb.xml]&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;shape xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:shape=&quot;rectangle&quot;&amp;gt;
    &amp;lt;solid android:color=&quot;@color/purple_200&quot; /&amp;gt;
    &amp;lt;size
        android:width=&quot;4dp&quot;
        android:height=&quot;4dp&quot; /&amp;gt;

&amp;lt;/shape&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thumb를 재정의 해준 코드입니다. 다 기본적으로 보라색 한 줄 처럼(?) 표시되도록 하기 위해 위와 같이 정의해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이 리스트에서 표시할 하단 seekbar도 비슷하게 &lt;code&gt;progressTint&lt;/code&gt;, &lt;code&gt;thumbTint&lt;/code&gt;을 보라색을 주었고 &lt;code&gt;clickable=&quot;false&quot;&lt;/code&gt;을 주어 클릭할 수 없게 만들었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 음악 서버 (저작권 없는 음반, mocky)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악의 경우에는 저작권에 민감하기 때문에 저작권이 없는 음악을 사용하였고 서버를 따로 구축하기 보다는 mocky를 사용하여 음악 정보 리스트를 가진 json 을 받아 이를 통해 음악 플레이리스트를 구성해주도록 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. Retrofit 사용 하기 (Dto, Entity, Service)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit 또한 마찬가지로 기본적인 서버 데이터를 받아올 때 많이 사용했었습니다! (이전 &lt;a href=&quot;https://whyprogrammer.tistory.com/category/Android/App&quot;&gt;앱 만들기 프로젝트들&lt;/a&gt; 참고) 핵심 코드만 보고 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[MusicDto]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class MusicDto (
    val musics: List&amp;lt;MusicEntity&amp;gt; // 서버에서 받아올 데이터
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[MusicEntity]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;data class MusicEntity(
    @SerializedName(&quot;track&quot;)val track: String,
    @SerializedName(&quot;streamUrl&quot;)val streamUrl: String,
    @SerializedName(&quot;artist&quot;)val artist: String,
    @SerializedName(&quot;coverUrl&quot;)val coverUrl: String
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성명이 동일하지만 @SerializedName 이 사용될 수도 있다라는 것을 알고가면 좋기 때문에 사용해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[MusicService]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface MusicService {
    @GET(&quot;/v3/e4db045a-23a9-4b49-a3fc-78cf51f3f964&quot;)
    fun listMusics(): Call&amp;lt;MusicDto&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[PlayerFragment.kt : 서버에서 데이터 가져오기]&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    private fun getVideoListFromServer() {
        val retrofit = Retrofit.Builder()
            .baseUrl(&quot;https://run.mocky.io/&quot;)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(MusicService::class.java)
            .also {
                it.listMusics()
                    .enqueue(object : Callback&amp;lt;MusicDto&amp;gt; {
                        override fun onResponse(
                            call: Call&amp;lt;MusicDto&amp;gt;,
                            response: Response&amp;lt;MusicDto&amp;gt;
                        ) {
                            response.body()?.let { musicDto -&amp;gt;
                                model = musicDto.mapper()

                                setMusicList(model.getAdapterModels())
                                adapter.submitList(model.getAdapterModels())
                            }
                        }

                        override fun onFailure(call: Call&amp;lt;MusicDto&amp;gt;, t: Throwable) {

                        }

                    })
            }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 url 을 지정, gson 컨버터 사용하도록 하여 Retrofit을 빌드하고 &lt;code&gt;MusicService&lt;/code&gt;를 사용해 생성 및 응답 처리를 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;code&gt;response.body()&lt;/code&gt;에서 &lt;code&gt;musicDto&lt;/code&gt;를 받아오는데 mapper를 이용하여 바로 모델로 바꾸어 사용할 수 있도록 해주었습니다. 아래는 사용된 맵퍼 함수 입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun MusicEntity.mapper(id: Long): MusicModel =
    MusicModel(id = id, track, streamUrl, artist, coverUrl)

fun MusicDto.mapper(): PlayerModel =
    PlayerModel(
        playMusicList = musics.mapIndexed { index, musicEntity -&amp;gt;
            musicEntity.mapper(index.toLong())
        }
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터에 id만 새로 갱신해서 넣어주도록 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Exoplayer 사용 해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exoplayer는 이전 &lt;a href=&quot;https://whyprogrammer.tistory.com/603&quot;&gt;유튜브 앱 만들어 보기 글&lt;/a&gt; 에서 한 번 다루어 본 적이 있습니다. 이번에는 &lt;code&gt;SimpleExoPlayer&lt;/code&gt;를 사용해 보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;private var player: SimpleExoPlayer? = null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleExoPlayer 형식의 player 라는 변수를 하나 정의하고 null을 할당합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;        context?.let {
            player = SimpleExoPlayer.Builder(it).build()
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시 &lt;code&gt;context&lt;/code&gt; 가 필요하기 때문에 &lt;code&gt;context&lt;/code&gt;를 널 체크 해준뒤 이를 사용해서 빌드해줍니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;binding.playerView.player = player&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃에서 playerView 로 지정해준 아이디 (엑소 플레이어의 ui playerview)에 빌드해준 player를 대입해줍니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미디어 아이템 설정하기 : &lt;code&gt;addMediaItems&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    private fun setMusicList(modelList: List&amp;lt;MusicModel&amp;gt;) {
        player ?: return
        context?.let {
            player?.addMediaItems(modelList.map { musicModel -&amp;gt;
                MediaItem.Builder()
                    .setMediaId(musicModel.id.toString()) // 미디어 아이디를 musicModel id로
                    .setUri(musicModel.streamUrl)
                    .build()
                /*
                미디어 아이템에 2가지 태그 지정 가능
                미디어 id, 뷰에 태그 지정했듯 미디어 아이템에 태그 지정 가능
                 */
            })

            player?.prepare()
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 프로젝트 에서는 Exoplayer를 사용할 때 재생할 동영상을 하나씩 설정해서 재생해주었는데 이번 뮤직 앱 프로젝트에서는 &lt;code&gt;MediaItems&lt;/code&gt;를 추가해서 마치 플레이리스트를 가진 플레이어를 사용하는 것 처럼 사용해주기 위해 &lt;code&gt;addMediaItems&lt;/code&gt;로 미디어 아이템을 추가해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 서버에서 음악 정보를 받아오면 이를 통해 MusicModel 리스트를 받아서 이를 player의 미디어 아이템으로 추가해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 MediaId로 뮤직 모델의 아이디를 설정해서 이후 구분 가능하게 사용할 수 있도록 설정해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setUri로 음악이 재생될 주소를 설정해주고 빌드하면 미디어 아이템이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 플레이어를 prepare를 통해 준비해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. Listener 추가 해보기&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;player?.addListener(object : Player.EventListener {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어에는 &lt;code&gt;addListener&lt;/code&gt;를 통해서 &lt;code&gt;Player.EventListener&lt;/code&gt; 를 구현하는 익명 객체를 넣어주면 쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 필요한 이벤트 리스너를 재정의 해서 구현해주면 되겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;플레이어가 재생 또는 일시 정지 될 때 : &lt;code&gt;onIsPlayingChanged&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;            override fun onIsPlayingChanged(isPlaying: Boolean) {
                super.onIsPlayingChanged(isPlaying)

                if (isPlaying) {
                    binding.playControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
                } else {
                    binding.playControlImageView.setImageResource(R.drawable.ic_baseline_play_arrow_24)
                }
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어가 재생 또는 일시정지 상태가 되면 재생/일시정지 버튼의 아이콘을 알맞게 보여주도록 해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미디어 아이템이 바뀔 때 : &lt;code&gt;onMediaItemTransition&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;            override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                super.onMediaItemTransition(mediaItem, reason)

                val newIndex: String = mediaItem?.mediaId ?: return
                model.currentPosition = newIndex.toInt()
                adapter.submitList(model.getAdapterModels())

                // 리사이클러 뷰 스크롤 이동
                binding.playListRecyclerView.scrollToPosition(model.currentPosition)

                updatePlayerView(model.currentMusicModel())
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;onMediaItemTransition&lt;/code&gt; 을 재정의 하여 미디어 아이템이 바뀔 때 마다 아이디로 지정했던 mediaId를 가져와서 리사이클러 뷰를 갱신(재생하고 있는 곡은 회색 배경으로 전환) 플레이어뷰를 갱신(이미지로드, 제목, 아티스트 텍스트 갱신) 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UX가 자연스러워 질 수 있게 다음/이전 곡 버튼을 통해 곡을 이동하면 리사이클러 뷰의 스크롤 포지션도 변경해서 재생 중인 곡으로 이동하도록 해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;재생, 재생완료, 버퍼링 등의 상태 변화 시 : &lt;code&gt;inPlaybackStateChanged&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;            override fun onPlaybackStateChanged(state: Int) {
                super.onPlaybackStateChanged(state)

                updateSeek()
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재생 중일 때 seekBar의 잔량과 상태를 갱신해주기 위해서 해당 체인지 리스너를 재정의해주었습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    private fun updateSeek() {
        val player = this.player ?: return
        val duration = if (player.duration &amp;gt;= 0) player.duration else 0 // 전체 음악 길이
        val position = player.currentPosition

        updateSeekUi(duration, position)

        val state = player.playbackState

        view?.removeCallbacks(updateSeekRunnable)
        // 재생 중 일때 (재생 중이 아니거나, 재생이 끝나지 않은 경우)
        if (state != Player.STATE_IDLE &amp;amp;&amp;amp; state != Player.STATE_ENDED) {
            view?.postDelayed(updateSeekRunnable, 1000) // 1초에 한번씩 실행
        }

    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;updateSeek&lt;/code&gt; 에서는 음악 길이 정보를 통해서 seekBar ui를 업데이트하고 1초 마다 &lt;code&gt;updateSeekRunnable&lt;/code&gt; 을 실행합니다. &lt;code&gt;updateSeekRunnable&lt;/code&gt;은 단지 &lt;code&gt;updateSeek()&lt;/code&gt;을 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 재생 중이면 해당 1초마다 seekBar를 갱신해서 여타 뮤집앱 처럼 탐색바가 갱신되도록 해주는 것 이지요. 해당 &lt;code&gt;Runnable&lt;/code&gt;이 겹쳐 동작하지 않도록 &lt;code&gt;updateSeek()&lt;/code&gt;을 타면 &lt;code&gt;removeCallbacks&lt;/code&gt;를 통해 기존에 있던 &lt;code&gt;Runnable&lt;/code&gt;을 제거해줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TimeUnit Convert&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    private fun updateSeekUi(duration: Long, position: Long) {
        binding.playListSeekBar.max = (duration / 1000).toInt() // 총 길이를 설정. 1000으로 나눠 작게
        binding.playListSeekBar.progress = (position / 1000).toInt() // 동일하게 1000으로 나눠 작게

        binding.playerSeekBar.max = (duration / 1000).toInt()
        binding.playerSeekBar.progress = (position / 1000).toInt()

        binding.playTimeTextView.text = String.format(
            &quot;%02d:%02d&quot;,
            TimeUnit.MINUTES.convert(position, TimeUnit.MILLISECONDS), // 현재 분
            (position / 1000) % 60 // 분 단위를 제외한 현재 초
        )
        binding.totalTimeTextView.text= String.format(
            &quot;%02d:%02d&quot;,
            TimeUnit.MINUTES.convert(duration, TimeUnit.MILLISECONDS), // 전체 분
            (duration / 1000) % 60 // 분 단위를 제외한 초
        )
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;updateSeekUi()&lt;/code&gt; 에서는 전체 길이와 현재 위치로 식바를 갱신합니다. &lt;code&gt;max&lt;/code&gt; 와 &lt;code&gt;progress&lt;/code&gt; 를 원래 위치 값에 1000을 나눠 너무 큰 값이 되지 않게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 시간을 표시하는 텍스트는 &lt;code&gt;String.format()&lt;/code&gt; 매서드를 통해 남은 시간 총 시간을 분:초 형식으로 0을 채운 2자리수로 나타내주도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;code&gt;java.util&lt;/code&gt;의 &lt;code&gt;TimeUnit&lt;/code&gt;을 사용하여 밀리초를 분으로 변경해주고 남은 초를 계산해서 텍스트로 뿌려줄 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. seekBar로 탐색&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;       binding.playerSeekBar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener{
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
            }

            override fun onStopTrackingTouch(seekBar: SeekBar) {
                player?.seekTo(seekBar.progress * 1000L)
            }

        })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐색 바를 통한 음악 탐색의 경우 &lt;code&gt;setOnSeekBarChangeListener&lt;/code&gt;를 설정하여 터치를 멈출 때 즉 손을 뗄 때 해당 위치의 &lt;code&gt;progress&lt;/code&gt;를 플레이어에 설정해서 구현해줍니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 생명주기를 통한 리소스 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도하지 않은 작업 (백 그라운드로 나갔는데도 계속 재생 등) 및 자원 처리를 위해서 생명주기를 활용합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    override fun onStop() {
        super.onStop()

        player?.pause()
        view?.removeCallbacks(updateSeekRunnable)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 백그라운드로 나가거나 다른 앱을 사용하는 경우 재생을 멈추고 seekBar를 업데이트하는 콜백을 제거합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;    override fun onDestroy() {
        super.onDestroy()

        _binding = null
        player?.release()
        view?.removeCallbacks(updateSeekRunnable)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 완전히 종료되는 경우 바인딩 해제, 플레이어 해제, 콜백 제거를 통한 처리를 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/603&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.13 - [Android/App] - [Android] Youtube 앱 만들어보기 (ExoPlayer, MotionLayout)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android/App</category>
      <category>안드로이드 뮤직 앱</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/619</guid>
      <comments>https://whyprogrammer.tistory.com/619#entry619comment</comments>
      <pubDate>Mon, 2 Aug 2021 11:43:11 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 13-Dagger 멀티 바인딩 하기</title>
      <link>https://whyprogrammer.tistory.com/618</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;12. 멀티 바인딩 하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dagger의 멀티 바인딩을 사용하면 여러 모듈에 있는 같은 타입의 객체를 하나의 set, map 형태로 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Set 멀티 바인딩&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@IntoSet&lt;/code&gt;, &lt;code&gt;@ElementsIntoSet&lt;/code&gt; -&amp;gt; &lt;code&gt;@Provides&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.multibindings.ElementsIntoSet
import dagger.multibindings.IntoSet

@Module
class SetModule {
    @Provides
    @IntoSet
    fun provideHello() = &quot;Hello&quot;

    @Provides
    @IntoSet
    fun provideWorld() = &quot;World&quot;

    @Provides
    @ElementsIntoSet
    fun provideSet() = setOf&amp;lt;String&amp;gt;(&quot;Lee&quot;, &quot;Kim&quot;)// set 일부를 한번에 추가
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [SetModule::class])
interface SetComponent {
    fun inject(foo: Foo)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import javax.inject.Inject

class Foo {
    @Inject
    lateinit var strings: Set&amp;lt;String&amp;gt;

    fun print() {
        for (item in strings) {
            println(item)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_multiBinding(){
        val foo = Foo()
        DaggerSetComponent.create().inject(foo)
        foo.print()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Hello
World
Lee
Kim    &lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Map 멀티 바인딩&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 내부에 &lt;code&gt;@Provides&lt;/code&gt; 메서드에 &lt;code&gt;@IntoMap&lt;/code&gt; 추가&lt;/li&gt;
&lt;li&gt;주의사항 : Map 사용 시 Key 필요하므로. 별도 키 어노테이션이 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 제공 키 종류&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@StringKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ClassKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@IntKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@LongKey&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Foo {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey

@Module
class MapModule {
    companion object {
        @Provides
        @IntoMap
        @StringKey(&quot;foo&quot;)
        fun provideFooValue() = 100L

        @Provides
        @IntoMap
        @ClassKey(Foo::class)
        fun provideFooStr() = &quot;Foo String&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [MapModule::class])
interface MapComponent {
    fun getLongByString(): Map&amp;lt;String, Long&amp;gt;
    fun getStringByClass(): Map&amp;lt;Class&amp;lt;*&amp;gt;, String&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_multiBindingMap(){
        val component = DaggerMapComponent.create()
        val value = component.getLongByString()[&quot;foo&quot;]
        val str = component.getStringByClass()[Foo::class.java]

        println(value)
        println(str)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;100
Foo String&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의 키 만들기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@MapKey&lt;/code&gt; 어노테이션 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AnimalKey, NumberKey를 사용하여 멀티 바인드 하는 예제&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum class Animal {
    CAT, DOG
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import dagger.MapKey

@MapKey
annotation class AnimalKey(val value: Animal)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import dagger.MapKey
import kotlin.reflect.KClass


@MapKey
annotation class NumberKey(val value: KClass&amp;lt;out Number&amp;gt;) // Number 클래스의 서브 클래스 타입을 속성으로.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [MapModule::class])
interface MapKeyComponent {
    fun getStringsByAnimal(): Map&amp;lt;Animal, String&amp;gt;
    fun getStringsByNumber(): Map&amp;lt;Class&amp;lt;out Number&amp;gt;, String&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap

@Module
class MapModule {
    @IntoMap
    @AnimalKey(Animal.CAT)
    @Provides
    fun provideCat() = &quot;Meow&quot;

    @IntoMap
    @AnimalKey(Animal.DOG)
    @Provides
    fun provideDog() = &quot;Bow-wow&quot;

    @IntoMap
    @NumberKey(Float::class)
    @Provides
    fun provideFloatValue() = &quot;100f&quot;

    @IntoMap
    @NumberKey(Int::class)
    @Provides
    fun provideIntValue() = &quot;1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_customMapKey(){
        val component = DaggerMapKeyComponent.create()
        val cat = component.getStringsByAnimal()[Animal.CAT]
        val dog = component.getStringsByAnimal()[Animal.DOG]
        val number = component.getStringsByNumber()[Float::class.java]

        println(cat)
        println(dog)
        println(number)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Meow
Bow-wow
100f&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상속된 서브 컴포넌트의 멀티 바인딩&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바인드된 set, map을 서브 컴포넌트도 그대로 물려 받을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 코드&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [ParentModule::class])
interface ParentComponent {
    fun strings():Set&amp;lt;String&amp;gt;
    fun childCompBuilder(): ChildComponent.Builder
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

@Module(subcomponents = [ChildComponent::class])
class ParentModule {
    @Provides
    @IntoSet
    fun string1() = &quot;parent string 1&quot;

    @Provides
    @IntoSet
    fun string2() = &quot;parent string 2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Subcomponent

@Subcomponent(modules = [ChildModule::class])
interface ChildComponent {
    fun strings(): Set&amp;lt;String&amp;gt;

    @Subcomponent.Builder
    interface Builder{
        fun build():ChildComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

@Module
class ChildModule {
    @Provides
    @IntoSet
    fun string3() = &quot;child string 1&quot;

    @Provides @IntoSet
    fun string4() = &quot;child string 2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
    @Test
    fun test_multiBindingSubcomponent(){
        val parentComp = DaggerParentComponent.create()
        val childComp = parentComp.childCompBuilder().build()

        println(&quot;List set in Parent&quot;)
        var itr = parentComp.strings().iterator()
        while(itr.hasNext()){
            println(itr.next())
        }

        println(&quot;List set in Child&quot;)

        itr = childComp.strings().iterator()
        while(itr.hasNext()){
            println(itr.next())
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;    List set in Parent
    parent string 1
    parent string 2
    List set in Child
    child string 2
    child string 1
    parent string 1
    parent string 2&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추상적인 멀티 바인딩 선언하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트는 여러 모듈을 사용할 수 있는데 다른 모듈에 의해서 멀티 바인드를 사용할 수도 있고 안할 수도 있다.&lt;/li&gt;
&lt;li&gt;멀티 바인딩 사용 여부와 상관 없이 &lt;code&gt;@Multibinds&lt;/code&gt; 어노테이션으로 멀티 바인딩 선언 가능&lt;/li&gt;
&lt;li&gt;매개 변수를 갖지 않는 추상 메서드에 사용 가능, 반환 타입은 Map 이나 Set&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [MultibindsModules::class])
interface MultibindsComponent {
    fun getStrings(): Set&amp;lt;String&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
import dagger.Module
import dagger.multibindings.Multibinds

@Module
abstract class MultibindsModules {
    @Multibinds
    abstract fun strings(): Set&amp;lt;String&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_multiBinds(){
        val component = DaggerMultibindsComponent.create()

        // 비어 있음
        for(s in component.getStrings()){
            println(s)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비어있기 때문에 아무 출력 없음&lt;/li&gt;
&lt;li&gt;비어 있는 Set을 멀티 바인딩한 효과와 같아 &lt;code&gt;@ElementsIntoSet&lt;/code&gt; 만 사용해서 멀티 바인딩일 선언할 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
abstract class MultibindsModules {
    companion object{
        @Provides
        @ElementsIntoSet
        fun emptyStrings() = Collections.emptySet&amp;lt;String&amp;gt;()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/617&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.26 - [Android/클린 아키텍처] - [Clean Architecture] 12-바인딩의 종류 (Dagger)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/615&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.25 - [Android/클린 아키텍처] - [Clean Architecture] 11-범위 지정하기 (@Scope)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.24 - [Android/클린 아키텍처] - [Clean Architecture] 10-한정자 (@named)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/618</guid>
      <comments>https://whyprogrammer.tistory.com/618#entry618comment</comments>
      <pubDate>Tue, 27 Jul 2021 20:06:46 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 12-바인딩의 종류 (Dagger)</title>
      <link>https://whyprogrammer.tistory.com/617</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;11.&amp;nbsp; Dagger 바인딩의 종류&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Binds&lt;/code&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 내의 추상 메서드에 붙일 수 있음&lt;/li&gt;
&lt;li&gt;추상 메서드 앞에 붙여 Binding을 위임 하는 annotation&lt;/li&gt;
&lt;li&gt;반드시 하나의 매개 변수만을 가져야 함&lt;/li&gt;
&lt;li&gt;매개 변수를 반환형으로 바인드할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Provides&lt;/code&gt; 메서드 대신 좀 더 효율적으로 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
abstract class MyModuleRandom{
    @Binds
    abstract fun bindRandom(secureRandom: SecureRandom):Random
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Random 객체를 SecureRandom 객체에 바인딩&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Binds&lt;/code&gt;는 객체를 생성하는 대신 Component 내에 있는 객체를 파라미터로 받아 바인딩하여 좀 더 효율적으로 동작하게 해줌&lt;/li&gt;
&lt;li&gt;좀 더 자세한 예제 보러가기 &lt;a href=&quot;https://jaejong.tistory.com/131#@Binds&quot;&gt;(jaejong님의 블로그 글)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@BindsOptionalOf&lt;/code&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 내의 추상 메서드에 붙일 수 있음&lt;/li&gt;
&lt;li&gt;매개 변수를 가질 수 없음&lt;/li&gt;
&lt;li&gt;unit이 아닌 특정 타입을 반환해야함&lt;/li&gt;
&lt;li&gt;예외를 던질 수 없음&lt;/li&gt;
&lt;li&gt;Java Optional 에 대해 구글링해보고 넘어가자&lt;/li&gt;
&lt;li&gt;Optional 은 메소드의 결과가 null이 될 수 있으며, 클라이언트가 이 상황을 처리해야 할 때 사용하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.BindsOptionalOf
import dagger.Module

@Module
abstract class CommonModule {
    @BindsOptionalOf
    abstract fun bindOptionalOfString():String
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class HelloModule {
    @Provides
    fun providesString() = &quot;Hello&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@BindsOptionalOf&lt;/code&gt; 메서드를 통한 의존성의 주입은 다음과 같은 Optional 타입 등으로 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;import dagger.Lazy
import java.util.*
import javax.inject.Inject
import javax.inject.Provider

class Foo {
    @Inject
    lateinit var str: Optional&amp;lt;String&amp;gt; // @Nullable 바인딩은 허용하지 않음

    @Inject
    lateinit var str2: Optional&amp;lt;Provider&amp;lt;String&amp;gt;&amp;gt;

    @Inject
    lateinit var str3: Optional&amp;lt;Lazy&amp;lt;String&amp;gt;&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 내에 Foo가 바인드된 적이 있으면 Optional 상태는 present, 아니라면 absent&lt;/li&gt;
&lt;li&gt;특징: 어떤 타입의 의존성이 바인드되었는지 여부와 관계없이 @Inject를 이용해 주입할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Test를 위한 컴포넌트 2개]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [CommonModule::class, HelloModule::class])
interface StrComponent {
    fun inject(foo: Foo)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [CommonModule::class])
interface NoStrComponent {
    fun inject(foo: Foo)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Test code]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_Foo(){
        val foo = Foo()

        DaggerStrComponent.create().inject(foo)
        println(foo.str.isPresent) // true
        println(foo.str.get()) // Hello

        DaggerNoStrComponent.create().inject(foo)
        println(foo.str.isPresent) // false
        println(foo.str.get()) // java.util.NoSuchElementException: No value present
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음에 생성한 컴포넌트는 string을 바인드 받았기 때문에 &lt;code&gt;isPresent == true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;두번째 생성한 컴포넌트는 바인드 되지 않아서 &lt;code&gt;isPresent == false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;바인드 되지 않은 &lt;code&gt;Optional&amp;lt;String&amp;gt;&lt;/code&gt;을 &lt;code&gt;get()&lt;/code&gt;하면 예외가 발생하는 것을 알 수 있었음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@BindsInstance&lt;/code&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 빌더의 세터 메서드 또는 컴포넌트 팩토리의 매개 변수에 붙일 수 있음&lt;/li&gt;
&lt;li&gt;모듈이 아닌 외부로 부터 생성된 인스턴스를 빌더 또는 팩토리를 통해서 넘겨줌으로써 컴포넌트가 해당 인스턴스를 반인드&lt;/li&gt;
&lt;li&gt;이러한 인스턴스들은 마찬가지로 &lt;code&gt;@Inject&lt;/code&gt;가 붙은 필드, 생성자, 메서드에 주입 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.BindsInstance
import dagger.Component

@Component
interface BindsComponent {
    fun inject(foo: Foo)

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun setString(str: String): Builder
        fun build(): BindsComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import javax.inject.Inject

class Foo {
    @Inject
    lateinit var str: String
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    @Test
    fun test_bindsInstance() {
        val hello = &quot;Hello World&quot;
        val foo = com.lilcode.hellodagger.bindsInstance.Foo()
        val component = DaggerBindsComponent.builder()
            .setString(hello)
            .build()
        component.inject(foo)
        assertEquals(&quot;Hello World&quot;, foo.str)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부로부터 생성한 String 객체를 바인드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/615&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.25 - [Android/클린 아키텍처] - [Clean Architecture] 11-범위 지정하기 (@Scope)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.24 - [Android/클린 아키텍처] - [Clean Architecture] 10-한정자 (@named)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/614&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.23 - [Android/클린 아키텍처] - [Clean Architecture] 09-Lazy 주입 &amp;amp; Provider 주입 (dagger)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/617</guid>
      <comments>https://whyprogrammer.tistory.com/617#entry617comment</comments>
      <pubDate>Mon, 26 Jul 2021 19:04:47 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 11-범위 지정하기 (@Scope)</title>
      <link>https://whyprogrammer.tistory.com/615</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10. 범위 지정하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 컴포넌트에 &lt;code&gt;@Scope&lt;/code&gt; 로 범위 지정 가능&lt;/li&gt;
&lt;li&gt;하나의 인스턴스만 만들어서 참조하는 싱글턴 패턴과 비슷
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트의 인스턴스는 의존성의 제공 방법에 대한 동일 성을 보장 받을 수 있음&lt;/li&gt;
&lt;li&gt;앱 생명주기와는 달리 별도로 생명 주기를 따로 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;example
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드에서 애플리케이션, 엑티비티, 프래그먼트 인스턴스에 때한 범위 지정을 다르게 해서 관리하여 오브젝트 그래프의 생성과 소멸을 각자 관리 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Singleton&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Singleton&lt;/code&gt;을 사용한 범위 지정 및 객체 재사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Singleton
@Component(modules = [MyModule2::class])
interface MyComponent2{
    fun getAny():Any
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
class MyModule2{
    @Provides
    @Singleton
    fun provideAny() = Any()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    @Test
    fun test_anyIdentity(){
        val myComponent = DaggerMyComponent2.create()
        val temp1 = myComponent.getAny()
        val temp2 = myComponent.getAny()
        assertNotNull(temp1)
        assertNotNull(temp2)
        assertSame(temp1, temp2)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Reusable&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 컴포넌트 스코프에 종속되지 않아 컴포넌트에 &lt;code&gt;@Reusable&lt;/code&gt;을 선언하지 않아도 된다&lt;/li&gt;
&lt;li&gt;이전 객체를 재사용할 수 있다면 재사용. 아니면 새로 생성&lt;/li&gt;
&lt;li&gt;인스턴스의 동일성을 보장하지는 않지만, 항상 동일 인스턴스가 필요한게 아니라면 메모리 관리측면에서 좀 더 효율적임&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Scope&lt;/code&gt; 확장하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스코프를 직접 만들어서 컴포넌트의 범위 지정하기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class UserScope{
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
class MyModule2{
    @Provides
    @UserScope
    fun provideAny() = Any()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/616&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.24 - [Android/클린 아키텍처] - [Clean Architecture] 10-한정자 (@named)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/614&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.23 - [Android/클린 아키텍처] - [Clean Architecture] 09-Lazy 주입 &amp;amp; Provider 주입 (dagger)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/613&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.22 - [Android/클린 아키텍처] - [Clean Architecture] 08-Dagger2의 컴포넌트(Component)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>dagger scope</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/615</guid>
      <comments>https://whyprogrammer.tistory.com/615#entry615comment</comments>
      <pubDate>Sun, 25 Jul 2021 20:07:56 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 10-한정자 (@named)</title>
      <link>https://whyprogrammer.tistory.com/616</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;09. 한정자 지정하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환형으로 바인드된 객체를 식별하기 애매할 때&lt;/li&gt;
&lt;li&gt;example: string 타입 2개를 반환 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class MyModuleEx{
    @Provides
    fun provideHello() = &quot;Hello&quot;
    fun provideWorld() = &quot;World&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;string 형식 반환이 2개 있으므로 오류!&lt;/li&gt;
&lt;li&gt;그러나 2개가 필요한 상황이 있을 수 있다.&lt;/li&gt;
&lt;li&gt;이럴때는 &lt;code&gt;javax.inject&lt;/code&gt; package 에서 &lt;code&gt;@Named&lt;/code&gt; 어노테이션을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Inject
import javax.inject.Named&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModuleNamed::class])
interface MyComponentNamed {
    fun inject(myClassNamed: MyClassNamed)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
class MyModuleNamed {
    @Provides
    @Named(&quot;hello&quot;)
    fun provideHello() = &quot;Hello&quot;

    @Provides
    @Named(&quot;world&quot;)
    fun provideWorld() = &quot;World&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;class MyClassNamed {
    @Inject
    @Named(&quot;hello&quot;)
    lateinit var strHello: String

    @Inject
    @Named(&quot;world&quot;)
    lateinit var strWorld: String
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_namedComponent(){
        val myClassNamed = MyClassNamed()
        val component = DaggerMyComponentNamed.create()
        component.inject(myClassNamed)
        println(myClassNamed.strHello + myClassNamed.strWorld) // HelloWorld
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의 한정자 만들기&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Named&lt;/code&gt;가 아닌 고유의 한정자를 만들려면 &lt;code&gt;@Qualifier&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Hello {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModuleNamed::class])
interface MyComponentNamed {
    fun inject(myClassNamed: MyClassNamed)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
class MyModuleNamed {
    @Provides
    @Hello // here 
    fun provideHello() = &quot;Hello&quot;

    @Provides
    fun provideWorld() = &quot;World&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;class MyClassNamed {
    @Inject
    @Hello // here
    lateinit var strHello: String

    @Inject
    lateinit var strWorld: String
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;test&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_namedComponent(){
        val myClassNamed = MyClassNamed()
        val component = DaggerMyComponentNamed.create()
        component.inject(myClassNamed)
        println(myClassNamed.strHello + myClassNamed.strWorld) // HelloWorld
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘 출력되는 것을 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/614&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.23 - [Android/클린 아키텍처] - [Clean Architecture] 09-Lazy 주입 &amp;amp; Provider 주입 (dagger)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/613&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.22 - [Android/클린 아키텍처] - [Clean Architecture] 08-Dagger2의 컴포넌트(Component)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/612&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.21 - [Android/클린 아키텍처] - [Clean Architecture] 07-Dagger2의 모듈(Module)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>named 한정자</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/616</guid>
      <comments>https://whyprogrammer.tistory.com/616#entry616comment</comments>
      <pubDate>Sat, 24 Jul 2021 20:11:27 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 09-Lazy 주입 &amp;amp; Provider 주입 (dagger)</title>
      <link>https://whyprogrammer.tistory.com/614</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;08. Lazy 주입과 Provider 주입&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상황에 따른 의존성 주입 시점 늦추기 또는 새로운 객체 요청
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Provider&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lazy 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 초기화되는 데 시간이 필요한 경우&lt;/li&gt;
&lt;li&gt;바인드된 타입 T를 제네릭으로 갖는 &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt;를 만듦. get() 이불리기 전 까지는 초기화 되는것을 늦출 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;import dagger.Component
import dagger.Lazy
import dagger.Module
import dagger.Provides
import javax.inject.Inject&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lazy 를 dagger 말고 다른 Lazy 를 써서 삽질 좀 했네요. import 주의 합시다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cannot be provided without an @Provides-annotated method.&lt;/code&gt; 오류 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class CounterModule {
    var next = 100

    @Provides
    fun provideInteger(): Int {
        println(&quot;computing...&quot;)
        return next++
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [CounterModule::class])
interface CounterComponent {
    fun inject(counter: Counter)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Counter {
    @Inject
    lateinit var lazy: Lazy&amp;lt;Int&amp;gt;

    fun printLazy() {
        println(&quot;printing...&quot;)
        println(lazy.get())
        println(lazy.get())
        println(lazy.get())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    @Test
    fun test_lazy(){
        val componentA = DaggerCounterComponent.create()
        val counter = Counter()
        componentA.inject(counter)
        counter.printLazy()

        val componentB = DaggerCounterComponent.create()
        componentB.inject(counter)
        counter.printLazy()

        componentA.inject(counter)
        counter.printLazy()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;result&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;    printing...
    computing...
    100
    100
    100
    printing...
    computing...
    100
    100
    100
    printing...
    computing...
    101
    101
    101&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Provider 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 새로운 인스턴스를 주입받고 싶을 때&lt;/li&gt;
&lt;li&gt;바인드된 타입 T를 제네릭으로 갖는 &lt;code&gt;Provider&amp;lt;T&amp;gt;&lt;/code&gt;, get() 메서드 호출 시 마다 새로운 개체 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import javax.inject.Provider

class Counter {
//    @Inject
//    lateinit var lazy: Lazy&amp;lt;Int&amp;gt;

    @Inject
    lateinit var provider:Provider&amp;lt;Int&amp;gt; // Provider 사용

    fun printLazy() {
        println(&quot;printing...&quot;)
        println(provider.get())
        println(provider.get())
        println(provider.get())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;    @Test
    fun test_provider(){
        val component = DaggerCounterComponent.create()
        val counter = Counter()
        component.inject(counter)
        counter.printLazy()

    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;    printing...
    computing...
    100
    computing...
    101
    computing...
    102&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Provider.get() 호출 시 새로운 객체를 생성&lt;/li&gt;
&lt;li&gt;컴포넌트가 @Singleton 과 같은 특정 범위로 지정되어 있다면, Provider 를 사용 한다고 해도 바인드된 의존성은 싱글턴으로 관리되어 같은 인스턴스를 제공 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/613&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.22 - [Android/클린 아키텍처] - [Clean Architecture] 08-Dagger2의 컴포넌트(Component)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/612&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.21 - [Android/클린 아키텍처] - [Clean Architecture] 07-Dagger2의 모듈(Module)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/611&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.20 - [Android/클린 아키텍처] - [Clean Architecture] 06-Dagger2 란? (+ Adnroid 적용 샘플)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>dagger</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/614</guid>
      <comments>https://whyprogrammer.tistory.com/614#entry614comment</comments>
      <pubDate>Fri, 23 Jul 2021 12:56:28 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 08-Dagger2의 컴포넌트(Component)</title>
      <link>https://whyprogrammer.tistory.com/613</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;07. 컴포넌트&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바인딩된 모듈로 부터 오브젝트 그래프를 생성하는 핵심적인 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt; 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;interface&lt;/code&gt;, &lt;code&gt;abstract class&lt;/code&gt; 에만 붙일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴파일 타임에 'Dagger', &lt;code&gt;@Component&lt;/code&gt;가 붙은 클래스 이름이 합쳐진 형식의 이름을 가짐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt;가 갖는 속성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;modules&lt;/code&gt; : 컴포넌트에 바인드되는 @Module 이 지정된 클래스 배열
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈이 다른 모듈을 포함한 경우 해당 모듈까지 컴포넌트에 구현해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependencies&lt;/code&gt; : 컴포넌트에 다른 컴포넌트의 의존성을 사용하는 경우 클래스 배열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오브젝트 크래프&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger에서는 컴포넌트, 모듈, 객체 등의 관계를 컨테이너 or (오브젝트)그래프 라고 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Hello World 예제의 그래프]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;1000&quot; data-filename=&quot;IMG_1D053030D574-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgOjM1/btq91mTV5hl/GENlX0Hti1jKrbW4pJesJk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgOjM1/btq91mTV5hl/GENlX0Hti1jKrbW4pJesJk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgOjM1/btq91mTV5hl/GENlX0Hti1jKrbW4pJesJk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgOjM1%2Fbtq91mTV5hl%2FGENlX0Hti1jKrbW4pJesJk%2Fimg.jpg&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;1000&quot; data-filename=&quot;IMG_1D053030D574-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 매서드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt;가 붙은 모든 타입이 하나 이상 가져야 하는 메서드&lt;/li&gt;
&lt;li&gt;메서드 이름은 상관없으나 반환 타입 규칙은 무조건 따라야 한다.&lt;/li&gt;
&lt;li&gt;프로비전 메서드와 멤버-인젝션 메서드로 구분된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로비전 메서드(Provision Methods)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger의 컴포넌트에서 매개변수가 없고, 반환형이 모듈로 부터 제공 또는 주입되는 메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class SomeModule {
    @Provides
    fun provideSomeType(): SomeType = SomeType()
}

class SomeType {}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [SomeModule::class])
interface SomeComponent {
    fun getSomeType(): SomeType // SomeModule 로 부터 제공 또는 주입 받은 SomeType 객체를 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;멤버-인젝션 메서드(Member-injection methods)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger의 컴포넌트에서 하나의 매개 변수를 갖는 메서드.&lt;/li&gt;
&lt;li&gt;멤버-인젝션 메서드는 unit을 반환 또는 빌더 패턴 처럼 메서드 체이닝 가능한 메서드를 만들기 위해 매개 변수 타입을 반환 형으로 갖느 메서드로 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐 빌더 패턴이 뭔지 알아보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌더 패턴(Builder pattern)이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;var customer: Member = Member.build()
    .name(&quot;홍길동&quot;)
    .age(30)
    .build()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멤버-인젝션 메서드 예시&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Component(modules = [SomeModule::class])
interface SomeComponent {
  fun injectSomeType(someType: SomeType)
  fun injectAndReturnSomeType(someType: SomeType): SomeType
}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;실제로 테스트 해보기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class MyClass {
    var str: String? = null
    @Inject set
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class MyModule {
    @Provides
    fun provideHelloWorld(): String = &quot;Hello World&quot;

    @Provides
    fun provideInt(): Int = 1234

    @Provides
    fun providePerson(name: String, age: Int): Person = Person(name, age)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModule::class])
interface MyComponent {
    fun getString(): String
    fun getInt(): Int
    fun getPerson(): Person

    fun inject(myClass: MyClass) // 멤버-인젝션 메서드
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class ExampleUnitTest {
    @Test
    fun test_memberInjection(){
        val myClass = MyClass()
        var str = myClass.str
        assertNull(&quot;조회 결과 null&quot;, str)

        val myComponent = DaggerMyComponent.create()
        myComponent.inject(myClass)
        str = myClass.str
        assertEquals(&quot;Hello World&quot;, str)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;myComponent.inject(myClass)&lt;/code&gt; 전에는 null 이었다가 이후에 &quot;Hello World&quot;가 주입된 것을 알 수 있다.&lt;/li&gt;
&lt;li&gt;매개 변수 없이 &lt;code&gt;MemberInjector&amp;lt;T&amp;gt;&lt;/code&gt;를 반환할 수도 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환된 &lt;code&gt;MemberInjector&lt;/code&gt; 객체의 &lt;code&gt;injectmembers(T)&lt;/code&gt; 메서드를 호출하면 멤버-인잭션 메서드와 동일 작업을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModule::class])
interface MyComponent {
    ...
    fun getInjector():MembersInjector&amp;lt;MyClass&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class ExampleUnitTest {
    @Test
    fun tet_memberInjector(){
        val myClass = MyClass()
        var str = myClass.str
        println(&quot;result = $str&quot;) // null

        val myComponent = DaggerMyComponent.create()
        val injector: MembersInjector&amp;lt;MyClass&amp;gt; = myComponent.getInjector()
        injector.injectMembers(myClass)
        str = myClass.str
        println(&quot;result = $str&quot;) // Hello World
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 주입하기&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger 3가지 의존성 주입 방법
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드 주입&lt;/li&gt;
&lt;li&gt;생성자 주입&lt;/li&gt;
&lt;li&gt;메서드 주입&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Inject&lt;/code&gt; 어노테이션이 붙은 필드, 메서드 그리고 생성자에 인스턴스를 주입&lt;/li&gt;
&lt;li&gt;실무에서는 주로 필드, 생성자 주입 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[PersonModule.kt] : 이름과 나이를 제공&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class PersonModule {

    @Provides
    fun provideName(): String = &quot;SangSuLee&quot;

    @Provides
    fun provideAge(): Int = 100
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[PersonComponent.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [PersonModule::class])
interface PersonComponent {

    fun getPersonA():PersonA // 프로비전 매서드 (PersonA 객체를 제공)

    fun inject(personB: PersonB) (PersonB에 멤버-인젝션)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[PersonA.kt]&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;//생성자 주입
class PersonA @Inject constructor(val name: String, val age: Int)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[PersonB.kt]&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;class PersonB {
    @Inject // 필드 주입
    var name: String? = null

    @set:Inject // 메서드 주입 (setter)
    var age = 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class ExampleUnitTest {
    @Test
    fun test_Injection(){
        val personComponent = DaggerPersonComponent.create()

        val personA = personComponent.getPersonA()
        println(&quot;${personA.name} : ${personA.age}&quot;) // SangSuLee : 100 

        val personB = PersonB()
        DaggerPersonComponent.create().inject(personB)
        assertEquals(&quot;SangSuLee&quot;, personB.name)

        assertEquals(100, personB.age)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상속된 클래스에 의존성 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멤버-인젝션 메서드를 호출할 때&lt;/li&gt;
&lt;li&gt;매개 변수 타입에 서브 클래스의 객체를 넣으면&lt;/li&gt;
&lt;li&gt;해당 슈퍼 클래스의 &lt;code&gt;@Inject&lt;/code&gt; 멤버만 의존성 주입이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;open class Parent {
    var a: A? = null
        @Inject set
}

open class Self : Parent() {
    var b: B? = null
        @Inject set
}

class Child : Self() {

    var c: C? = null
        @Inject set
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시로&lt;/li&gt;
&lt;li&gt;컴포넌트에 멤버-인젝션 메서드인 &lt;code&gt;fun inject(Self)&lt;/code&gt;가 존재하고&lt;/li&gt;
&lt;li&gt;Child 의 인스턴스를 멤버-인젝션 메서드의 매개변수로 참조하여 호출하면&lt;/li&gt;
&lt;li&gt;Child의 인스턴스 에는 a 하고 b 만 주입되며 c에는 주입되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 객체 만들기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성된 빌더 또는 팩토리를 통해 만들 수 있음&lt;/li&gt;
&lt;li&gt;컴포넌트 내의 &lt;code&gt;@Component.Builder&lt;/code&gt; 또는 &lt;code&gt;@Component.Factory&lt;/code&gt; 타입 선언을 통해 빌더 혹은 팩토리 생성&lt;/li&gt;
&lt;li&gt;빌더, 팩토리 어노테이션이 둘 다 없으면 &lt;code&gt;@Component&lt;/code&gt; 어노테이션에 선언된 모듈 및 의존성을 참조하여 빌더를 자동으로 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModule::class])
interface MyComponent {
    ...
    @Component.Builder
    interface Builder{
        fun setMyModule(myModule: MyModule): Builder
        fun build():MyComponent
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컴포넌트 빌더를 만드는 조건&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Component.Builder&lt;/code&gt; 어노테이션은 컴포넌트 타입 내에 선언되어야 함&lt;/li&gt;
&lt;li&gt;반드시 매개변수 가없고 컴포넌트 타입 또는 컴포넌트의 슈퍼 타입을 반환하는 추상 메서드를 하나 포함 해야함 (이를 빌드 메서드 라고 함)&lt;/li&gt;
&lt;li&gt;빌드 메서드를 제외한 나머지는 세터 메서드(setter methods)라고 함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt; 어노테이션에 modules, dependencies로 선언된 요소들은 세터 메서드로 선언 해야함&lt;/li&gt;
&lt;li&gt;세터 메서드는 반드시 하나의 매개 변수만 가져야 하며, 반환 형으로는 unit, 빌더 또는 빌더의 슈퍼 타입이 될 수 있음&lt;/li&gt;
&lt;li&gt;세터 메서드에 &lt;code&gt;@BindsInstance&lt;/code&gt;를 붙이면, 해당 컴포넌트에 인스턴스를 넘겨 바인드 시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[위 조건을 만족하는 컴포넌트 빌더의 예시]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [BackendModule::class, FrontendModule::class])
interface ExampleComponent{
    fun myWidget(): MyWidget

    @Component.Builder
    interface Builder {
        fun backendModule(bm: BackendModule): Builder
        fun frontendModule(fm: FrontendModule): Builder

        @BindsInstance
        fun foo(foo: Foo): Builder)
        fun build(): MyComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컴포넌트 팩토리를 만드는 조건&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Component.Factory&lt;/code&gt; 어노테이션은 컴포넌트 타입 내에 선언되어야 한다.&lt;/li&gt;
&lt;li&gt;컴포넌트 팩토리는 컴포넌트 타입 또는 컴포넌트의 슈퍼 타입을 반환하는 하나의 추상 메서드만 존재해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex. &lt;code&gt;newInstance()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;팩토리 메서드에는 &lt;code&gt;@Component&lt;/code&gt; 어노테이션에 &lt;code&gt;modules&lt;/code&gt;, &lt;code&gt;dependencies&lt;/code&gt;로 지정된 속성들을 반드시 매개변수로 가져야 한다.&lt;/li&gt;
&lt;li&gt;메서드에 &lt;code&gt;@BindsInstance&lt;/code&gt; 어노테이션이 붙은 매개 변수는 해당 컴포넌트에 인스턴스를 넘겨 바인드 시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [BackendModule::class, FrontendModule::class])
interface ExampleComponent {
    fun myWidget(): MyWidget

    @Component.Factory
    interface Factory {
        fun newMyComponent(
            bm: BackendModule,
            fm: FrontendModule,
            @BindsInstance foo: Foo
        ): MyComponent
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성되는 컴포넌트 형식에는 factory() 라는 정적 메서드를 갖게됨&lt;/li&gt;
&lt;li&gt;팩토리 인스턴스를 반환, 이 팩토리 인스턴스로 컴포넌트를 초기화할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1626694209633&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;NetLSS/AndroidCleanArchitecture&quot; data-og-description=&quot;안드로이드 클린 아키텍처 (Dagger2, RxJava, JetPack ...). Contribute to NetLSS/AndroidCleanArchitecture development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; data-og-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cJxzm9/hyKVdmOLK9/obQXig04CHIgp5dyxD8wq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cJxzm9/hyKVdmOLK9/obQXig04CHIgp5dyxD8wq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NetLSS/AndroidCleanArchitecture&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 클린 아키텍처 (Dagger2, RxJava, JetPack ...). Contribute to NetLSS/AndroidCleanArchitecture development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/612&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.21 - [Android/클린 아키텍처] - [Clean Architecture] 07-Dagger2의 모듈(Module)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/611&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.20 - [Android/클린 아키텍처] - [Clean Architecture] 06-Dagger2 란? (+ Adnroid 적용 샘플)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/610&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 05-의존성 주입(DI)과 그 필요성&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>dagger2 component</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/613</guid>
      <comments>https://whyprogrammer.tistory.com/613#entry613comment</comments>
      <pubDate>Thu, 22 Jul 2021 21:30:57 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 07-Dagger2의 모듈(Module)</title>
      <link>https://whyprogrammer.tistory.com/612</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;06. 모듈&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트에 의존 성을 제공하는 역할 (&lt;code&gt;@Module&lt;/code&gt; 이 붙은 클래스)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로바이더&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 클래스 내 메서드에 &lt;code&gt;@Provides&lt;/code&gt; 어노테이션을 붙여서 컴파일 타임에 의존성을 제공하는 바인드된 프로바이드를 만들 수 있다&lt;/li&gt;
&lt;li&gt;반환 타입을 보게되는데 이때 컴포넌트 내에 중복되는 반환타입이 있으면 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class MyModule {
    @Provides
    fun provideHelloWorld(): String = &quot;Hello World&quot;

    @Provides
    fun provideSSLee(): String = &quot;Lee Sang Su&quot; // error!
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또는 컴포넌트에 바인드된 메서드의 반환형은 &lt;code&gt;@Provides&lt;/code&gt; 메서드의 매개변수로 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class MyModule {
//    @Provides
//    fun provideHelloWorld(): String = &quot;Hello World&quot;

    @Provides
    fun provideName():String = &quot;SangSuLee&quot;

    @Provides
    fun provideInt():Int = 1234

    @Provides // name, age 를 제공 받음 (위)
    fun providePerson(name: String, age: Int): Person = Person(name, age)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;providePerson()&lt;/code&gt;은 그 위에서 String, int 타입을 제공 받는다.&lt;/li&gt;
&lt;li&gt;매개 변수 타입에 맞는 의존성이 컴포넌트와 컴포넌트와 바인드된 모듈에 없으면 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModule::class])
interface MyComponent {
    fun getString(): String // 프로비전 메서드, 바인드된 모듈로부터 의존성을 제공
    fun getInt(): Int
    fun getPerson(): Person
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 클래스가 추상 클래스인 경우 @Provides 메서드는 static 메서드 이어야함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
abstract class MyModule{
    companion object{
        @Provides
        fun provideName(): String {
            return &quot;SangSuLee&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Null의 비허용&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Provides&lt;/code&gt; 메서드는 null을 반환하는 것을 기본적으로 제한.&lt;/li&gt;
&lt;li&gt;null 반환을 명시적으로 허용할 경우 &lt;code&gt;@Nullable&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 제공하는 부분과, 주입받는 쪽이 둘다 &lt;code&gt;@Nullable&lt;/code&gt; 이어야함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module
class MyModule {
//    @Provides
//    fun provideHelloWorld(): String = &quot;Hello World&quot;

    @Provides
    fun provideName():String = &quot;SangSuLee&quot;

    @Provides
    @Nullable
    fun provideInt(): Int? = 1234

    @Provides
    fun providePerson(name: String, age: Int): Person = Person(name, age)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component(modules = [MyModule::class])
interface MyComponent {
    fun getString(): String // 프로비전 메서드, 바인드된 모듈로부터 의존성을 제공
    @Nullable
    fun getInt(): Int
    fun getPerson(): Person
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Provides&lt;/code&gt; 매서드 뿐만 아니라 멤버-인젝션 메서드를 써서 null을 주입하는 경우에도 멤버 변수에 &lt;code&gt;@Nullable&lt;/code&gt;이 꼭 붙어야함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈의 상속&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Module&lt;/code&gt;이 가질 수 있는 속성 중 &lt;code&gt;includes&lt;/code&gt;라는 것이 있는데&lt;/li&gt;
&lt;li&gt;&lt;code&gt;includes&lt;/code&gt;에 다른 모듈 클래스들의 배열을 정의하는 것만으로도 &lt;code&gt;@Provides&lt;/code&gt; 매서드의 상속이 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[ModuleB가 BoduleA를 상속하는 코드]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module
class ModuleA {
    @Provides
    fun provideA(): A = A()
}

class A{}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Module(includes = [ModuleA::class])
class ModuleB {
    @Provides
    fun provideB() = B()
}

class B{}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 ModuleB를 참조하는 경우 ModuleA를 상속해 A 타입 객체도 바인딩 된다.&lt;/li&gt;
&lt;li&gt;주의점 : 모듈 간 상속 시 중복 타입이 존재하면 안된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주의해야 보일러 플레이트 코드를 많이 제거 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;보일러 플레이트 코드 : 상용구 코드, 변경 없이 재사용 가능한 코드 or 비슷한 형태로 반복되는 코드&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/611&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.20 - [Android/클린 아키텍처] - [Clean Architecture] 06-Dagger2 란? (+ Adnroid 적용 샘플)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/610&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 05-의존성 주입(DI)과 그 필요성&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/609&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 04-안드로이드 앱 설계 패턴 (MVC, MVP, MVVM)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>dagger2 module</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/612</guid>
      <comments>https://whyprogrammer.tistory.com/612#entry612comment</comments>
      <pubDate>Wed, 21 Jul 2021 21:23:57 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 06-Dagger2 란? (+ Adnroid 적용 샘플)</title>
      <link>https://whyprogrammer.tistory.com/611</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Dagger2란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바와 안드로이드를 위한 강력하고 빠른 의존성 주입 프레임워크&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리플렉션을 사용하지 않음 (&lt;code&gt;리플렉션 : 리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들을 접근할 수 있도록 해주는 자바 API&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;런 타임에 바이트 코드도 생성하지 않음&lt;/li&gt;
&lt;li&gt;컴파일 타임에 애노테이션 프로세서에 의해 의존성 주입과 관련된 모든 코드를 분석하고 자바 소스 코드를 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger 전에는 자바 진영에서 Guice를 사용했는데 리플렉션을 사용 및 런 타임에 오브젝트 그래프를 구성하여 성능에 악영향을 끼치는 경우가 많았음&lt;/li&gt;
&lt;li&gt;Guice와 비슷한 API를 가지며 더 빠른 Dagger1(2012) 이후 구글 코어 라이브러리 팀과 함께 Dagger2를 만들어 현재까지 발전&lt;/li&gt;
&lt;li&gt;프로젝트에 적용하기는 쉽지 않음. 가파른 학습 곡선. 중도 포기.&lt;/li&gt;
&lt;li&gt;하나씩 용어, 개념을 익히면 어렵지 않을 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dagger의 장점
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자원 공유의 단순화. 지정된 범위의 생명 주기 내에서 동일 인스턴스 제공&lt;/li&gt;
&lt;li&gt;복잡한 의존성 -&amp;gt; 단순하게. 앱이 커질 수록 많은 의존성을 갖는데 Dagger가 이를 쉽게 제어해줌&lt;/li&gt;
&lt;li&gt;유닛 테스트를 쉽게 도와줌&lt;/li&gt;
&lt;li&gt;자동 코드 생성. 생성된 코드는 명확. 디버깅 가능.&lt;/li&gt;
&lt;li&gt;Dagger2 는 난독화 문제가 ㅇ벗음. (Dagger1은 리플렉션 사용으로 난독화, 성능에 문제..)&lt;/li&gt;
&lt;li&gt;라이브러리 크기가 작음&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 프로젝트에 Dagger 설정하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 프로젝트 모듈 수준 그레이들에 아래 의존성을 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[app/build.gradle]&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;    implementation 'com.google.dagger:dagger-android:2.35.1'
    implementation 'com.google.dagger:dagger-android-support:2.35.1'
    kapt 'com.google.dagger:dagger-android-processor:2.35.1'
    kapt 'com.google.dagger:dagger-compiler:2.35.1'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java 프로젝트라면 &lt;code&gt;kapt&lt;/code&gt;를 &lt;code&gt;annotationProcessor&lt;/code&gt; 으로&lt;/li&gt;
&lt;li&gt;안드로이드 데이터 바인딩 라이브러리 사용시 바인딩 오류가 나면 javac에 의해 오류 메세지가 출력됨. Dagger와 같이 사용하면 Dagger 관련 오류 발생 시 데이터 바인딩 컴파일이 멈추며 오류 메세지가 100줄 이상 뜨는데 이를 더 확인하기 위해 아래와 같은 코드가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;android {
    ...   
    gradle.projectsEvaluated{
        tasks.withType(JavaCompile){
            options.compilerArgs &amp;lt;&amp;lt; &quot;-Xmaxerrs&quot;&amp;lt;&amp;lt; &quot;500&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. 첫 번째 의존성 주입 구현하기&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;Hello World&quot; 문자열 주입 예제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[MyModule.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Module
import dagger.Provides

@Module
class MyModule {
    @Provides
    fun provideHelloWorld(): String = &quot;Hello World&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열을 제공할 모듈 클래스&lt;/li&gt;
&lt;li&gt;Dagger는 컴파일 타임의 의존서 주입에 필요한 어노테이션을 읽어서 의존성 주입에 필요한 클래스 파일들을 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Module&lt;/code&gt; : 의존성을 제공하는 클래스에 붙여준다. (이후 포스팅에서 더 자세히 학습해볼 것임)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Provides&lt;/code&gt; : 의존성을 제공하는 매서드에 붙여준다. (이후 포스팅에서 더 자세히 학습해볼 것임)&lt;/li&gt;
&lt;li&gt;사실, MyModule 클래스 하나만으로는 별도 클래스 파일이 생기지않으며. 모듈을 참조하는 컴포넌트가 있어야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[MyComponent.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import dagger.Component

@Component(modules = [MyModule::class])
interface MyComponent {
    fun getString(): String // 프로비전 메서드, 바인드된 모듈로부터 의존성을 제공
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제공할 의존성에 대한 메서드를 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt;에 참조된 모듈 클래스로부터 의존성을 제공받는다.&lt;/li&gt;
&lt;li&gt;컴포넌트 메서드의 봔환 타입을 보고 모듈과 관계를 맺으므로 바인드된 모듈로 부터 해당 반환형을 갖는 메서드를 못찾으면 컴파일 타임 에러 발생.&lt;/li&gt;
&lt;li&gt;Dagger 가 생성한 클래스는 Dagger 라는 접두사가 붙는다. (&lt;code&gt;@Component&lt;/code&gt; 를 구현한 클래스 생성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 해보자 (참고로 Dagger 클래스가 빨간줄로 표기된다면 빌드를 한번 해주면 된다.)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class ExampleUnitTest {
    @Test
    fun test_helloWorld(){
       val myComponent = DaggerMyComponent.create()
        println(&quot;result = ${myComponent.getString()}&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;result = Hello World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1626667479077&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;NetLSS/AndroidCleanArchitecture&quot; data-og-description=&quot;안드로이드 클린 아키텍처 (Dagger2, RxJava, JetPack ...). Contribute to NetLSS/AndroidCleanArchitecture development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; data-og-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cJxzm9/hyKVdmOLK9/obQXig04CHIgp5dyxD8wq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/NetLSS/AndroidCleanArchitecture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cJxzm9/hyKVdmOLK9/obQXig04CHIgp5dyxD8wq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NetLSS/AndroidCleanArchitecture&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 클린 아키텍처 (Dagger2, RxJava, JetPack ...). Contribute to NetLSS/AndroidCleanArchitecture development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/610&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 05-의존성 주입(DI)과 그 필요성&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/609&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 04-안드로이드 앱 설계 패턴 (MVC, MVP, MVVM)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/608&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.17 - [Android/클린 아키텍처] - [Clean Architecture] 03-안드로이드 권장 애플리케이션 설계 원칙&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>Dagger2 란</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/611</guid>
      <comments>https://whyprogrammer.tistory.com/611#entry611comment</comments>
      <pubDate>Tue, 20 Jul 2021 13:54:51 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 05-의존성 주입(DI)과 그 필요성</title>
      <link>https://whyprogrammer.tistory.com/610</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 의존성 주입이란?&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 주입(DI, Dependency Injection)&lt;/li&gt;
&lt;li&gt;하나의 객체의 다른 객체의 의존성을 제공하는 기술&lt;/li&gt;
&lt;li&gt;의존성과 주입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 : 객체 지향에서 두 클래스 간의 관계, 일반적으로 둘중 하나가 다른 하나를 필요로함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CPU

class Computer {

    private val cpu: CPU

    init {
        cpu = CPU()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드를 보면&lt;/li&gt;
&lt;li&gt;컴퓨터를 생성할 떄 CPU가 강하게 결합한다.&lt;/li&gt;
&lt;li&gt;다른 CPU로 변경이 불가능 -&amp;gt; 'Computer가 CPU에 의존성을 갖는다'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주입 : 생성자나 메서드를 통해 외부로 부터 생성자 객체를 전달 받는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CPU

class Computer {
    private var cpu: CPU? = null
    fun setCPU(cpu: CPU) {
        this.cpu = cpu
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드는 &lt;code&gt;setCPU()&lt;/code&gt;로 부터 외부에서 생성된 객체를 받아온다&lt;/li&gt;
&lt;li&gt;즉, 의존 관계에 있는 클래스의 객체를 외부로 부터 생성하여 주입받음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 의존성 주입의 필요성&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 주입 꼭 해야하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경의 전이&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(앞서 보인 예제 코드에서)cpu를 변경하고 싶은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Computer {
    private val cpu: A_CPU

    init {
        cpu = A_CPU()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU를 의존하고 있던 Computer 클래스의 코드 수정이 필요하게된다...&lt;/li&gt;
&lt;li&gt;하나의 클래스 변경으로 다른 의존 관계 까지 변경사항이 전달됨&lt;/li&gt;
&lt;li&gt;이를 위해 CPU를 인터페이스로 만든다면&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface CPU

class A_CPU : CPU

class Computer {
    private val cpu: CPU

    init {
        cpu = A_CPU()
        //cpu = new I_CPU()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이래도 의존성이 생긴다&lt;/li&gt;
&lt;li&gt;Computer 에서 CPU를 생성하기 때문에 Computer 클래스를 어쩔수없이 변경해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제어의 역전(IoC, Inversion of Control)&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 일을 수행하도록 만들어진 프레임워크에 제어권을 위임 -&amp;gt; 관심사를 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Computer {
    private var cpu: CPU? = null

    constructor() {}
    constructor(cpu: CPU) {
        this.cpu = cpu
    }

    fun setCPU(cpu: CPU) {
        this.cpu = cpu
    }
}

fun main() {
    val cpu = I_CPU()
    val computer1 = Computer(cpu)
    val computer2 = Computer()
    computer2.setCPU(cpu)
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부로 부터 CPU를 생성해서 주입시키고 있음&lt;/li&gt;
&lt;li&gt;결합도를 낮췄다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 주입의 장단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 주입은 인터페이스를 기반으로 설계되며, 코드를 유연하게 함&lt;/li&gt;
&lt;li&gt;주입하는 코드만 따로 변경하기 쉬워. 리팩토링 수월&lt;/li&gt;
&lt;li&gt;의존성 주입을 사용하는 결과로 stub 또는 mock 객체를 사용해서 단위 테스트 하기 더 쉬워짐 &lt;/li&gt;
&lt;li&gt;클래스 간 결합도를 느슨하게&lt;/li&gt;
&lt;li&gt;인터페이스 기반이므로 여러 개발자가 서로 사용하는 클래스를 독립적으로 개발 가능 (클래스간 의존하는 인터페이스만 알면됨)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;단점도 있다
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 프로그램을 만들때는 번거롭다.&lt;/li&gt;
&lt;li&gt;동작과 구성을 분리하게 되면서 코드 추적이 어려워지고 가독성을 떨어뜨릴 수 있음 (더 많은 파일을 와리가리 해야함)&lt;/li&gt;
&lt;li&gt;Dagger2와 같은 의존성 주입 프레임워크는 컴파일 타임에 애노테이션 프로세서를 이용하여 파일을 생성하므로 빌드에 시간 소요가 좀 늘어난다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지속적인 유지보수가 필요한 일반적인 상용 앱의 경우에는 장점이 더욱 많다.&lt;/li&gt;
&lt;li&gt;코드 가독이 어려워지는 것은 적응하면 그래도 수월해질 수 있음.  &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/609&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.19 - [Android/클린 아키텍처] - [Clean Architecture] 04-안드로이드 앱 설계 패턴 (MVC, MVP, MVVM)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/608&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.17 - [Android/클린 아키텍처] - [Clean Architecture] 03-안드로이드 권장 애플리케이션 설계 원칙&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/607&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.16 - [Android/클린 아키텍처] - [Clean Architecture] 02-안드로이드 클린 아키텍처&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>의존성 주입의 필요성</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/610</guid>
      <comments>https://whyprogrammer.tistory.com/610#entry610comment</comments>
      <pubDate>Mon, 19 Jul 2021 12:45:49 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 04-안드로이드 앱 설계 패턴 (MVC, MVP, MVVM)</title>
      <link>https://whyprogrammer.tistory.com/609</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 안드로이드 애플리케이션 설계 패턴 (MVC,&amp;nbsp;MVP,&amp;nbsp;MVVM)&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 MVC, MVP, MVVM 디자인 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.1. MVC 디자인 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model, View, Controller 로 관심사 분리&lt;/li&gt;
&lt;li&gt;안드로이드 플랫폼 등장 초기에 자연스럽게 적용되기 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모델의 역할 &lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션의 비즈니스 로직, 사용 되는 데이터를 다룸&lt;/li&gt;
&lt;li&gt;표현 형식에 의존적이지 않고, 사용자에게 보이지 않아 어떻게 보일지 신경쓰지 않아도됨&lt;/li&gt;
&lt;li&gt;비즈니스 데이터 = DBMS에 의해서 관리&lt;/li&gt;
&lt;li&gt;몇 함수를 통해서 데이터를 제공, 입력, 수정&lt;/li&gt;
&lt;li&gt;안드로이드에서 데이터베이스의 Entity 를 담당하는 POJO 클래스를 포함한 SQLite, Room, Realm 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[POJO 클래스 예시]&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class Employee(
    var name: String, var id: String,
    val salary: Double
)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;뷰의 역할&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자에게 보여지는 영역&lt;/li&gt;
&lt;li&gt;모델로 부터 얻은 데이터를 뷰에서 나타냄&lt;/li&gt;
&lt;li&gt;안드로이드에서 Activity, Fragment 가 그 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨트롤러의 역할&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델과 뷰에 의존&lt;/li&gt;
&lt;li&gt;뷰로 부터 입력 받거나, 특정 이벤트 발생 시 모델 또는 뷰를 변경&lt;/li&gt;
&lt;li&gt;새로운 데이터가 입력되는 모델로 전달하여 데이터베이스에 저장하고 그 동시에 모델의 상태 변화를 감지하여 등록된 뷰에 상태를 업데이트 해서 보여줄 수 있는것.&lt;/li&gt;
&lt;li&gt;MVC 에서는 Activity 와 Fragment는 뷰의 역할도 하고 컨트롤러의 역할도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1000&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu4h2Z/btq90mltB6u/qBzAaERaEz4maAG8tp9bo1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu4h2Z/btq90mltB6u/qBzAaERaEz4maAG8tp9bo1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu4h2Z/btq90mltB6u/qBzAaERaEz4maAG8tp9bo1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu4h2Z%2Fbtq90mltB6u%2FqBzAaERaEz4maAG8tp9bo1%2Fimg.jpg&quot; width=&quot;100%&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1000&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러는 뷰와 모델에 의존&lt;/li&gt;
&lt;li&gt;뷰는 모댈 상태 변화에 따라 능동적인 대응을 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MVC 디자인 패턴의 장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직관적&lt;/li&gt;
&lt;li&gt;규모가 적은 앱에 적용시 개발 기간 단축가능&lt;/li&gt;
&lt;li&gt;모든 코드가 액티비티, 프래그먼트 같은 컨트롤러에 작성되는 경향 -&amp;gt; 코드 파악 편리&lt;/li&gt;
&lt;li&gt;코드량 증가 시 스파케티 코드로 전락.. -&amp;gt; 시간 흐를 수록 유지보수 비용 증가!&lt;/li&gt;
&lt;li&gt;컨트롤러가 뷰와 모델에 의존, 뷰는 모델에 의존. -&amp;gt; 결합도 높아..! -&amp;gt; 유닛 테스트 거의 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.2 MVP 디자인 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVC 는 UI와 비즈니스 로직이 Activity 와 Fragment에 공존&lt;/li&gt;
&lt;li&gt;MVP 디자인 패턴은 비슷하지만 Activity 와 Fragment의 UI , 비즈니스 로직을 분리하는데 집중&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1000&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y13tA/btq9Wpv4dRG/oOPHp53uVBWiktYhiAMXqK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y13tA/btq9Wpv4dRG/oOPHp53uVBWiktYhiAMXqK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y13tA/btq9Wpv4dRG/oOPHp53uVBWiktYhiAMXqK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy13tA%2Fbtq9Wpv4dRG%2FoOPHp53uVBWiktYhiAMXqK%2Fimg.jpg&quot; width=&quot;100%&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;1000&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;model 과 view 의 역할은 MPC 와 비슷허나&lt;/li&gt;
&lt;li&gt;controller 대시 presenter 라는 개념 사용&lt;/li&gt;
&lt;li&gt;UI 와 비즈니스 로직 분리 -&amp;gt; 유닛 테스트 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MVP 디자인 패턴 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presenter 는 View 와 Model의 인스턴스를 가지면서 이 둘을 연결해주는 역할.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presenter 와 View 는 1:1 관계를 갖음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;View model 간의 의존성이 없음 (장점)&lt;/li&gt;
&lt;li&gt;UI / 비즈니스 로직 분리 (유닛 테스트 수월)&lt;/li&gt;
&lt;li&gt;하지만, View / Presenter 간의 의존성이 높고 1:1 관계를 유지해야 하므로 Presenter 를 재사용할 수 없음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;view 가 늘어날 때마다 Presenter 도 같이 늘어남. 클래스가 많아지게 된다...&lt;/li&gt;
&lt;li&gt;앱의 기능이 추가될 수록 Presenter가 거대해짐...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MVP 디자인 패턴 구현하기&lt;/h4&gt;
&lt;h5&gt;Contract Class 만들기&lt;/h5&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구성 요소의 혁할과 관계의 정의&lt;/li&gt;
&lt;li&gt;구성 요소 = View, Presenter 정도&lt;/li&gt;
&lt;li&gt;Model = Contract 클래스에 포함 또는 Repository 패턴으로 따로 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MainContract {
    interface View {
        fun showPersonList(personList: List&amp;lt;Person&amp;gt;)
        fun notifyDataChanged()
    }

    interface Presenter {
        fun load()
        fun addPerson(person: Person)
        fun removePerson(person: Person)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Presenter Class 만들기&lt;/h5&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View 의 역할
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View 인터페이스에 정의된 매서드 재정의로 데이터를 화면에 뿌림&lt;/li&gt;
&lt;li&gt;Presenter 생명 주기 또는 Click event에 대한 내용 알림&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;View 인터페이스는 Activity, Fragment 에 주로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MainPresenter(database: Database, view: MainContract.View) : Presenter {
    private val database: Database
    private val view: MainContract.View // 오로지 계약된 MainContract.Presenter 에만 참조 된다
    override fun load() {
        view.showPersonList(database.getPersonList())
    }

    override fun addPerson(person: Person) {
        database.add(person)
    }

    override fun removePerson(person: Person) {
        database.remove(person)
    }

    init {
        this.database = database
        this.view = view
        this.database.setOnDatabaseListener(object : DatabaseListener() {
            fun onChanged() {
                this@MainPresenter.view.notifyDataChanged()
            }
        })
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7.3 MVVM 디자인 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVP 패턴애서는 Presenter가 view에 어떤 일을 요청하는지 명백히 확인이 가능했다&lt;/li&gt;
&lt;li&gt;하지만, View 와 Presenter 가 강하게 결합되는 문제점이 있음&lt;/li&gt;
&lt;li&gt;MVVM 에서는 데이터 바인딩 및 LiveData 또는 RxJava 같은 Observable 타입을 이용하여 Presenter와 View 사이 의존력을 끊는데 집중한다.&lt;/li&gt;
&lt;li&gt;Presenter 대신 ViewModel 이라는 구성 요소를 사용!&lt;/li&gt;
&lt;li&gt;ViewModel
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View 에 표현할 데이터를 Observable 타입으로 관리&lt;/li&gt;
&lt;li&gt;View 들이 ViewModel 데이터를 구독! 요청하여 화면에 나타냄&lt;/li&gt;
&lt;li&gt;ViewModel : View = 1 : N 관계를 가진다!  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ViewModel 이 View 에 느슨하게 연결되도록 Data Binding 라이브러리가 필수적으로 사용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Qog9/btq9Y4ZGAnt/xDAQMAxRPQHEZw8KS2aM1k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Qog9/btq9Y4ZGAnt/xDAQMAxRPQHEZw8KS2aM1k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Qog9/btq9Y4ZGAnt/xDAQMAxRPQHEZw8KS2aM1k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Qog9%2Fbtq9Y4ZGAnt%2FxDAQMAxRPQHEZw8KS2aM1k%2Fimg.jpg&quot; width=&quot;100%&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생명 주기 or 유저 상호작용에 따른 ViewModel의 Model 데이터 요청&lt;/li&gt;
&lt;li&gt;Model로부터 받은 데이터를 가공해 관찰가능한(observable) 타입의 형태로 ViewModel에 저장&lt;/li&gt;
&lt;li&gt;View와 ViewModel은 Databinding이 이루어져야함&lt;/li&gt;
&lt;li&gt;데이터 상태가 벼경되면 해당 데이터를 구독하는 view 들에 변경사항을 알리게됨. 그러면 뷰 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ViewModel 구현하기&lt;/h4&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MainViewModel 은 데이터 바인딩 패키지의 BaseObservable을 상속.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터에 반응해서 뷰를 갱신할 수있도록 해준 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;핵심은 MainViewModel이 Person 목록을 가지며, View 관련 코드를 참조하지 않는 다는 것!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[MainViewModel.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
class MainViewModel(database: Database) : BaseObservable() {
    private val database: Database
    private val items: MutableList&amp;lt;Person&amp;gt; = ArrayList&amp;lt;Person&amp;gt;()
    fun load() {
        items.clear()
        items.addAll(database.getPersonList())
        notifyChange()
    }

    fun addPerson(person: Person?) {
        database.add(person)
    }

    fun removePerson(person: Person) {
        database.remove(person)
    }

    fun getItems(): List&amp;lt;Person&amp;gt; {
        return items
    }

    init {
        this.database = database
        this.database.setOnDatabaseListener(object : DatabaseListener() {
            fun onChanged() {
                load()
            }
        })
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[MainActivity.kt]&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MainActivity : AppCompatActivity(), MainViewHolder.HolderClickListener {
    var viewModel: MainViewModel? = null
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        ...
        viewModel = MainViewModel(instance!!)
        binding!!.viewModel = viewModel
        viewModel!!.load()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        viewModel!!.addPerson(
            Person(
                System.currentTimeMillis(),
                String.format(&quot;New Charles %d&quot;, Random().nextInt(1000))
            )
        )
        return super.onOptionsItemSelected(item)
    }

    override fun onDeleteClick(person: Person) {
        viewModel!!.removePerson(person)
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[activity_main.kt]&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;layout&amp;gt;
    &amp;lt;data&amp;gt;
        &amp;lt;variable
            name=&quot;viewModel&quot;
            type=&quot;com.sample.viewmodel.MainViewModel&quot; /&amp;gt;
    &amp;lt;/data&amp;gt;
    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
        xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;&amp;gt;

        &amp;lt;androidx.recyclerview.widget.RecyclerView
            android:id=&quot;@+id/recycler_view&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;0dp&quot;
            app:items=&quot;@{viewModel.items}&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;
&amp;lt;/layout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/606&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.16 - [Android/클린 아키텍처] - [Clean Architecture] 01-안드로이드 애플리케이션 설계 SOLID&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/607&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.16 - [Android/클린 아키텍처] - [Clean Architecture] 02-안드로이드 클린 아키텍처&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whyprogrammer.tistory.com/608&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.07.17 - [Android/클린 아키텍처] - [Clean Architecture] 03-안드로이드 권장 애플리케이션 설계 원칙&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>안드로이드 앱 설계 패턴</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/609</guid>
      <comments>https://whyprogrammer.tistory.com/609#entry609comment</comments>
      <pubDate>Mon, 19 Jul 2021 10:35:25 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 03-안드로이드 권장 애플리케이션 설계 원칙</title>
      <link>https://whyprogrammer.tistory.com/608</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 안드로이드의 특징&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 애플리케이션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 진입점을 가지고, 하나의 프로세스에서 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;안드로이드 애플리케이션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액티비티, 서비스, 브로드케스트 리시버, 콘텐츠 프로바이더를 대표적인 컴포넌트로 구성&lt;/li&gt;
&lt;li&gt;여러 프로세스로 실행가능&lt;/li&gt;
&lt;li&gt;진입점 또한 다양&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;짧은 시간에 여러 어플과 상호 작용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스타로 사진찍고 카카오톡으로 공유하는 도중에 전화가 걸려오면?&lt;/li&gt;
&lt;li&gt;또는 메모리 부족으로 강제 종료되면?&lt;/li&gt;
&lt;li&gt;그래도 사용자는 복귀후 작업을 재개하고 싶을 것이다. (이러한 처리를 잘 해주어야함)&lt;/li&gt;
&lt;li&gt;컴포넌트의 생명 주기는 안드로이드 시스템이 제어권을 가지기 때문에 데이터 등을 컴포넌트에 저장하는 것은 치명적일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 안드로이드 애플리케이션 설계 원칙&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액티비티와 프래그먼트의 클래스 의존성을 최소화하는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 반대의 경우 예외 사항 대응이 어렵고 코드가 거대해져 가독성 저하를 불러올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;관심사를 분리하자!
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 간의 강한 의존성을 느슨하게 만들어서 모듈화를 시켜야한다.&lt;/li&gt;
&lt;li&gt;모듈이란? 다른 모듈로 부터 독립적이고 영역에 따라 다른 역할을 하는 녀석&lt;/li&gt;
&lt;li&gt;설계, 배포, 유닛 테스트 등에서 더욱 높은 자유도를 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;코드가 단순화 되고, 유지 보수 측면에서 더 적은 비용이 들것이다.&lt;/li&gt;
&lt;li&gt;재사용성 증가, 독립적 개발 가능, 생산성 향상!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관심사 분리는 추상화의 일종
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스 사용&lt;/li&gt;
&lt;li&gt;실행에 쓰이는 더 순수한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 권장하는 애플리케이션 설계&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Activity, Fragment 는 단지 ViewModel 만을 참조&lt;/li&gt;
&lt;li&gt;ViewModel 이 어떻게 변경되든간 Activity, Fragment 는 노관심&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel은 Repository라는 저장소를 참조&lt;/li&gt;
&lt;li&gt;UI 컴포넌트가 화면을 구성하는 데 필요한 데이터를 불러옴&lt;/li&gt;
&lt;li&gt;불러온 데이터는 데이터의 변화를 감지할 수 있는 LiveData 로 관리&lt;/li&gt;
&lt;li&gt;저장소
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 모델&lt;/li&gt;
&lt;li&gt;원격 네트워크 모델&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-filename=&quot;IMG_D29E316160E4-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFQKa/btq9R4lfC4O/8t4UlDeqIOn4RAHA7LkSak/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFQKa/btq9R4lfC4O/8t4UlDeqIOn4RAHA7LkSak/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFQKa/btq9R4lfC4O/8t4UlDeqIOn4RAHA7LkSak/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFQKa%2Fbtq9R4lfC4O%2F8t4UlDeqIOn4RAHA7LkSak%2Fimg.jpg&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-filename=&quot;IMG_D29E316160E4-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 모델
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스&lt;/li&gt;
&lt;li&gt;SQLite, SQLite 기반의 Room 또는 Realm&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;원격 모델
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Http 통신 (OkHttp, Retrofit 등 라이브러리) 를 주로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 모델, 원격 모델 로부터 얻은 데이터는 ViewModel이 관리&lt;/li&gt;
&lt;li&gt;변경이 감지되는 순간 UI 컴포넌트의 바인딩된 뷰에 업데이트 됨&lt;/li&gt;
&lt;li&gt;사용자 경험 증진을 위해 일반적으로 서버에서 얻은 데이터는 내부 데이터베이스에 저장해 놓았다가 불러온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;1000&quot; data-filename=&quot;IMG_1C8F806D86AA-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5DFZ/btq9TGxEDHV/N9xq3ZEYYacGfefmyjwJf0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5DFZ/btq9TGxEDHV/N9xq3ZEYYacGfefmyjwJf0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5DFZ/btq9TGxEDHV/N9xq3ZEYYacGfefmyjwJf0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5DFZ%2Fbtq9TGxEDHV%2FN9xq3ZEYYacGfefmyjwJf0%2Fimg.jpg&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;1000&quot; data-filename=&quot;IMG_1C8F806D86AA-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뷰모델은 내부 데이터베이스만 참조하게 되며&lt;/li&gt;
&lt;li&gt;클라이언트 데이터베이스와 서버 데이터베이스가 요청으로 비동기적으로 동기화 되게된다.&lt;/li&gt;
&lt;li&gt;이로써 전파 수신 약전계 에서도 애플리케이션이 원활 동작 가능하다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전파 수신 약전계 : 오프라인 또는 느린 네트웤 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크 상황이 좋아지면 다시 최신 데이터를 요청.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해당&amp;nbsp;글은&amp;nbsp;'아키텍처를&amp;nbsp;알아야&amp;nbsp;앱&amp;nbsp;개발이&amp;nbsp;보인다'&amp;nbsp;를&amp;nbsp;공부하며&amp;nbsp;요약&amp;nbsp;정리한&amp;nbsp;글&amp;nbsp;입니다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>안드로이드 권장 애플리케이션 설계</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/608</guid>
      <comments>https://whyprogrammer.tistory.com/608#entry608comment</comments>
      <pubDate>Sat, 17 Jul 2021 23:31:20 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 02-안드로이드 클린 아키텍처</title>
      <link>https://whyprogrammer.tistory.com/607</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 클린 아키텍처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어의 관심사를 계층별로 분리하는 소프트웨어 디자인 철학&lt;/li&gt;
&lt;li&gt;주요 원칙 : 코드 종속성이 외부로부터 내부로 의존
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 계층 코드는 외부 계층의 기능을 알 수 없음&lt;/li&gt;
&lt;li&gt;외부 계층에 존재하는 변수, 함수, 클래스 등 모든 엔티티는 안쪽 계층에서 다시 등장 불가&lt;/li&gt;
&lt;li&gt;데이터 형식도 계층 간에 별도로 유지하는 것이 좋아&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-filename=&quot;IMG_3BDC775A999C-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wyHIV/btq9I4HA8Vf/CSpOQWIrxnXSUfSA089fR0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wyHIV/btq9I4HA8Vf/CSpOQWIrxnXSUfSA089fR0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wyHIV/btq9I4HA8Vf/CSpOQWIrxnXSUfSA089fR0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwyHIV%2Fbtq9I4HA8Vf%2FCSpOQWIrxnXSUfSA089fR0%2Fimg.jpg&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;999&quot; data-filename=&quot;IMG_3BDC775A999C-1.jpeg&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 추상 적인 영역은 가장 가운데 있는 녀석&lt;/li&gt;
&lt;li&gt;비즈니스 로직을 포함.&lt;/li&gt;
&lt;li&gt;플랫폼, 프레임워크에 의존하면 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 원은 네트워크, 데이터베이스 접근 등 플랫폼에 특정한 구체적 구현 세부사항이 포함됨.&lt;/li&gt;
&lt;li&gt;내부로 갈 수록 추상화 캡슐화 수준이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 계층을 분리. 계층 간의 의존성을 단방향으로 유지. 코드의 재사용성 용이 및 유닛 테스트가 쉬워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Entities&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전사적 비즈니스 규칙 캡슐화&lt;/li&gt;
&lt;li&gt;데이터 구조, 매서드를 포함&lt;/li&gt;
&lt;li&gt;가장 일반적이고 상위 수준의 규칙 캡슐화 할것&lt;/li&gt;
&lt;li&gt;외부에서 변화가 나면 가장 최소한의 변경 사항을 가져야함&lt;/li&gt;
&lt;li&gt;안드로이드 애플리케이션과 관련된 코드를 포함해서는 아니된다. (순수 코틀린 or 자바 코드 사용 권장)&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Use Cases&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션과 관련된 비즈니스 규칙 포함, 시스템의 모든 유스 케이스 구현체 캡슐화&lt;/li&gt;
&lt;li&gt;엔티티로 부터 데이터 흐름 관리&lt;/li&gt;
&lt;li&gt;유즈 케이스 목적 달성을 위해 엔티티에 비즈니스 규칙 사용을 가르침&lt;/li&gt;
&lt;li&gt;엔티티 또는 UI, 프레임워크에 영향을 받지 않음&lt;/li&gt;
&lt;li&gt;안드로이드 에서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model : 데이터베이스 질의, 네트워크 요청 등 비즈니스 로직 수행&lt;/li&gt;
&lt;li&gt;Repository : 내부 DB 접근, 원격 서버 데이터 요청 (interface)&lt;/li&gt;
&lt;li&gt;Executor : Repository, Model 관련 작업을 백그라운드에서 실행되도록 작업 스레드 관리 및 제공&lt;/li&gt;
&lt;li&gt;등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interface Adapters&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유즈케이스, 엔티티로부터 얻은 데이터 가공&lt;/li&gt;
&lt;li&gt;UI에 바인딩&lt;/li&gt;
&lt;li&gt;흔히 말하는 Presenter, View, ViewModel, Controller 같은 관심사가 여기 속함&lt;/li&gt;
&lt;li&gt;UI에서 얻은 데이터를 내부 DB 또는 원격 서버로 올릴 때도 이 계층에서 데이터를 가공하여 전달&lt;/li&gt;
&lt;li&gt;목적 : 비스니스 로직과 프레임워크 코드를 자연스럽게 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Frameworks와 Drivers&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드에서 UI 관련하여 액티비티, 프래그먼트, 인텐트 전달 그리고 데이터에 접근/저장하는 데이터베이스, 콘텐츠 프로바이더가 포함&lt;/li&gt;
&lt;li&gt;Retrofit 같은 네트워크 관련 프레임워크 코드가 여기 속함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관심사를 분리하면 SW가 방해받지 않고 집중해야할 문제에 집중할 수 있음&lt;/li&gt;
&lt;li&gt;SOLID 원칙을 따른 일종의 모범이되는 패턴&lt;/li&gt;
&lt;li&gt;꼭 위처럼 해야하는 것이 정답은 아닐 수 있음. 코딩에 정답은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>클린 아키텍쳐</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/607</guid>
      <comments>https://whyprogrammer.tistory.com/607#entry607comment</comments>
      <pubDate>Fri, 16 Jul 2021 23:14:53 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Architecture] 01-안드로이드 애플리케이션 설계 SOLID</title>
      <link>https://whyprogrammer.tistory.com/606</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 애플리케이션 설계란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구성 요소간 유기적 관계를 표현&lt;/li&gt;
&lt;li&gt;요구사항 해결을 위한 계획 및 과정&lt;/li&gt;
&lt;li&gt;텍스트, 그림, 다이어그램 등으로 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션은 일단 구현되고 나면 변경하는데 큰 비용이 듦&lt;/li&gt;
&lt;li&gt;잘 설계된 어플리케이션은 유지 보수비를 줄여줌.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능, 보안, 안정성 등 측면에서 많은 이점이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱의 경우 미래에 나오는 플랫폼 호환성, 정책 등으로 끊임없는 변화에 대한 대응이 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 애플리케이션의 설계 원칙&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로버트 C. 마틴의 객체지향 프로그래밍 및 설계에 대한 SOLID 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단일 책임 원칙(Single Responsibility Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 클래스는 하나의 책임만 가져야하며, 클래스는 그 책임을 완전히 캡슐화해야 함.&lt;/li&gt;
&lt;li&gt;어떤 클래스나 모듈 또는 매서드가 단 하나의 기능만을 가져야 한다는 뜻.&lt;/li&gt;
&lt;li&gt;이것이 클래스를 더욱 튼튼하게 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 개방-폐쇄 원칙(Open Closed Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어가 확장에 대해서는 열려있고, 수정에 대해서는 닫혀있어야 한다.&lt;/li&gt;
&lt;li&gt;시스템 구조를 옳바르게 구성하여 변경 사항이 발생하더라도 다른 코드나 모듈에 영향이 없도록 함.&lt;/li&gt;
&lt;li&gt;이를 통해 새기능 추가 및 유지보수에 용이하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 리스코프 치환 원칙(Liskov Subsitution Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 S가 클래스 T의 자식 클래스라면 별다른 변경 없이 부모 클래스 T를 자식 클래스 S로 치환할 수 있어야 함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다운 캐스팅된 인스턴스가 논리적으로 역할에 있어서 문제가 되면 아니됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아래 사항을 강제한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하위 클래스에서 메서드 파라미터의 반공변성&lt;/li&gt;
&lt;li&gt;하위 클래스에서 반환형의 공변성&lt;/li&gt;
&lt;li&gt;하위 클래스에서 메서드는 상위 클래스 메서드에서 던져진 예외 사항을 제외하고 새로운 예외 사항을 던지면 안됨.&lt;/li&gt;
&lt;li&gt;하위 클래스에서 선행 조건은 강화될 수 없음&lt;/li&gt;
&lt;li&gt;하위 클래스에서 후행 조건은 약화될 수 없음&lt;/li&gt;
&lt;li&gt;하위형에서 상위형 불변 조건은 반드시 유지되어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;open class A{}
open class B: A() {}
open class C: B() {}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 같은 클래스가 있을 때&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-kotlin&quot;&gt;fun main() {
  val list:List&amp;lt;B&amp;gt; = listOf&amp;lt;B&amp;gt;()
  val list2:List&amp;lt;B&amp;gt; = listOf&amp;lt;C&amp;gt;() // 공변성
  val list3:List&amp;lt;B&amp;gt; = listOf&amp;lt;A&amp;gt;() // 반공변성 (error!)

  // 불변성은 list2, list3 둘다 허용하지 않는 것.
}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인터페이스 분리 원칙(Interface Segregation Principle)&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;!-- 본문용반응형광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-9600379972579559&quot; data-ad-slot=&quot;6142923836&quot; data-ad-format=&quot;auto&quot; data-ad-language=&quot;ko&quot; data-full-width-responsive=&quot;false&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
          (adsbygoogle = window.adsbygoogle || []).push({});
  &lt;/script&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떠한 클래스가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙&lt;/li&gt;
&lt;li&gt;큰 덩어리 인터페이스를 작은 단위로 분리&lt;/li&gt;
&lt;li&gt;작은 단위 인터페이스를 역할 인터페이스 라고 부른다.&lt;/li&gt;
&lt;li&gt;시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 용이하게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;abstract class Bird{
    abstract fun fly()
    abstract fun cry()
}

class Eagle : Bird(){
    override fun fly() {
    }

    override fun cry() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새는 울고, 난다.&lt;/li&gt;
&lt;li&gt;독수리도 울고, 난다.&lt;/li&gt;
&lt;li&gt;그런데 펭귄은?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날지 못한다.. 이는 ISP 원칙에 위배될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;abstract class Bird{
    abstract fun cry()
}

interface Flyable{
    fun fly()
}

class Eagle : Bird(), Flyable{
    override fun fly() {
    }

    override fun cry() {
    }
}

class Penguin : Bird(){
    override fun cry() {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날기, 울기 기능을 나눠 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 의존 역전 원칙(Dependency Inversion Principle)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈들을 분리하는 특정 형식을 지칭&lt;/li&gt;
&lt;li&gt;상위 계층이 하위 계층에 의존하는 전통적인 의존 관계를 역전 시킨다.&lt;/li&gt;
&lt;li&gt;상위 계층이 하위 계층의 구현으로 부터 독립되게 함.
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상위 모듈은 하위 모듈에 의존해서는 안됨. 상위 모듈 하위 모듈 모두 추상화에 의존해야함&lt;/li&gt;
&lt;li&gt;추상화는 세부 사항에 의존해서는 안됨. 세부 사항이 추상화에 의존해야함 (상위와 하위 객체 모두 동일한 추상화에 의존해야함)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;안드로이드 기기 -&amp;gt; 전용 충전기&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기기 마다 충전기가 다를 수 있고 A 기기 충전기로 B 기기를 충전하지 못하는 상황 발생 가능.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 전용 충전기에 강한 의존성을 갖는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;안드로이드 기기 &amp;rarr; C-Type 충전기 &amp;larr; (S 제조사 충전기, L 제조사 충전기, X 제조사 충전기)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;USB-C 규격을 가지는 충전기와 모두 호환 (interface)&lt;/li&gt;
&lt;li&gt;특정 충전기가 아니라 type-C라는 인터페이스에 의존하게 된다. (의존성 역전: 제조사가 인터페이스에 의존하게됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Android/클린 아키텍처</category>
      <category>안드로이드 애플리케이션 설계</category>
      <author>상추님</author>
      <guid isPermaLink="true">https://whyprogrammer.tistory.com/606</guid>
      <comments>https://whyprogrammer.tistory.com/606#entry606comment</comments>
      <pubDate>Fri, 16 Jul 2021 22:40:24 +0900</pubDate>
    </item>
  </channel>
</rss>