<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chris_park.log</title>
        <link>https://velog.io/</link>
        <description>Android Developer</description>
        <lastBuildDate>Tue, 02 Jan 2024 09:03:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>chris_park.log</title>
            <url>https://images.velog.io/images/chris_seed/profile/7895ab98-97e3-4187-a33c-18918546df04/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. chris_park.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chris_seed" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Project/Android] 오늘의 물가 - #4. Activity 화면 기본 UI 만들기]]></title>
            <link>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-4.-Activity-%ED%99%94%EB%A9%B4-%EA%B8%B0%EB%B3%B8-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0-3xdyqgnh</link>
            <guid>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-4.-Activity-%ED%99%94%EB%A9%B4-%EA%B8%B0%EB%B3%B8-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0-3xdyqgnh</guid>
            <pubDate>Tue, 02 Jan 2024 09:03:05 GMT</pubDate>
            <description><![CDATA[<p>기획 및 디자인을 보면 화면마다 중복되는 부분이 있다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/488de47f-be99-4810-a138-da1d365ed16f/image.png" width="400" />

<br>

<p>바로, 툴바(Toolbar)다.
위 이미지의 빨간색 선 구역을 우린 보통 Toolbar, Appbar로 지칭하는데 이 부분은 대부분의 화면에 들어가기 때문에 이 부분을 기본적으로 구현하고 있는 Activity를 만들것이다. 그리고 이 툴바가 필요한 화면을 만들때는 이 툴바가 구현된 Activity를 상속받을 것이다.</p>
<p><br><br><br><br></p>
<h1 id="구현-기능">구현 기능</h1>
<br>

<ol>
<li><p>여러 종류의 툴바 제공</p>
</li>
<li><p>사이드 네비게이션 제공</p>
</li>
<li><p>화면 전환 시 애니메이션 제공 </p>
</li>
</ol>
<br>

<p>위 3가지 기능을 제공하는 Activity를 상속받아 중복되는 코드 줄이는 것이 최종적인 이번 구현의 목적이다.</p>
<p><br><br><br><br></p>
<h1 id="ui-그리기">UI 그리기</h1>
<p>기본이 되는 화면의 UI 구조는 아래와 같다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/73ef77b7-8b34-4098-9137-5ce9de8d153b/image.png" width="400" />

<br>

<p>메뉴 버튼을 누르면 왼쪽에서 메뉴 화면이 슬라이드되면서 나온다. 이 화면이 필요없으면 DrawerLayout이랑 NavigationView는 제외하고 CoordinatorLayout부터 만들면 된다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/f99eb98c-e0bb-4718-b7cb-29acd7c401a0/image.png" width="400" />




<hr>
<p><strong>activity_base.xml</strong></p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.drawerlayout.widget.DrawerLayout 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:id=&quot;@+id/drawerLayout&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&gt;

    &lt;androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        tools:context=&quot;.component.BaseActivity&quot;&gt;

        &lt;com.google.android.material.appbar.AppBarLayout
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:background=&quot;@color/background&quot;
            app:elevation=&quot;0dp&quot;
            android:fitsSystemWindows=&quot;true&quot;&gt;

            &lt;com.google.android.material.appbar.CollapsingToolbarLayout
                android:id=&quot;@+id/toolbarLayout&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:fitsSystemWindows=&quot;true&quot;
                app:collapsedTitleTextColor=&quot;@color/black&quot;
                app:contentScrim=&quot;@color/background&quot;
                app:expandedTitleTextColor=&quot;@color/white&quot;
                app:expandedTitleTextAppearance=&quot;@style/TextAppearance.Design.CollapsingToolbar.Expanded.Shadow&quot;
                app:layout_scrollFlags=&quot;scroll|exitUntilCollapsed&quot;&gt;

                &lt;androidx.appcompat.widget.AppCompatImageView
                    android:id=&quot;@+id/backgroundImageView&quot;
                    android:layout_width=&quot;match_parent&quot;
                    android:layout_height=&quot;300dp&quot;
                    android:background=&quot;@color/background&quot;
                    android:fitsSystemWindows=&quot;true&quot;
                    android:scaleType=&quot;centerCrop&quot;
                    android:visibility=&quot;gone&quot; /&gt;

                &lt;androidx.appcompat.widget.Toolbar
                    android:id=&quot;@+id/toolbar&quot;
                    android:elevation=&quot;0dp&quot;
                    android:layout_width=&quot;match_parent&quot;
                    android:layout_height=&quot;?attr/actionBarSize&quot;
                    app:contentInsetStart=&quot;0dp&quot;
                    app:contentInsetEnd=&quot;0dp&quot;
                    app:layout_collapseMode=&quot;pin&quot; /&gt;

            &lt;/com.google.android.material.appbar.CollapsingToolbarLayout&gt;


        &lt;/com.google.android.material.appbar.AppBarLayout&gt;
        &lt;ScrollView
            android:id=&quot;@+id/contentLayout&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;match_parent&quot;
            android:contentDescription=&quot;@string/activity_container&quot;
            app:layout_behavior=&quot;com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior&quot; /&gt;

    &lt;/androidx.coordinatorlayout.widget.CoordinatorLayout&gt;

    &lt;com.google.android.material.navigation.NavigationView
        android:id=&quot;@+id/navigationView&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;match_parent&quot;
        android:layout_gravity=&quot;start&quot;
        android:background=&quot;@color/background&quot;
        android:fitsSystemWindows=&quot;true&quot;
        app:headerLayout=&quot;@layout/drawer_hearder&quot;
        app:menu=&quot;@menu/drawer&quot; /&gt;

&lt;/androidx.drawerlayout.widget.DrawerLayout&gt;</code></pre>
<hr>
<p><br><br><br><br></p>
<h1 id="기본-activity-만들기">기본 Activity 만들기</h1>
<p>각 코드에 주석 참고</p>
<hr>
<p><strong>BaseActivity.kt</strong></p>
<pre><code class="language-kotlin">// 툴바 타입
enum class ToolbarType {
    MENU,
    BACK,
    HOME,
}

// 애니메이션 타입
enum class TransitionMode {
    NONE,
    HORIZON,
    VERTICAL
}

open class BaseActivity(
    private val toolbarType: ToolbarType,
    private val transitionMode: TransitionMode = TransitionMode.NONE,
) : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {

    // activity_base의 dataBinding
    protected lateinit var baseBinding: ActivityBaseBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        baseBinding = ActivityBaseBinding.inflate(layoutInflater)
        setContentView(baseBinding.root)
        setSupportActionBar(baseBinding.toolbar)
        setToolbar()

       // navigationView에 리스너 추가
       baseBinding.navigationView.setNavigationItemSelectedListener(this)

        // 애니메이션 모드에 따라 애니메이션 설정 (화면 나타날때)
        when (transitionMode) {
            TransitionMode.HORIZON -&gt; overridePendingTransition(R.anim.horizon_enter, R.anim.none)
            TransitionMode.VERTICAL -&gt; overridePendingTransition(R.anim.vertical_enter, R.anim.none)
            else -&gt; {}
        }

    }

    override fun finish() {
        super.finish()

        // 애니메이션 모드에 따라 애니메이션 설정 (화면 사라질 때)
        when (transitionMode) {
            TransitionMode.HORIZON -&gt; overridePendingTransition(R.anim.none, R.anim.horizon_exit)
            TransitionMode.VERTICAL -&gt; overridePendingTransition(R.anim.none, R.anim.vertical_exit)
            else -&gt; {}
        }
    }

    override fun onBackPressed() {

        // navigationView가 나와있으면 onBackPressed()가 아닌 navigationView 닫기
        if (baseBinding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
            baseBinding.drawerLayout.closeDrawers()
        } else {
            super.onBackPressed()

            if (isFinishing) {
                when (transitionMode) {
                    TransitionMode.HORIZON -&gt; overridePendingTransition(
                        R.anim.none,
                        R.anim.horizon_exit
                    )
                    TransitionMode.VERTICAL -&gt; overridePendingTransition(
                        R.anim.none,
                        R.anim.vertical_exit
                    )
                    else -&gt; Unit
                }
            }
        }
    }

    override fun onDestroy() {
        // navigationView 리스너 제거
        baseBinding.navigationView.setNavigationItemSelectedListener(null)
        super.onDestroy()
    }

    // Toolbar의 아이템 클릭 별 수행 코드
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_search -&gt; {
                actionMenuSearch()
            }

            R.id.action_favorite -&gt; {
                actionMenuFavorite()
            }

            R.id.action_home -&gt; {
                actionMenuHome()
            }

            android.R.id.home -&gt; {
                actionHome()
            }
        }
        return super.onOptionsItemSelected(item)
    }

    // 툴바 타입에 따라 메뉴 아이템 변경
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        when (toolbarType) {
            ToolbarType.MENU -&gt; menuInflater.inflate(R.menu.menu_with_search, menu)
            ToolbarType.BACK -&gt; menuInflater.inflate(R.menu.back_with_search, menu)
            ToolbarType.HOME -&gt; menuInflater.inflate(R.menu.back_with_home, menu)
        }
        return true
    }

    private fun setToolbar() {
    // 툴바 타입에 따라 왼쪽 상단 이미지 변경
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.setBackgroundDrawable(
            ColorDrawable(
                ContextCompat.getColor(
                    applicationContext,
                    R.color.background
                )
            )
        )

        when (toolbarType) {
            ToolbarType.MENU -&gt; supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
            ToolbarType.BACK -&gt; supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_back)
            ToolbarType.HOME -&gt; supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_back)
        }

    }

    // 상단에 이미지 띄우는게 필요할 때 사용 (상속받는 Activity에서 호출)
    fun setImageView(url: String) {
        if (url.isNotEmpty()) {
            Glide.with(baseBinding.root)
                .load(url)
                .centerCrop()
                .error(R.drawable.no_picture)
                .into(baseBinding.backgroundImageView)
        } else {
            baseBinding.backgroundImageView.setImageResource(R.drawable.no_picture)
        }
    }

    // 상단에 타이틀이 필요한 경우 사용 (상속받는 Activity에서 호출)
    fun setTitle(title: String) {
        baseBinding.toolbarLayout.title = title
        baseBinding.backgroundImageView.isVisible = true
        supportActionBar?.setBackgroundDrawable(
            ColorDrawable(
                ContextCompat.getColor(
                    applicationContext,
                    android.R.color.transparent
                )
            )
        )
    }

    private fun actionMenuSearch() {
        Toast.makeText(this, &quot;검색하기&quot;, Toast.LENGTH_SHORT).show()
    }

    private fun actionMenuFavorite() {
        Toast.makeText(this, &quot;즐겨찾기&quot;, Toast.LENGTH_SHORT).show()
    }

    private fun actionMenuHome() {
        Toast.makeText(this, &quot;호오옴&quot;, Toast.LENGTH_SHORT).show()
    }


    private fun actionHome() {
        Toast.makeText(this, &quot;왼쪽 상단 버튼&quot;, Toast.LENGTH_SHORT).show()
    }

    // navigationView의 item 클릭 시 수행 코드
    override fun onNavigationItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.menuItem1 -&gt; Toast.makeText(this, &quot;item1 is clicked&quot;, Toast.LENGTH_SHORT).show()
            R.id.menuItem2 -&gt; Toast.makeText(this, &quot;item2 is clicked&quot;, Toast.LENGTH_SHORT).show()
            R.id.menuItem3 -&gt; Toast.makeText(this, &quot;item3 is clicked&quot;, Toast.LENGTH_SHORT).show()
        }
        return false
    }
}</code></pre>
<hr>
<p><br><br><br><br></p>
<h1 id="상속받는-activity-만들기">상속받는 Activity 만들기</h1>
<p>이제 기본 Activity를 상속받는 Activity를 만들어보자.</p>
<hr>
<p><strong>activity_item.xml</strong></p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;layout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;
    &lt;androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;match_parent&quot;
        android:padding=&quot;10dp&quot;
        android:orientation=&quot;vertical&quot;&gt;

        &lt;androidx.appcompat.widget.AppCompatImageView
            android:id=&quot;@+id/itemImageView&quot;
            android:layout_width=&quot;150dp&quot;
            android:layout_height=&quot;150dp&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot;
            android:layout_marginTop=&quot;20dp&quot;
            android:background=&quot;@drawable/background_image&quot; /&gt;

    &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
&lt;/layout&gt;
</code></pre>
<hr>
<p><strong>ItemActivity.kt</strong></p>
<pre><code class="language-kotlin">//  BaseActivity를 상속하고 인자로 툴바 타입과 애니메이션 방향 타입 전달
class ItemActivity: BaseActivity(ToolbarType.HOME, TransitionMode.HORIZON)  {

    private lateinit var binding: ActivityItemBinding

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

        binding = ActivityItemBinding.inflate(layoutInflater)

        // BaseActivity의 contentLayout (ScrollView)에 추가
        baseBinding.contentLayout.addView(binding.root)

    }
}</code></pre>
<hr>
<p><br><br><br><br></p>
<h1 id="결과물">결과물</h1>
<p>이후로 좀 더 개발이 진행된 상태다.</p>
<div class="video-container">
    <video width="300" autoplay muted controls src="https://velog.velcdn.com/images/chris_seed/post/f97a0eb9-8538-4ad7-9b9d-7d0071dc4c1d/image.mp4"></video>
</div>



<p><br><br><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project/Android] 오늘의 물가 - #3. open API 선정 및 테스트]]></title>
            <link>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-3.-open-API-%EC%84%A0%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-3.-open-API-%EC%84%A0%EC%A0%95-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Tue, 02 Jan 2024 06:59:36 GMT</pubDate>
            <description><![CDATA[<h1 id="🙄-open-api-선정-후-문제점">🙄 open API 선정 후 문제점</h1>
<p>이 프로젝트의 원래 취지는 마트나 시장의 생필품 가격 비교 및 할인 정보 등을 알려주기 위함이었다. </p>
<p>그래서 공공데이터에서 제공되는 open API(<a href="https://www.data.go.kr/data/3043385/openapi.do)%EB%A5%BC">https://www.data.go.kr/data/3043385/openapi.do)를</a> 사용하려고 했다. </p>
<p>공공데이터에서 제공하는 open API인 <span style="color: blue">한국소비자원_생필품 가격 정보</span>는 참가격정보서비스에서 수집하는 상품별 생필품 가격 관련 데이터로 상품정보 조회, 판매점 정보 조회, 생필품가격정보 조회, 생필품 가격 정보 기준 데이터 조회 기능 등을 open API로 제공해준다.</p>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/1c6920f1-2d97-4638-8df1-c52382a6b296/image.png" alt=""></p>
<p>그래서 기능도 똑같기 때문에 해당 API를 사용하려고 했다. </p>
<br>

<p>그런데 테스트를 하면서 매우 치명적인 문제점을 찾게 되었다.
<img src="https://velog.velcdn.com/images/chris_seed/post/f175a03a-740f-4c8c-9bf2-1b83721467ce/image.png" width="500" /></p>
<p>결과로 받을 수 있는 최대 메시지 사이즈가 1000KB다. 그런데 요청 메시지 명세를 보면 그 어디에도 index나 page가 없다. 보통은 시작하는 index나 page가 있어서 1번째부터 얼마까지 들고오는 식으로 하는데 이 API는 그 인자값이 없었다. 그래서 이 API는 내가 데이터를 불러오면 처음 호출할 때의 1000KB만 가지고 올 수 있다는 말이다. </p>
<br>

<p>그래서 우선 공공데이터에 문의를 했다. </p>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/33f5c39a-12eb-48e6-b1f9-da2652fe4a25/image.png" alt=""></p>
<p>생각보다 빠르게 답장이 왔지만, API를 뜯어고쳐야해서 그런지 즉각적인 해결 방법은 없었다. </p>
<p><br><br><br><br></p>
<h1 id="📍-대안을-찾다">📍 대안을 찾다.</h1>
<p>어쩔수없이 다른 open API를 찾기로 했고, 모든 기능을 만족하지는 못하지만 대체할 API를 찾게 되었다.</p>
<p>서울열린데이터광장(<a href="https://data.seoul.go.kr/)%EC%97%90%EC%84%9C">https://data.seoul.go.kr/)에서</a> 제공하는 서울시 생필품 농수축산물 가격 정보(<a href="https://data.seoul.go.kr/dataList/OA-1170/S/1/datasetView.do)%EB%8B%A4">https://data.seoul.go.kr/dataList/OA-1170/S/1/datasetView.do)다</a>.</p>
<p>제공하는 데이터는 서울시에 있는 전통시장의 생필품 가격이다. 기존에는 대형마트도 제공이 되었는데 2023년 3월 27일부터 제외되었다고 한다.</p>
<p><br><br><br><br></p>
<h1 id="✨-api-테스트">✨ API 테스트</h1>
<p>그럼 API 테스트를 해보자!</p>
<p>API 테스트 툴은 Postman으로 했다.
open API 제공하는 페이지의 설명대로 요청을 보냈고, 정상적으로 처리되었다.</p>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/591064ac-95df-42e5-b2b1-59ff5782328b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project/Android] 오늘의 물가 - #2. 기획 및 디자인]]></title>
            <link>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-2.-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-2.-%EA%B8%B0%ED%9A%8D-%EB%B0%8F-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Tue, 02 Jan 2024 05:38:23 GMT</pubDate>
            <description><![CDATA[<p>데이터베이스를 통합하는 동안 미루었던 글을 작성하려고 한다.</p>
<p>우선 기존에 생각했던 디자인 및 기획이다.
디자인 툴은 Figma를 사용했고, 처음 기획하고 디자인할 때는 매우 기본적인거만 하고 나중에 개발하면서 수정하려고 간단하게 만들었다.</p>
<p>화면도 홈, 카테고리 상세 화면, 상품 상세 화면, 즐겨찾기 화면만 만들어두었던 이유가 그것이다.</p>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/4bd865f8-0374-40b8-9507-ed3e0b1e2f3c/image.png" alt=""></p>
<br>

<p>하지만 가져다쓰는 API를 변경하면서 앱의 기능 및 용도도 살짝 변경이 되어 개발 후에 결과물은 계획대로 나오지 않을거같다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project/Android] 오늘의 물가 - #0. 프로젝트 기록 관리]]></title>
            <link>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%A1%9D-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-0.-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%A1%9D-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 06 Nov 2023 14:25:08 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 시 세부적인 하루에 한 일을 기록하기 위한 페이지다.</p>
<br>
<br>


<blockquote>
<p>프로젝트 1주차 (11월 둘째주)</p>
</blockquote>
<table>
<thead>
<tr>
<th>날짜</th>
<th>완료</th>
<th>진행 중</th>
<th>기타</th>
</tr>
</thead>
<tbody><tr>
<td>2023.11.06</td>
<td><span style="color: green;">[디자인]</span> 홈 화면<br><span style="color: green;">[디자인]</span> 카테고리 화면</td>
<td><span style="color: green;">[디자인]</span> 상품 상세 화면</td>
<td>- 공공 데이터 open API 활용 신청 완료</td>
</tr>
<tr>
<td>2023.11.07</td>
<td>[환경설정] IDE 버전 업그레이드<br>[환경설정] Firebase 프로젝트 생성 및 연동</td>
<td><span style="color: pink;">[개발]</span> 홈 화면 (툴바)</td>
<td>-</td>
</tr>
<tr>
<td>2023.11.08</td>
<td><span style="color: pink;">[개발]</span> BaseActivity 생성<br><span style="color: pink;">[개발]</span> 홈 화면(툴바)</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2023.11.09</td>
<td><span style="color: pink;">[개발]</span> 카테고리 화면 생성</td>
<td><span style="color: pink;">[개발]</span> API 연동 준비</td>
<td>-</td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>프로젝트 2, 3, 4주차 (12월)</p>
</blockquote>
<table>
<thead>
<tr>
<th>날짜</th>
<th>완료</th>
<th>진행 중</th>
<th>기타</th>
</tr>
</thead>
<tbody><tr>
<td>2023.12.12</td>
<td>-</td>
<td>-</td>
<td><span style="color: pink;">[개발]</span> open API의 제공처를 공공데이터 &gt; 서울열린데이터광장으로 변경</td>
</tr>
<tr>
<td>2023.12.13 ~ 15</td>
<td><span style="color: pink;">[개발]</span> MVVM 패턴 적용<br><span style="color: pink;">[개발]</span> open API 테스트</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2023.12.18 ~ 22</td>
<td><span style="color: pink;">[개발]</span> 개별 항목 상세 화면<br><span style="color: pink;">[개발]</span> 시장 리스트 화면</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2023.12.26 ~ 29</td>
<td>-</td>
<td><span style="color: pink;">[개발]</span> Firebase Functions 사용해서 oepn API에서 받아온 데이터를 Firestore에 저장</td>
<td>-</td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>프로젝트 5주차 (1월 첫째주)</p>
</blockquote>
<table>
<thead>
<tr>
<th>날짜</th>
<th>완료</th>
<th>진행 중</th>
<th>기타</th>
</tr>
</thead>
<tbody><tr>
<td>2023.01.02</td>
<td><span style="color: pink;">[개발]</span> 2023년도 데이터베이스 업로드</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2023.01.03</td>
<td>-</td>
<td><span style="color: pink;">[개발]</span> 데이터베이스 업로드 배포 함수</td>
<td>-</td>
</tr>
<tr>
<td>2023.01.10</td>
<td><span style="color: pink;">[개발]</span> 데이터베이스 배포 함수 스케쥴 작업<br><span style="color: pink;">[개발]</span> Market, Item 레포지토리 및 뷰모델 작업</td>
<td>-</td>
<td>-</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project/Android] 오늘의 물가 - #1. 계획 수립 단계]]></title>
            <link>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-0.-%EA%B3%84%ED%9A%8D-%EC%88%98%EB%A6%BD-%EB%8B%A8%EA%B3%84</link>
            <guid>https://velog.io/@chris_seed/ProjectAndroid-%EC%98%A4%EB%8A%98%EC%9D%98-%EB%AC%BC%EA%B0%80-0.-%EA%B3%84%ED%9A%8D-%EC%88%98%EB%A6%BD-%EB%8B%A8%EA%B3%84</guid>
            <pubDate>Mon, 06 Nov 2023 14:20:14 GMT</pubDate>
            <description><![CDATA[<p>새로운 프로젝트를 시작하려고 한다.
생필품 물가를 확인하고 할인이나, 1+1 이벤트를 확인할 수 있는 앱을 만들거다.
굳 아이디어인거 같다. (사슴이 눈이 좋으면? <code>굳아이디어</code>)</p>
<p>아래 글은 프로젝트를 시작하고자 한 계기, 어떻게 만들고자 하는지 적고자 한다. 
결론은 이 글 자체가 프로젝트 시작 전 프롤로그인 셈이다.</p>
<p><br><br>
<br><br></p>
<h1 id="이-앱을-만들고자-한-이유">이 앱을 만들고자 한 이유</h1>
<p>요즘 마트가서 우유나 시리얼같은 생필품 사는데 가격이 비싸다. 그리고 자주 할인하는 제품이라 정가격에 사면 뭔가 좀 속이 쓰리다. </p>
<p>이런 일이 종종 생길때 생각했다. 
할인 정보나 마트별 가격 정보를 제공하는 게 있으면 좋겠다고.</p>
<p>물론 웹과 앱이 있다. 참가격(<a href="https://www.price.go.kr/tprice/portal/main/main.do)%EC%9D%B4%EB%9D%BC%EB%8A%94">https://www.price.go.kr/tprice/portal/main/main.do)이라는</a> 웹 사이트가 있다. 그리고 앱으로도 비슷한 기능을 하는 앱이 있다. </p>
<p>그런데 왜 또 만들려고 하는지 궁금할 수 있다. 그 이유는 심플하다. 보기 쉬운 UI와 좀 더 추가된 핵심기능이 필요하기 때문이다.</p>
<p>앱을 만들 때 실생활에 유용한 것을 만들려고 늘 생각한다. 그 부분이 충족되면 사람들도 많이 쓸것이고 나의 생활에도 도움이 될거라고 생각하기 때문이다.</p>
<p><br><br>
<br><br></p>
<h1 id="프로젝트-계획">프로젝트 계획</h1>
<p>프로젝트 계획은 별거없다.</p>
<ol>
<li>앱 기획 및 디자인 (Figma) </li>
<li>open API를 하나의 데이터베이스에 통합 (Firebase Firestore)</li>
<li>스케쥴러나 배치 프로그램으로 open API의 데이터를 주기적으로 내 데이터베이스에 insert (Schedule functions)</li>
<li>앱 개발</li>
</ol>
<br>

<h3 id="😶프로젝트-인원">😶프로젝트 인원</h3>
<p><strong>1명, 오로지 나 혼자다.</strong></p>
<p>나 혼자 기획하고 디자인하고 개발하면 된다.
혼자 프로젝트를 진행할 때 장점은 의견 교환 및 방향 결정을 혼자 정하면 된다는 점이다. 매우 빠르게 진행할 수 있다. 단점은 의견 교환할 사람이 없다는 점이다.</p>
<p>언젠가는 사이드프로젝트 구해서 여러명이서 해보고 싶다.</p>
<br>


<h3 id="📢개발-타임라인">📢개발 타임라인</h3>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/f3928a7a-77d2-4ac4-9ae6-37e72babbaf7/image.png" alt=""></p>
<p><br><br>
<br><br></p>
<h1 id="앱-기능">앱 기능</h1>
<p>최소 기능으로만 개발하고 출시할 예정이다. 추후 업데이트로 또 다른 기능을 추가할것이다.</p>
<ul>
<li>생필품 가격 조회</li>
<li>상품 판매점 조회</li>
<li>최근 6개월 or 1주일 가격 비교 (최근 6개월: 1개월의 평균 가격 / 최근 1주일: 하루 가격 제공)</li>
<li>할인 정보, 1+1 정보 제공</li>
<li>판매점 위치 제공 (맵 api는 넣을지 말지 고민중)</li>
<li>즐겨찾기 기능</li>
</ul>
<p><br><br>
<br><br></p>
<h1 id="기대하는-결과">기대하는 결과</h1>
<ol>
<li>출시 후 다운로드 1000돌파</li>
<li>추후 애드몹을 통한 수익 실현</li>
<li>개발 능력 향상</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[onCreateOptionsMenu() 호출이 안될때]]></title>
            <link>https://velog.io/@chris_seed/onCreateOptionsMenu-%ED%98%B8%EC%B6%9C%EC%9D%B4-%EC%95%88%EB%90%A0%EB%95%8C</link>
            <guid>https://velog.io/@chris_seed/onCreateOptionsMenu-%ED%98%B8%EC%B6%9C%EC%9D%B4-%EC%95%88%EB%90%A0%EB%95%8C</guid>
            <pubDate>Mon, 24 Jul 2023 02:08:23 GMT</pubDate>
            <description><![CDATA[<p>앱 화면에 메뉴바를 만들때 사용하는 방법 중 하나가 Android Resource Directory/menu에 관련 xml을 만들고 그것을 Activity의 <code>onCreateOptionsMenu()</code>에서 inflate하는 방법이다.</p>
<br>

<p>코드는 이런식일 것이다.</p>
<p>res/menu/options_menu.xml</p>
<pre><code class="language-kotlin">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;menu xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;

    &lt;item
        android:id=&quot;@+id/search&quot;
        android:icon=&quot;@drawable/round_search_24&quot;
        android:title=&quot;@string/search_hint&quot;
        app:actionViewClass=&quot;androidx.appcompat.widget.SearchView&quot;
        app:showAsAction=&quot;ifRoom|withText&quot; /&gt;
&lt;/menu&gt;</code></pre>
<br>

<p>MainActivity.kt</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).apply {
            setContentView(root)
            view = this@MainActivity
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {

        menuInflater.inflate(R.menu.options_menu, menu)

        (menu.findItem(R.id.search).actionView as SearchView).apply {
            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String): Boolean {
                    return false
                }

                override fun onQueryTextChange(newText: String): Boolean {
                    return false
                }

            })
        }

        return true
    }
}
</code></pre>
<br>

<p>근데 실행했을때 메뉴바가 안나오는 경우가 있다. 보통은 이 onCreateOptionsMenu()가 호출이 안되서인데 이유는 <code>themes.xml</code>을 보면 알 수 있다.</p>
<p><br><br></p>
<p>res/themes/themes.xml</p>
<pre><code class="language-xml">&lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;
    &lt;!-- Base application theme. --&gt;
    &lt;style name=&quot;Base.Theme.MediaSearch&quot; parent=&quot;Theme.Material3.DayNight.NoActionBar&quot;&gt;
        &lt;!-- Customize your light theme here. --&gt;
        &lt;!-- &lt;item name=&quot;colorPrimary&quot;&gt;@color/my_light_primary&lt;/item&gt; --&gt;
    &lt;/style&gt;

    &lt;style name=&quot;Theme.MediaSearch&quot; parent=&quot;Base.Theme.MediaSearch&quot; /&gt;
&lt;/resources&gt;</code></pre>
<br>

<p>NoActionBar라고 적힌 설정 때문에 onCreateOptionsMenu()가 호출이 안되는 것이다. 호출을 해주고 싶으면 아래처럼 변경해준다.</p>
<pre><code>&lt;style name=&quot;Base.Theme.MediaSearch&quot; parent=&quot;Theme.Material3.DayNight&quot;&gt;</code></pre><p><br><br></p>
<p>이게 안드로이드 스튜디오를 업데이트하고 나서부터 NoActionBar 설정이 디폴트가 되었다. (더 자세한 것은 조사 후 추가 예정)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android 스튜디오 업데이트 일대기]]></title>
            <link>https://velog.io/@chris_seed/Android-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%9D%BC%EB%8C%80%EA%B8%B0</link>
            <guid>https://velog.io/@chris_seed/Android-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%9D%BC%EB%8C%80%EA%B8%B0</guid>
            <pubDate>Thu, 20 Apr 2023 08:53:37 GMT</pubDate>
            <description><![CDATA[<p>2021년쯤에 Adnroid Studio를 설치했다. 그때 버전이 Arctic Fox였다. 지금까지 Android Studio 업데이트를 한번도 안해서 오늘 이 글을 쓰기전까지도 나의 소중한 IDE는 2021년 버전 그대로다. 그래 Arctic Fox다. 그 당시엔 Bumblebee가 막 나올때라 안정화 버전인 Arctic Fox를 다운받았었다. 그리고 그 당시에 Gradle 버전과 Android Gradle Plugin 버전이 호환이 잘안되서 세팅하는데 고생했던 기억에 업데이트를 차일피일 미루고 오늘에까지 왔다.</p>
<p>오늘 큰 마음 먹고 Android Studio를 업데이트하기로 했다. 매일 경고창 뜨는 것도 지겹고 버전이 많이 나와서 해야되겠다 싶었다. </p>
<p>우선 Android Studio를 업그레이드 하기전에 나에게 찾아올 재앙을 생각해봤다. </p>
<br>

<h3 id="android-studio-버전-업데이트-전-고려사항">Android Studio 버전 업데이트 전 고려사항</h3>
<ul>
<li>Java Version </li>
<li>Gradle Version</li>
<li>Android Gradle Plugin Version</li>
</ul>
<br>

<p>우선적으로 기존의 Android Studio 버전에서 진행하던 프로젝트들도 계속 작업을 해야해서 버전 생각밖에 안들었다. 호환이 안맞으면 어떡하지, 빌드 에러나면 어떡하지, 다시 프로젝트 생성해야된다면 난 끝이야, 이런 생각으로 한거같다. 그만큼 소프트웨어나 IDE나 프레임워크의 버전을 업그레이드 할 때는 무섭다. </p>
<p><br><br>
<br><br></p>
<h1 id="업데이트-댕-쉽ㄴㅔ">업데이트 댕 쉽ㄴ..ㅔ...?</h1>
<hr>
<p>업데이트하는 방법은 간단했다. <a href="https://developer.android.com/studio?gclid=CjwKCAjwov6hBhBsEiwAvrvN6Fu_Qspj3MnaTLWSgWGeDmRdbS_5kmDOc_5jfISh7-FEC5zhq1e35hoCiusQAvD_BwE&amp;gclsrc=aw.ds">Android Developers</a>에서 새로운 버전을 다운로드한 후 기존에 설치된 Android Studio을 대체하면 되었다. 그리고 이제 대망의 프로젝트 빌드 순간이었다. 그리고 어느순간부터 Build창에 시뻘건 글씨가 매우 많이 뜨는 걸 보면서 &#39;아..큰일났네&#39; 싶었다.  몇년만에 업데이트하는 거라 당연히 버전이 안맞을거라고는 생각했었는데 뭐 이상한거부터 다 안되는 거 같았다. 하나 해결하면 그 다음 에러가 터지고 또 그 다음 에러가 터지는 연쇄에러였다.</p>
<br>

<p>우선 내가 약 2년동안 사용한 환경이다. </p>
<h3 id="기존-환경-설정">기존 환경 설정</h3>
<ul>
<li>Android Studio : Arctic Fox</li>
<li>Gradle version : 7.2</li>
<li>Android Gradle Plugin : 7.0</li>
<li>Java : openJDK 11.0.15</li>
</ul>
<br> 

<h2 id="android-gradle-plugin-업데이트를-추천드립니다">Android Gradle Plugin 업데이트를 추천드립니다.</h2>
<p>Android Studio를 업데이트하고 Android Gradle Plugin 버전을 8.0으로 업그레이드하라는 경고 메세지가 떴다. 그래서 &#39;오케이, 이게 호환이 맞나보군?&#39;하면서 바로 Android Gradle Plugin 버전을 8.0으로 업그레이드했다. 그리고 경고 메세지가 사라지고 이제는 Gradle version 에러가 떴다. Android Gradle Plugin 8.0이 필요한 최소 Gradle 버전은 8.0이라는 거다. ㅇㅋ 그까짓꺼 업그레이드 해준다. 그렇게 나는 Android Gradle Plugin과 Gradle 버전을 둘 다 8.0으로 업그레이드 후 다시 빌드를 진행했다. </p>
<p>그리고 또 에러가 나왔다. Android Gradle Plugin(AGP) 8.0은 Java 최소 버전이 17인 것이다.ㅋㅋㅋㅋㅋㅋㅋㅋㅋ? 17? Java 버전 17로 바로 올리는건 라이브러리 호환성이라든가 뭐 이것저것 캥기는게 많았다. 그래서 쭈굴쭈굴해진채로 AGP downgrade를 하기위해 Gradle과 Android Gradle Plugin의 최소 버전을 찾아봤다.</p>
<br>


<h4 id="span-stylecolorskyblueandroid-studio--android-gradle-plugin"><span style="color:skyblue;">Android Studio &amp; Android Gradle Plugin</h4>
</span>

<table>
<thead>
<tr>
<th align="center">Android Studio 버전</th>
<th align="center">필요한 플러그인 버전</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Giraffe 2022.3.1</td>
<td align="center">3.2~8.1</td>
</tr>
<tr>
<td align="center">Flamingo 2022.2.1</td>
<td align="center">3.2~8.0</td>
</tr>
<tr>
<td align="center">Electric Eel 2022.1.1</td>
<td align="center">3.2~7.4</td>
</tr>
<tr>
<td align="center">Dolphin 2021.3.1</td>
<td align="center">3.2~7.3</td>
</tr>
<tr>
<td align="center">Chipmunk 2021.2.1</td>
<td align="center">3.2~7.2</td>
</tr>
<tr>
<td align="center">Bumblebee 2021.1.1</td>
<td align="center">3.2~7.1</td>
</tr>
<tr>
<td align="center">Arctic Fox 2020.3.1</td>
<td align="center">3.1~7.0</td>
</tr>
</tbody></table>
<br>

<h4 id="span-stylecolorvioletandroid-gradle-plugin--gradlespan"><span style="color:violet;">Android Gradle Plugin &amp; Gradle</span></h4>
<table>
<thead>
<tr>
<th align="center">플러그인 버전</th>
<th align="center">최소 Gradle 버전</th>
<th align="center">최소 Java 버전</th>
</tr>
</thead>
<tbody><tr>
<td align="center">8.1</td>
<td align="center">8.0</td>
<td align="center">17</td>
</tr>
<tr>
<td align="center">8.0</td>
<td align="center">8.0</td>
<td align="center">17</td>
</tr>
<tr>
<td align="center">7.4</td>
<td align="center">7.5</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">7.3</td>
<td align="center">7.4</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">7.2</td>
<td align="center">7.3.3</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">7.1</td>
<td align="center">7.2</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">7.0</td>
<td align="center">7.0</td>
<td align="center">11</td>
</tr>
<tr>
<td align="center">4.2.0+</td>
<td align="center">6.7.1</td>
<td align="center">-</td>
</tr>
</tbody></table>
<br>

<p>이제 내가 버전을 어떻게 맞춰야할지 대강 보였다. </p>
<ol>
<li>첫번째로 Flamingo를 사용하니 Android Gradle Plugin(AGP)는 3.2 ~ 8.0 버전을 사용해야 한다.
 -&gt; ㅇㅋ. 아무거나 쓰면 되겠군<br>    </li>
<li>두번째로 내가 사용하는 Java version인 11을 쭉 써야하니 나에게 Gradle 선택지는 7.0 ~ 7.5다. 
 -&gt; 그 중에서 제일 최신인 7.5를 채택한다.<br></li>
<li>마지막으로 플러그인도 7.4로 하면 되겠다.</li>
</ol>
<br>

<p>이런 과정을 통해서 내 Android Studio의 환경 설정은 아래처럼 변했다.</p>
<h3 id="변경된-환경-설정">변경된 환경 설정</h3>
<ul>
<li>Android Studio : Flamingo</li>
<li>Gradle version : 7.5</li>
<li>Android Gradle Plugin : 7.4</li>
<li>Java : openJDK 11.0.15</li>
</ul>
<br>
<br>
<br>



<h2 id="classpath는-앞으로-사용할-수-없을것이야">classpath는 앞으로 사용할 수 없을것이야</h2>
<p>이제 끝났나? 해치웠나? 하면서 빌드를 했다. 그리고 또 에러가 뿅하고 튀어나왔다. 자꾸 어디서 그렇게 기어나오는지 모르겠다.</p>
<p>이번 에러는 <code>Plugin with id &#39;com.google.gms.google-services&#39; not found.</code>다. 이때까지 잘 썼는데 갑자기 못찾는다고 에러가 뜨면 당연히 변경된 환경부터 의심하기 시작한다. build.gradle에서 에러가 났고 마침 내가 gradle 버전을 변경했으니 당연히 관련된 내용을 조사했다. 그래서 찾아보니 Android Gradle Plugin을 업데이트해서 발생한 에러 같았다. </p>
<p>Android Developers의 출시 노트를 참고하면 Gradle 7.0부터는 buildscript 블록 내부에서 classpath를 사용하는 것이 더 이상 권장하지 않고 settings.gradle 파일에서 외부 라이브러리나 플러그인과 같은 의존성을 지정하는 것이 권장된다고 한다. 사용은 된다고 했는데 왜 나는 안되나싶어서 캐시 날리고 clean build -&gt; rebuild 이 방식으로 계속 테스트 해봤는데 어느 순간 됐다. 뭐지..?</p>
<p>기존에 사용했던 방식은 프로젝트 단의 build.gradle에서 <code>buildScript {}</code>에서 dependencies 블록에서 선언하는 방식이었는데 <code>plugins</code>에 선언하면 된다. <code>com.google.gms.google-services</code>를 예시로 확인해보자!</p>
<br>


<p><span style="color:violet;"><strong>7.x 이전</strong></span></p>
<pre><code class="language-kotlin">buildscript {
  ...
  dependencies {
    classpath &#39;com.google.gms:google-services:4.3.10&#39;
  }
}
</code></pre>
<br>

<p><span style="color:violet;"><strong>7.x 이후 build.gradle(project)</strong></span></p>
<pre><code class="language-kotlin">plugins {
    id &#39;com.google.gms.google-services&#39; version &#39;4.3.10&#39; apply false
}
</code></pre>
<p>이후 버전에서는 콜론(:)을 사용하는 대신 .이나 -을 사용해야 한다.</p>
<br>

<p>그렇다면, app단의 build.gradle에서는 어떻게 설정해야할까?</p>
<p><span style="color:skyblue;"><strong>7.x 이후 build.gradle(app)</strong></span></p>
<pre><code class="language-kotlin">plugins {
    ...
    id &#39;com.google.gms.google-services&#39;
}</code></pre>
<p>똑같이 plugins에 선언해주면 된다. 단지 version 부분은 빼고 넣는다.</p>
<br>
<br>
<br>
<br>


<h1 id="업데이트-완료-후기">업데이트 완료 후기</h1>
<p>이게 뭐라고 이렇게 에러를 많이 만났나 생각도 들고, 이때까지 업데이트 안한게 뻘줌해질 정도로 업데이트 방식은 간단하다. 단지 딸려오는 에러가 많아서 힘들었는데 그건 내가 Gradle과 플러그인에 대한 지식이 부족해서 그런거같다. Java 버전을 안바꿔서 에러가 이 정도로 그친거같기도 하다..?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Kakao 계정으로 Firebase 연동 - 블로그 이전]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-Kakao-%EA%B3%84%EC%A0%95%EC%9C%BC%EB%A1%9C-Firebase-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-Kakao-%EA%B3%84%EC%A0%95%EC%9C%BC%EB%A1%9C-Firebase-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Wed, 01 Feb 2023 15:09:56 GMT</pubDate>
            <description><![CDATA[<p>블로그 이전으로 동일한 포스팅을 개재할 수 없어 Velog의 내용을 지웁니다.</p>
<p>혹시 해당 포스팅을 참고하실 분은 아래 링크로 가시면 동일한 내용의 글을 확인할 수 있습니다. </p>
<p>감사합니다.</p>
<p>블로그 주소 : <a href="https://super-strength-crystal.blogspot.com/2024/02/android-kakao-firebase-authentication.html">이전한 블로그 포스팅</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] RecyclerView에서 Index 0번 아이템을 Drag할 때 순서 뒤바뀜과 스크롤이 빠르게 넘어가는 현상, 또는 Item을 빠르게 Drag할 때 순서가 뒤바뀌는 현상]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-Index-0%EB%B2%88-%EC%95%84%EC%9D%B4%ED%85%9C%EC%9D%84-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C-%EB%92%A4%EB%B0%94%EB%80%9C-%ED%98%84%EC%83%81-%EB%98%90%EB%8A%94-Item%EC%9D%84-%EB%B9%A0%EB%A5%B4%EA%B2%8C-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C%EA%B0%80-%EB%92%A4%EB%B0%94%EB%80%8C%EB%8A%94-%ED%98%84%EC%83%81</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-Index-0%EB%B2%88-%EC%95%84%EC%9D%B4%ED%85%9C%EC%9D%84-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C-%EB%92%A4%EB%B0%94%EB%80%9C-%ED%98%84%EC%83%81-%EB%98%90%EB%8A%94-Item%EC%9D%84-%EB%B9%A0%EB%A5%B4%EA%B2%8C-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C%EA%B0%80-%EB%92%A4%EB%B0%94%EB%80%8C%EB%8A%94-%ED%98%84%EC%83%81</guid>
            <pubDate>Mon, 30 Jan 2023 14:06:01 GMT</pubDate>
            <description><![CDATA[<p>RecyclerView로 리스트를 만들고 리스트에 있는 아이템의 순서를 바꿀 때 우리는 Drag and Drop으로 한다. 아이템을 끌어서 옮기는 방식인데 한번씩 예상치 못한 현상을 조우하게 되었다. 스크롤이 가능한 길이일 때 Index 0번 아이템을 아래로 Drag하면 스크롤이 빨라지면서 내가 원하는 곳으로 Drop이 안되는 현상이다. 심지어 UI로 보이는 아이템의 순서와 일치하지 않는 경우가 있다. 또한 Index와 상관없이 Drag를 빠르게 하면 아이템의 순서와 데이터가 일치하지 않는 경우가 있는데 이럴 경우의 대안을 기록할것이다.</p>
<p><br><br></p>
<h1 id="오류-및-오류-코드">오류 및 오류 코드</h1>
<p>우선, 어떤 현상인지 눈으로 보자</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/a4d78d0e-0804-485b-a396-bae04de63032/image.gif">


<p>user3이라는 index 2의 아이템을 drag할때는 스크롤이 움직이지않고 정상적으로 작동하는데, user1이라는 index 0의 아이템을 drag할때는 스크롤이 밑으로 빠르게 넘어가고 drop후 리스트 목록을 보면 drag&amp;drop하지않은 아이템이 섞인게 보인다. 이러면 안된다. 내가 의도한 현상이 아니다.</p>
<p><br><br></p>
<h3 id="오류-코드">오류 코드</h3>
<p>보통 drag&amp;drop을 구현할 때 사용하는 ItemTouchHelper의 콜백 클래스다. 우리가 주목해야할 부분인 <code>onMove()</code>에서 현재 포지션(from)과 옮길 포지션(to)을 구해서 Collections.swap으로 List에 있는 from과 to의 순서를 바꾸고 adapter의 diffUtil을 이용해서 (<code>adapter.differ.submitList(List)</code>) adapter에 데이터 변경 사항을 반영한다. (전체 코드 및 설명은 아래 링크 참조)</p>
<blockquote>
<p>*관련 포스팅 : <a href="https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95">[Android][Kotlin] RecyclerView의 모든것 (기본 사용법, Diffutil로 데이터 변화 시 UI 반영,Drag&amp;Drop으로 아이템 순서 변경, Swipe 후 아이템 삭제)</a> <br>*전체 코드 : <a href="https://github.com/park-chris/RecyclerView">github 주소</a></p>
</blockquote>
<pre><code class="language-kotlin">class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
    ...
    interface OnItemMoveListener {
        fun onItemMove(from: Int, to: Int)
    }

    private var listener: OnItemMoveListener? = null

    fun setOnItemMoveListener(listener: OnItemMoveListener) {
        this.listener = listener
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {

        // 어댑터 획득
        val adapter = recyclerView.adapter as UserAdapter

        // 현재 포지션 획득
        val fromPosition = viewHolder.absoluteAdapterPosition

        // 옮길 포지션 획득
        val toPosition = target.absoluteAdapterPosition

        // adapter가 가지고 있는 현재 리스트 획득
        val list = arrayListOf&lt;User&gt;()
        list.addAll(adapter.differ.currentList)

        // 리스트 순서 바꿈
        Collections.swap(list, fromPosition, toPosition)

        // adapter.notifyItemMoved(fromPosition, toPosition)
        adapter.differ.submitList(list)

        // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
        listener?.onItemMove(fromPosition, toPosition)

        return true
    }
    ...
}</code></pre>
<p><br><br><br><br></p>
<h1 id="drag--drop-시-순서-섞이는-현상-해결방법">Drag &amp; Drop 시 순서 섞이는 현상 해결방법</h1>
<p>Drag &amp; Drop 시 순서가 섞이는 현상을 해결하기위해서는 위의 오류 코드에서 <code>onMove()</code>의 <code>Collections.swap(list, fromPosition, toPosition)</code>을 아래와 같이 수정해준다.</p>
<pre><code class="language-kotlin">override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {
    ...
    // 리스트 순서 바꿈
    if (fromPosition &lt; toPosition) {
        for (i in fromPosition until toPosition) {
            Collections.swap(list, i, i + 1)
        }
    } else {
        for (i in fromPosition downTo toPosition + 1) {
            Collections.swap(list, i, i - 1)
        }
    }

    ...
}</code></pre>
<p>만약 우리가 0번 아이템을 3번으로 옮긴다면, fromPosition보다 toPosition이 크므로 if문에 걸려서 for문에 의해 0에서 2까지 반복하며 순차적으로 바꿔줄것이다. 반대로 3번 아이템을 0번으로 옮기면, fromPosition이 toPosition보다 커서 else문에 걸리게 된다. 그렇다면 for문에 의해 3부터 1까지 반복하며 바꿔준다.</p>
<br>

<p>이렇게 하면 아무리 빨리 drag &amp; drop해도 RecyclerView의 리스트가 이상하게 섞이지않는다. </p>
<p><br><br><br><br></p>
<h1 id="index-0-아이템을-drag-시-스크롤이-빨리-내려가는-현상-해결방법">index 0 아이템을 drag 시 스크롤이 빨리 내려가는 현상 해결방법</h1>
<p>RecyclerView의 adapter에 adapterDataObserver를 등록하여 fromPosition이나 toPosition이 0일 때는 RecyclerView의 scroll이 0에 있도록 하면 된다. adapterDataObserver 등록은 RecyclerView를 초기화할 때나 adapter를 초기화할 때 하면 된다. </p>
<pre><code class="language-kotlin">adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
        if (fromPosition == 0 || toPosition == 0) {
            binding.recyclerview.scrollToPosition(0)
        }
    }
})</code></pre>
<p><br><br><br></p>
<h1 id="결과물">결과물</h1>
<img src="https://velog.velcdn.com/images/chris_seed/post/9d06004a-c1a4-4014-8006-ce568e8c6f46/image.gif">

<p><br><br><br><br></p>
<h1 id="전체-코드">전체 코드</h1>
<h3 id="itemtouchsimplehelperkt">ItemTouchSimpleHelper.kt</h3>
<pre><code class="language-kotlin">class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
    ...
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {

        // 어댑터 획득
        val adapter = recyclerView.adapter as UserAdapter

        // 현재 포지션 획득
        val fromPosition = viewHolder.absoluteAdapterPosition

        // 옮길 포지션 획득
        val toPosition = target.absoluteAdapterPosition

        // adapter가 가지고 있는 현재 리스트 획득
        val list = arrayListOf&lt;User&gt;()
        list.addAll(adapter.differ.currentList)

/*  수정 전
        // 리스트 순서 바꿈
        Collections.swap(list, fromPosition, toPosition)
*/

/* 수정 후 */
        // 리스트 순서 바꿈
        if (fromPosition &lt; toPosition) {
            for (i in fromPosition until toPosition) {
                Collections.swap(list, i, i + 1)
            }
        } else {
            for (i in fromPosition downTo toPosition + 1) {
                Collections.swap(list, i, i - 1)
            }
        }

        // adapter.notifyItemMoved(fromPosition, toPosition)
        adapter.differ.submitList(list)

        // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
        listener?.onItemMove(fromPosition, toPosition)

        return true
    }
    ...
}</code></pre>
<br>

<h3 id="mainactivitykt">MainActivity.kt</h3>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: UserAdapter
    private var userList = mutableListOf&lt;User&gt;()
    private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
    private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // RecyclerView에 사용할 리스트 제공
        for (i in 1 until 11) {
            val mUser = User(i, &quot;user$i&quot;)
            userList.add(mUser)
        }
    }

    override fun onResume() {
        super.onResume()

        initRecyclerView()
        setupEvents()

    }

    private fun initRecyclerView() {
        // RecyclerView에 리스트 추가 및 어댑터 연결
        adapter = UserAdapter(this)
        binding.recyclerview.layoutManager = LinearLayoutManager(this)
        binding.recyclerview.adapter = adapter

        // DiffUtil 적용 후 데이터 추가
        adapter.differ.submitList(userList)

        // itemTouchSimpleCallback 인터페이스로 추가 작업
        itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
            override fun onItemMove(from: Int, to: Int) {
                // Collections.swap(userList, from, to) 처럼 from, to가 필요하다면 사용
                Log.d(&quot;MainActivity&quot;, &quot;from Position : $from, to Position : $to&quot;)
            }
        })

        // itemTouchHelper와 recyclerview 연결
        itemTouchHelper.attachToRecyclerView(binding.recyclerview)

        // RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경
        binding.recyclerview.setOnTouchListener { _, _ -&gt;
            itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview)
            false
        }

        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
                if (fromPosition == 0 || toPosition == 0) {
                    binding.recyclerview.scrollToPosition(0)
                }
            }
        })

    }

    private fun setupEvents() {
        // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
        binding.addButton.setOnClickListener {

            // 추가할 데이터 생성
            val mUser = User(userList.size+1, &quot;added user ${userList.size+1}&quot;)

            // differ의 현재 리스트를 받아와서 newList에 넣기
            val newList = adapter.differ.currentList.toMutableList()

            // newList에 생성한 유저 추가
            newList.add(mUser)

            // adapter의 differ.submitList()로 newList 제출
            // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
            adapter.differ.submitList(newList)

            // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
            // userList = adapter.differ.currentList 이렇게 사용하면 안됨
            userList.add(mUser)

            // 추가 메시지 출력
            Toast.makeText(this, &quot;${mUser.name}이 추가되었습니다.&quot;, Toast.LENGTH_SHORT).show()

            // 추가된 포지션으로 스크롤 이동
            binding.recyclerview.scrollToPosition(newList.indexOf(mUser))
        }
    }
}</code></pre>
<p><br><br><br><br></p>
<h1 id="마치며">마치며</h1>
<p>전체 코드를 보거나, 수정 부분만 보고 싶다면 아래 링크의 커밋을 참고하면 된다.</p>
<p><a href="https://github.com/park-chris/RecyclerView">전체 코드 Github 주소</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] google play store에서 앱 버전 업데이트 확인]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-google-play-store%EC%97%90%EC%84%9C-%EC%95%B1-%EB%B2%84%EC%A0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%99%95%EC%9D%B8</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-google-play-store%EC%97%90%EC%84%9C-%EC%95%B1-%EB%B2%84%EC%A0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%99%95%EC%9D%B8</guid>
            <pubDate>Mon, 30 Jan 2023 02:11:30 GMT</pubDate>
            <description><![CDATA[<p>우리가 google play store에 앱을 등록하고 난 후 버그가 발생하거나 신규 기능이 추가되는 등의 이유로 앱을 업데이트를 해야 된다. 그때, 아무런 조치가 없다면 앱을 설치한 사용자는 구글 플레이 스토어에 들어가서 업데이트가 있는지 확인하지 않는 이상은 업데이트가 있는지조차 모를 수 있다.
<br>
이런 일을 방지하기 위해 앱이 시작될때 google play store에 업데이트가 가능한 버전이 있는지 확인하는 코드를 넣는다. 이것을 하기위해 나도 구글링하여 관련 글들을 확인하는데 다양한 방법들이 있었다. 보통의 방법은 구글 플레이 스토어의 페이지를 파싱해서 버전 코드 부분만 가지고 오는 경우가 있는데 이것은 한 가지 문제가 있다. 구글 플레이 스토어의 웹 페이지 태그가 바뀌는 경우, 파싱을 해오지 못한다는 것이다. 그래서 구글 플레이 스토어의 웹 UI가 바뀌면 태그가 바뀌었는지 확인 후 코드를 수정해야한다.</p>
<br>

<p>나는 저렇게 귀찮게는 하기싫었다. 그리고 분명 업데이트 관련 기능이 있을텐데라는 생각을 가지고 희망회로를 돌리면서 공식 사이트(Android Developers)에서 찾아보았다. </p>
<br>

<p>역시 있었다. Android Developers의 가이드에 보면 인앱 업데이트에서 내용을 확인할 수 있다. Google Play Core 라이브러리 안에 Play In-App Update라는 라이브러리를 이용하여 구글 플레이 스토어에서 앱의 유효한 업데이트가 있는지 확인 가능하다. 링크는 아래 참고문헌에 있다.</p>
<p><br><br><br><br><br></p>
<h1 id="google-play-store에-앱-업데이트-확인-방법">google play store에 앱 업데이트 확인 방법</h1>
<ol>
<li>build.gradle(:app)에 Gradle 종속 항목을 포함시킨 후 <code>sync now..</code> 클릭 (아래는 Kotlin일 경우이고 Java일 경우는 뒤에 <code>-ktx</code> 제거)<pre><code class="language-kotlin"> dependencies {
     ...
     implementation(&quot;com.google.android.play:app-update-ktx:2.0.1&quot;)
 }
</code></pre>
</li>
</ol>
<br>

<ol start="2">
<li>AppUpdateManager의 appUpdateInfo로 정보를 받아오는 걸 성공했을 때 이 appUpdateInfo에서 업데이트가 가능한지, 버전 코드는 몇인지 확인 가능하다.<pre><code class="language-kotlin"> val appUpdateManager = AppUpdateManagerFactory.create(this)
  val appUpdateInfoTask = appUpdateManager.appUpdateInfo
 appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -&gt;
     if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &amp;&amp; appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
         // 유효한 업데이트가 있을 때
         // ex. 스토어에서 앱 업데이트하기
     } else {
         // 유효한 업데이트가 없을 때
         // 바로 로그인 화면이나 홈 화면 등으로 이동
     }
 }
</code></pre>
</li>
</ol>
<br>




<p><br><br><br><br><br></p>
<h1 id="참고문헌">참고문헌</h1>
<ul>
<li><a href="https://developer.android.com/guide/playcore/in-app-updates/kotlin-java?hl=ko">Adnroid Developers 인앱 업데이트 지원</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.]]></title>
            <link>https://velog.io/@chris_seed/Android-number</link>
            <guid>https://velog.io/@chris_seed/Android-number</guid>
            <pubDate>Fri, 16 Dec 2022 03:56:12 GMT</pubDate>
            <description><![CDATA[<h1 id="에러">에러</h1>
<hr>
<blockquote>
<p>java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you&#39;ve changed schema but forgot to update the version number. You can simply fix this by increasing the version number.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/chris_seed/post/0a794a50-f912-462e-b05d-d9aaf5eaa2b4/image.png" alt=""></p>
<p><br><br><br><br></p>
<h1 id="에러-발생-이유">에러 발생 이유</h1>
<hr>
<p>Room을 사용할 때 schema의 변경이 있으면 업데이트 해줘야하는데 안해줘서 그렇다. </p>
<p><br><br><br><br></p>
<h1 id="해결-방법">해결 방법</h1>
<hr>
<p>이 경우 database의 클래스 버전을 높여서 Room이 데이터베이스를 새 버전으로 이행(migration)하게 해야한다.</p>
<h2 id="1-database-migration-생성">1. Database migration 생성</h2>
<p><strong>생성 전 코드</strong></p>
<pre><code class="language-kotlin">@Database(entities = [Sound::class], version = 1)
abstract class SoundRoomDatabase : RoomDatabase() {

    abstract fun soundRoomDao(): SoundRoomDao

}</code></pre>
<br>

<p><strong>생성 후 코드</strong></p>
<pre><code class="language-kotlin">@Database(entities = [Sound::class], version = 2)
abstract class SoundRoomDatabase : RoomDatabase() {

    abstract fun soundRoomDao(): SoundRoomDao

}

val migration_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            &quot;ALTER TABLE Sound Add COLUMN isChecked INTEGER NOT NULL DEFAULT 0&quot;
        )
    }

}</code></pre>
<ul>
<li>database의 version을 바꾸고 migration을 하기위해 변수 <code>migration_1_2</code>를 선언한다.</li>
<li>변수 <code>migration_1_2</code>는 database의 변화를 적용한다. <code>Migration(이전버전, 이후버전)</code>을 적어주고 migrate를 상속받아와서 변화가 생긴 database의 컬럼이나 관련 스키마를 적어주면 된다.</li>
<li>나는 Sound라는 데이터에 isChecked라는 Boolean값의 컬럼을 추가해주었다.</li>
</ul>
<p><br><br></p>
<h2 id="2-database-인스턴스화할-때-migration-객체-제공">2. Database 인스턴스화할 때 Migration 객체 제공</h2>
<p><strong>수정 전 코드</strong></p>
<pre><code class="language-kotlin">class SoundRepository private constructor(context: Context) {

    private val roomDatabase: SoundRoomDatabase = Room.databaseBuilder(
        context.applicationContext,
        SoundRoomDatabase::class.java,
        ROOM_DATABASE_NAME
    ).build()
}</code></pre>
<br>

<p><strong>수정 후 코드</strong></p>
<pre><code class="language-kotlin">
class SoundRepository private constructor(context: Context) {

    private val roomDatabase: SoundRoomDatabase = Room.databaseBuilder(
        context.applicationContext,
        SoundRoomDatabase::class.java,
        ROOM_DATABASE_NAME
    ).addMigrations(migration_1_2).build()
}    </code></pre>
<p><br><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] BottomSheet로 내리고 올릴 수 있는 뷰 만들기]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-BottomSheet%EB%A1%9C-%EC%84%A0%ED%83%9D%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-BottomSheet%EB%A1%9C-%EC%84%A0%ED%83%9D%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 12 Dec 2022 15:59:38 GMT</pubDate>
            <description><![CDATA[<p>오늘은 잘 사용하면 UI도 다채롭고 재활용도 쉬운 BottomSheet를 정리한다.</p>
<p><br><br><br><br></p>
<h1 id="bottomsheet-종류">BottomSheet 종류</h1>
<blockquote>
<p>우선 만들기전에 BottomSheet의 종류를 알아보자. BottomSheet는 두 종류가 있다.</p>
</blockquote>
<p><strong>Persistent bottomSheet</strong></p>
<ul>
<li>화면상에 존재하면서 위아래로 슬라이드 할 수 있다.</li>
<li>미리보기가 가능하다.<img src="https://velog.velcdn.com/images/chris_seed/post/818ed704-8582-42b1-9d2e-4594cbf862e4/image.gif">


</li>
</ul>
<br>

<p><strong>Modal bottomSheet</strong></p>
<ul>
<li>dialog식으로 불러낸다.<img src="https://velog.velcdn.com/images/chris_seed/post/0dd85539-5008-4e90-9334-c4b368c52f48/image.gif">

</li>
</ul>
<p><br><br><br><br></p>
<h1 id="persistent-bottomsheet-구현">Persistent BottomSheet 구현</h1>
<br>

<ol>
<li>/app/res/drawable 디렉토리에 BottomSheet의 background을 그려줄 xml파일을 추가한다.</li>
</ol>
<p><br><strong>/app/res/drawable/background_bottom_sheet.xml</strong>
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"></p>
<pre><code>    &lt;solid
        android:color=&quot;@color/black&quot;/&gt;

    &lt;corners
        android:topLeftRadius=&quot;40dp&quot;
        android:topRightRadius=&quot;40dp&quot; /&gt;

    &lt;padding
        android:left=&quot;10dp&quot;
        android:right=&quot;10dp&quot;
        android:top=&quot;10dp&quot;
        android:bottom=&quot;10dp&quot; /&gt;

&lt;/shape&gt;</code></pre><p><br><strong>결과물</strong>
<img src="https://velog.velcdn.com/images/chris_seed/post/2a358b13-0671-4e54-97a4-7c49465f971a/image.png" width="300px" /></p>
<br>

<ol start="2">
<li><p>/app/res/layout 디렉토리에 BottomSheet의 view를 그리는 xml파일을 추가한다.
<br><strong>/app/res/layout/bottom_sheet.xml</strong></p>
 <?xml version="1.0" encoding="utf-8"?>
<p> &lt;LinearLayout xmlns:android=&quot;<a href="http://schemas.android.com/apk/res/android&quot;">http://schemas.android.com/apk/res/android&quot;</a></p>
<pre><code> xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
 android:id=&quot;@+id/bottom_sheet_layout&quot;
 android:layout_width=&quot;match_parent&quot;
 android:layout_height=&quot;300dp&quot;
 android:background=&quot;@drawable/background_bottom_sheet&quot;
 app:behavior_hideable=&quot;true&quot;
 app:behavior_peekHeight=&quot;50dp&quot;
 android:orientation=&quot;vertical&quot;
 android:padding=&quot;10dp&quot;
 android:clickable=&quot;true&quot;
 android:focusable=&quot;true&quot;
 app:layout_behavior=&quot;com.google.android.material.bottomsheet.BottomSheetBehavior&quot;&gt;

 &lt;TextView
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:gravity=&quot;center&quot;
     android:textColor=&quot;@color/white&quot;
     android:text=&quot;Persistent BottomSheet&quot;
     android:textStyle=&quot;bold&quot;
     android:layout_margin=&quot;10dp&quot;
     android:textSize=&quot;20sp&quot; /&gt;

 &lt;Button
     android:id=&quot;@+id/expand_persistent_button&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:padding=&quot;10dp&quot;
     android:text=&quot;확장하기&quot;
     android:textSize=&quot;15sp&quot; /&gt;

 &lt;Button
     android:id=&quot;@+id/hide_persistent_button&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:padding=&quot;10dp&quot;
     android:text=&quot;숨기기&quot;
     android:textSize=&quot;15sp&quot;  /&gt;

 &lt;Button
     android:id=&quot;@+id/show_modal_button&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:padding=&quot;10dp&quot;
     android:text=&quot;Modal BottomSheet 열기&quot;
     android:textSize=&quot;15sp&quot;  /&gt;</code></pre> </LinearLayout>
<br>**결과물**
<img src="https://velog.velcdn.com/images/chris_seed/post/6f35affa-1990-4fb5-af06-1823f65c325c/image.png" width="300px" /></li>
</ol>
<p><strong>관련 옵션</strong>
    - app:behavior_hideable : bottomSheet 숨기기 가능 유무
    - app:behavior_peekHeight : 미리보기 상태로 제일 처음 bottomSheet의 크기
    - app:layout_behavior=&quot;com.google.android.material.bottomsheet.BottomSheetBehavior&quot; : CoordinatorLayout에서 자식 뷰에 대한 플러그인 중 하나다. 이 옵션을 자식 뷰의 app:layout_behavior에서 설정해주면 하단에서 펼쳐지는 방식으로 자식 뷰가 동작한다.</p>
<br>

<ol start="3">
<li><p>activity_main.xml에 1,2번에서 만든 BottomSheet를 include해준다.
<br><strong>/app/java/res/activity_main.xml</strong></p>
 <?xml version="1.0" encoding="utf-8"?>
 <layout>
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity">

<pre><code> &lt;Button
     android:id=&quot;@+id/show_button&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;wrap_content&quot;
     android:layout_margin=&quot;10dp&quot;
     android:text=&quot;bottomSheet 위로 올리기&quot; /&gt;

 &lt;include
     layout=&quot;@layout/bottom_sheet&quot; /&gt;</code></pre><p> &lt;/androidx.coordinatorlayout.widget.CoordinatorLayout&gt;</p>
 </layout>
<br>**결과물**
<img src="https://velog.velcdn.com/images/chris_seed/post/63a8fe4d-0a71-4dcf-b7fa-f151a2426a81/image.png" width="300px" />


</li>
</ol>
<br>

<ol start="4">
<li><p>MainActivity.kt에 BottomSheet 초기화 및 이벤트를 넣어준다. 자세한 설명은 주석 확인
<br><strong>/app/java/패키지/MainActivity.kt</strong></p>
<pre><code class="language-kotlin"> class MainActivity : AppCompatActivity() {

     // 데이터 바인딩
     private var _binding: ActivityMainBinding? = null
     private val binding get() = _binding!!

     // BottomSheet layout 변수
     private val bottomSheetLayout by lazy { findViewById&lt;LinearLayout&gt;(R.id.bottom_sheet_layout) }
     private val bottomSheetExpandPersistentButton by lazy { findViewById&lt;Button&gt;(R.id.expand_persistent_button) }
     private val bottomSheetHidePersistentButton by lazy { findViewById&lt;Button&gt;(R.id.hide_persistent_button) }
     private val bottomSheetShowModalButton by lazy { findViewById&lt;Button&gt;(R.id.show_modal_button) }

     // bottomSheetBehavior
     private lateinit var bottomSheetBehavior: BottomSheetBehavior&lt;LinearLayout&gt;

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         _binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

         initializePersistentBottomSheet()
         persistentBottomSheetEvent()

     }

     override fun onResume() {
         super.onResume()

         binding.showButton.setOnClickListener {
             // BottomSheet의 peek_height만큼 보여주기
             bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
         }
     }

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

</code></pre>
</li>
</ol>
<pre><code>    // Persistent BottomSheet 초기화
    private fun initializePersistentBottomSheet() {

        // BottomSheetBehavior에 layout 설정
        bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout)

        bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {

                // BottomSheetBehavior state에 따른 이벤트
                when (newState) {
                    BottomSheetBehavior.STATE_HIDDEN -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: hidden&quot;)
                    }
                    BottomSheetBehavior.STATE_EXPANDED -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: expanded&quot;)
                    }
                    BottomSheetBehavior.STATE_COLLAPSED -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: collapsed&quot;)
                    }
                    BottomSheetBehavior.STATE_DRAGGING -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: dragging&quot;)
                    }
                    BottomSheetBehavior.STATE_SETTLING -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: settling&quot;)
                    }
                    BottomSheetBehavior.STATE_HALF_EXPANDED -&gt; {
                        Log.d(&quot;MainActivity&quot;, &quot;state: half expanded&quot;)
                    }
                }

            }

            override fun onSlide(bottomSheet: View, slideOffset: Float) {
            }

        })

    }

    // PersistentBottomSheet 내부 버튼 click event
    private fun persistentBottomSheetEvent() {

        bottomSheetExpandPersistentButton.setOnClickListener {
            // BottomSheet의 최대 높이만큼 보여주기
            bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
        }

        bottomSheetHidePersistentButton.setOnClickListener {
            // BottomSheet 숨김
            bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
        }

        bottomSheetShowModalButton.setOnClickListener {
            // 추후 modal bottomSheet 띄울 버튼
        }

    }


}</code></pre><pre><code>
&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;

# Persistent BottomSheet 구현 결과

- `확장하기 버튼`을 클릭하면 Persistent BottomSheet를 원래 사이즈로 확장한다.
- `숨기기 버튼`을 클릭하면 Persistent BottomSheet를 숨긴다.
- `BOTTOMSHEET 위로 올리기 버튼`을 클릭하면 Persistent BottomSheet를 behavior_peekHeight의 크기만큼 확장한다. 

&lt;p align=&quot;center&quot;&gt;
&lt;img src=&quot;https://velog.velcdn.com/images/chris_seed/post/818ed704-8582-42b1-9d2e-4594cbf862e4/image.gif&quot;&gt;
&lt;/p&gt;


&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;


# Modal BottomSheet 구현

&lt;br&gt;

1. Modal BottomSheet가 생기고 사라질때 적용할 애니메이션을 /app/res/anim 디렉토리에 애니메이션 파일을 2개 추가한다. 
(anim 디렉토리가 없으면 디렉토리 생성 후 파일을 추가)
&lt;br&gt;**modal_slide_in_bottom.xml**
    &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
    &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

        &lt;translate
            android:duration=&quot;@android:integer/config_mediumAnimTime&quot;
            android:fromYDelta=&quot;100%p&quot;
            android:toYDelta=&quot;0&quot; /&gt;

    &lt;/set&gt;
&lt;br&gt;**modal_slide_out_bottom.xml**
    &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
    &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

        &lt;translate
            android:duration=&quot;@android:integer/config_mediumAnimTime&quot;
            android:fromYDelta=&quot;0&quot;
            android:toYDelta=&quot;100%p&quot; /&gt;

    &lt;/set&gt;

&lt;br&gt;

2. /app/res/layout 디렉토리에 Modal BottomSheet의 view를 그리는 xml파일을 추가한다.


3. /app/res/values/themes 디렉토리에 있는 themes.xml, themes.xml (night) 두 파일에 Modal BottomSheet의 애니메이션을 style로 등록해준다.
&lt;br&gt;**themes.xml, themes.xml (night)**

    &lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;
        ...
        &lt;style name=&quot;DialogAnimation&quot;&gt;
            &lt;item name=&quot;android:windowEnterAnimation&quot;&gt;@anim/modal_slide_in_bottom&lt;/item&gt;
            &lt;item name=&quot;android:windowExitAnimation&quot;&gt;@anim/modal_slide_out_bottom&lt;/item&gt;
        &lt;/style&gt;
    &lt;/resources&gt;


&lt;br&gt;

4. /app/java/패키지/MainActivity.kt에서 ModalBottomSheet에 관련된 코드를 넣는다.
&lt;br&gt;**MainActivity.kt**
```kotlin
    class MainActivity : AppCompatActivity() {
        ...

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

        override fun onResume() {
            ...
        }

        override fun onDestroy() {
            ...
        }

        // Persistent BottomSheet 초기화
        private fun initializePersistentBottomSheet() {
            ...
        }

        // PersistentBottomSheet 내부 버튼 click event
        private fun persistentBottomSheetEvent() {
            ...
            bottomSheetShowModalButton.setOnClickListener {
                // Modal BottomSheet 띄우기
                showModalBottomSheet()
            }
        }

        // Modal BottomSheet 띄우기
        private fun showModalBottomSheet() {

            val dialog: Dialog = Dialog(this)
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
            dialog.setContentView(R.layout.modal_bottom_sheet)

            // modal_bottom_sheet.xml의 dismiss_button로 변수 초기화
            val dismissButton: Button = dialog.findViewById(R.id.dismiss_button)

            // dismiss_button 클릭 시 Modal BottomSheet 닫기
            dismissButton.setOnClickListener {
                dialog.dismiss()
            }

            // Modal BottomSheet 크기
            dialog.window?.setLayout(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )

            // Modal BottomSheet의 background를 제외한 부분은 투명
            dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            dialog.window?.attributes?.windowAnimations = R.style.DialogAnimation
            dialog.window?.setGravity(Gravity.BOTTOM)

            // Modal BottomSheet 보여주기
            dialog.show()
        }
    }</code></pre><p><br><br><br><br></p>
<h1 id="modal-bottomsheet-구현-결과">Modal BottomSheet 구현 결과</h1>
<p align="center">
<img src="https://velog.velcdn.com/images/chris_seed/post/0dd85539-5008-4e90-9334-c4b368c52f48/image.gif">
</p>




<p><br><br><br><br>
<br><br><br><br></p>
<h1 id="마치며">마치며</h1>
<p>google에 bottomsheet를 검색하면 다양한 응용법들이 있다.
사람들이 어떤 방식으로 사용하는지 보고 적용하면 좋을듯하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] RecyclerView에서 데이터 변화 시 이미지 깜박임 제거]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%ED%99%94-%EC%8B%9C-Glide-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B9%9C%EB%B0%95%EC%9E%84-%EC%A0%9C%EA%B1%B0</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%ED%99%94-%EC%8B%9C-Glide-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B9%9C%EB%B0%95%EC%9E%84-%EC%A0%9C%EA%B1%B0</guid>
            <pubDate>Wed, 09 Nov 2022 02:27:29 GMT</pubDate>
            <description><![CDATA[<p>RecyclerView를 사용할 때 데이터가 변하면 그것을 adapter에 반영해줘야한다. 그 과정에서 RecyclerView의 아이템 자체가 깜박거리는 것은 animator를 false로 설정하면 되지만 Glide로 로드한 이미지는 여전히 깜박거린다.</p>
<p>아래 그것에 대해 내가 찾은 방법을 적는다.</p>
<p><br><br><Br></p>
<h1 id="recyclerview-아이템-깜박임-제거">RecyclerView 아이템 깜박임 제거</h1>
<p>우선 RecyclerView의 아이템 자체가 깜박거리는 것은 RecyclerView에 아래 코드를 추가하면 된다. 애니메이션을 끄는 코드다.</p>
<pre><code class="language-kotlin">recyclerView.apply {
        ...
        itemAnimator = null
}</code></pre>
<p><br><Br><br></p>
<h1 id="glide-이미지-깜박임-제거">Glide 이미지 깜박임 제거</h1>
<p>RecyclerView 아이템의 이미지에 Glide로 로드했다. 그리고 diffUtil이나 NotifyDataSetChanged()등으로 데이터가 변경되면 adapter에 변경사항을 알리고 adapter는 변경 내용을 반영한다. </p>
<p>아래 이미지에서는 이미지나 제목의 변화는 없지만 클릭 시 가지고 있는 데이터의 isPlaying이라는 필드에서 변화가 생겨서 그것을 adapter가 반영해준다. 그리고 반영 때 image가 Glide를 통해 다시 로드되며 깜박이는 것을 볼 수 있다.</p>
<p>심히 거슬린다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/e2a3ca85-ad5b-439c-a0d4-44c8fc0dff5c/image.gif">


<p><br><br></p>
<p>  아래 코드는 솔루션 적용 전 코드다.
  ViewHolder 클래스 내의 함수 bind를 통해 각 아이템에 맞는 제목과 이미지를 넣는다. 
  Glide를 통해 이미지를 로드하고 있다.</p>
<pre><code class="language-kotlin">inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {

    fun bind(sound: Sound) {
        val trackTextView =itemView.findViewById&lt;TextView&gt;(R.id.item_track_text_view)
        val categoryTextView =itemView.findViewById&lt;TextView&gt;(R.id.item_category_text_view)
        val coverImageView =itemView.findViewById&lt;ImageView&gt;(R.id.item_cover_image_view)

        trackTextView.text = &quot;제목 &quot;
        categoryTextView.text = &quot;부제목 &quot;

        Glide.with(context)
            .load(sound.imgUrl)
            .override(100, 100)
            .skipMemoryCache(true)
            .dontAnimate()
            .into(coverImageView)

    }

}</code></pre>
<p>  <br><br></p>
<p>  이미지 깜박거림 제거를 위해서는 Glide 옵션에 thumnail을 추가하는 것이다.</p>
<pre><code class="language-kotlin">Glide.with(context)
    .load(sound.imgUrl)
    .thumbnail(Glide.with(context).load(sound.imgUrl).override(100, 100))
    .override(100, 100)
    .skipMemoryCache(true)
    .dontAnimate()
    .into(coverImageView)</code></pre>
<p>  <br><br></p>
<p>  코드를 적용하고 나면 아래처럼 깜박거림이 사라진다.</p>
  <img src="https://velog.velcdn.com/images/chris_seed/post/6e669e0e-7c10-48e2-a3bd-2a08554c8224/image.gif">



<p><br><br><br></p>
<h1 id="또-다른-방법">또 다른 방법</h1>
<p>나는 사용하지 않았지만, Adapter 클래스 내에서 getItemViewType(), getItemId()를 오버라이딩받아서 아이템마다 아이디를 부여하는 방법도 있다.</p>
<p><strong>Adapter</strong>  </p>
<pre><code class="language-kotlin">class PlayListAdapter(
    private val context: Context
    ) : RecyclerView.Adapter&lt;PlayListAdapter.ViewHolder&gt;()  {

    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {

        fun bind(sound: Sound) {
            val trackTextView =itemView.findViewById&lt;TextView&gt;(R.id.item_track_text_view)
            val categoryTextView =itemView.findViewById&lt;TextView&gt;(R.id.item_category_text_view)
            val coverImageView =itemView.findViewById&lt;ImageView&gt;(R.id.item_cover_image_view)

             trackTextView.text = &quot;제목&quot;
             categoryTextView.text = &quot;부제목&quot;

            Glide.with(context)
                .asBitmap()
                .load(sound.imgUrl)
                .override(100, 100)
                .skipMemoryCache(true)
                .dontAnimate()
                .into(object : CustomTarget&lt;Bitmap&gt;() {
                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition&lt;in Bitmap&gt;?
                    ) {
                        coverImageView.setImageBitmap(resource)
                    }

                    override fun onLoadCleared(placeholder: Drawable?) {
                    }

                })

        }

    }

  ...

    override fun getItemViewType(position: Int): Int {
        return position
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

}
</code></pre>
<p>  <br><br></p>
<p>  adapter를 초기화할때  초기화 후 꼭 <code>setHasStableIds(true)</code>를 넣어줘야한다. RecyclerView에 adapter를 붙이고 나서 setHasStableIds()를 선언하면 에러나니까 붙이기 전에 넣어준다.</p>
<p>  <strong>Activity</strong></p>
<pre><code class="language-kotlin">val playListAdapter = PlayListAdapter(requireContext())
playListAdapter.setHasStableIds(true)</code></pre>
<p><br><br><br><br></p>
<h1 id="마치며">마치며...</h1>
<p>getViewType()을 사용하면 RecyclerView를 drag&amp;drop할때 이상하게 되는 현상이 발생한다. 이 부분은 나중에 한번 알아봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Mac] brew 설치, 환경 변수 설정, 명령어]]></title>
            <link>https://velog.io/@chris_seed/Mac-brew-%EC%84%A4%EC%B9%98-%EB%B0%8F-PATH-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@chris_seed/Mac-brew-%EC%84%A4%EC%B9%98-%EB%B0%8F-PATH-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 19 Oct 2022 02:36:33 GMT</pubDate>
            <description><![CDATA[<p>Mac을 쓰면서 편하다고 느낀 HomeBrew를 설치하는 방법을 소개한다.</p>
<p><br><br><br></p>
<h1 id="homebrew-그게-뭔데">HomeBrew? 그게 뭔데?</h1>
<hr>
<p>HomeBrew는 <strong>패키지 관리자(Package Manager)</strong>이고, 우리가 필요한 프로그램을 손쉽게 설치하는 프로그램이다.</p>
<br>

<p>OS 별 패키지 관리자</p>
<ul>
<li>Linux : yum , apt</li>
<li>Mac : homebrew</li>
</ul>
<p><br><br><br><br></p>
<h1 id="설치하기">설치하기</h1>
<hr>
<ol>
<li><p>Mac에서 터미널을 열고 homebrew 설치 명령어 실행</p>
<pre><code class="language-shell"> /bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</code></pre>
 <br>
</li>
<li><p>homebrew를 환경 변수로 등록</p>
<pre><code class="language-shell"> echo &#39;export PATH=&quot;/opt/homebrew/bin:$PATH&quot;&#39; &gt;&gt; ~/.zshrc</code></pre>
 <br>
</li>
<li><p>환경 변수 반영</p>
<pre><code class="language-shell"> source ~/.zshrc</code></pre>
 <br></li>
<li><p>설치 확인</p>
<pre><code class="language-kotlin"> brew config</code></pre>
 <br>


</li>
</ol>
<p><br><br></p>
<p>HomeBrew 설치가 끝났다!</p>
<p>이제 우리는 더 편하게 프로그램을 설치하고 관리할 수 있다.</p>
<p><br><br><br><br></p>
<h1 id="homebrew-명령어">HomeBrew 명령어</h1>
<hr>
<h2 id="프로그램-설치">프로그램 설치</h2>
<pre><code class="language-shell">brew install &quot;프로그램&quot;</code></pre>
<p><br><br><br><br></p>
<p>명령어는 앞으로도 사용한 것은 정리해서 기록할 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 반복문 (while, do-while, for)]]></title>
            <link>https://velog.io/@chris_seed/Kotlin-%EB%B0%98%EB%B3%B5%EB%AC%B8-while-do-while-for</link>
            <guid>https://velog.io/@chris_seed/Kotlin-%EB%B0%98%EB%B3%B5%EB%AC%B8-while-do-while-for</guid>
            <pubDate>Mon, 10 Oct 2022 08:26:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>코틀린에서는 같은 명령 시퀀스를 주어진 데이터에 대해 수행하거나 주어진 조건이 만족될 때까지 수행하는 세 가지 제어 구조를 제공한다. whlie, do-while, for 문이다.</p>
</blockquote>
<p><br><br><br><br></p>
<h1 id="while-문">while 문</h1>
<ul>
<li>기본 구조<pre><code class="language-kotlin">while (조건문) {
  조건문이 참일 때 실행되는 코드
}</code></pre>
</li>
</ul>
<br>


<ul>
<li>예시 : 실행 시 생성된 랜덤한 수(num)와 입력받은 수가 일치하면 반복문 종료 후 마지막 <code>println(&quot;Right: it&#39;s $num&quot;)</code> 실행<pre><code class="language-kotlin">import kotlin.random.*
</code></pre>
</li>
</ul>
<p>fun main() {
    val num = Random.nextInt(1, 101)
    var guess = 0</p>
<pre><code>while (guess != num) {
    guess = readLine()!!.toInt()
    if (guess &lt; num) println(&quot;Too small&quot;)
    else if (guess &gt; num) println(&quot;Too big&quot;)
}

println(&quot;Right: it&#39;s $num&quot;)</code></pre><p>}</p>
<pre><code>

&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;

# do-while 문

- 기본 구조
```kotlin
do {
    실행 코드
} while (조건문)</code></pre><br>

<ul>
<li><p>do-while 루프는 아래 순서로 실행된다.</p>
<ol>
<li>do와 while 키워드 사이에 있는 루프 몸통(실행 코드) 실행</li>
<li>while 조건문을 평가 후 이 값이 참이면 1번 단계로 되돌아가고, 이 값이 거짓이면 루프 문 다음에 있는 코드 실행</li>
</ol>
</li>
</ul>
<br>

<ul>
<li>do-while에서는 루프 몸통(실행 코드)를 실행한 다음에 조건을 검사하므로 루프 몸통이 최소 한 번은 실행된다.</li>
</ul>
<br>

<ul>
<li><p>예시 : 입력한 숫자의 합을 구하고 0이 입력된다면 반복문이 끝나고 println으로 입력되었던 수의 합을 출력</p>
<pre><code class="language-kotlin">fun main() {
  var sum = 0
  var num: Int

  do {
      num = readLine()!!.toInt()
      sum += num
  } while (num != 0)

  println(&quot;sum: $sum&quot;)
}</code></pre>
</li>
</ul>
<p><br><br><br><br></p>
<h1 id="for-문">for 문</h1>
<ul>
<li>기본 구조<pre><code class="language-kotlin">for (변수 in 배열) {
  실행 코드
}</code></pre>
</li>
</ul>
<br>

<ul>
<li>예시 : 배열 a의 배열 원소의 합</li>
</ul>
<pre><code class="language-kotlin">fun main() {
    val a = IntArray(10) { it*it } // index가 0이면 0*0, 1이면 1*1, 2이면 2*2
    var sum = 0

    for (x in a) {
        sum += x
    }

    println(&quot;sum: $sum&quot;)
}</code></pre>
<p><br><br><br><br></p>
<h1 id="참고">참고</h1>
<ul>
<li>코틀린 완벽 가이드 by 알렉세이 세두노프</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 조건문 (if 문, when 문)]]></title>
            <link>https://velog.io/@chris_seed/Kotlin-%EC%A1%B0%EA%B1%B4%EB%AC%B8-if-%EB%AC%B8-when-%EB%AC%B8</link>
            <guid>https://velog.io/@chris_seed/Kotlin-%EC%A1%B0%EA%B1%B4%EB%AC%B8-if-%EB%AC%B8-when-%EB%AC%B8</guid>
            <pubDate>Mon, 10 Oct 2022 07:28:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>조건문을 사용하면 어떤 조건의 값에 따라 둘 이상의 동작 중 하나를 수행할 수 있다. 코틀린에서는 <em><strong>if</strong></em>,  <strong><em>when</em></strong>이 있다.</p>
</blockquote>
<p><br><br></p>
<h1 id="if-문">if 문</h1>
<ul>
<li><strong>기본 구조</strong><pre><code class="language-kotlin">  if (조건문) {
      조건문이 참일 때 실행되는 코드
  }
  else {
      조건문이 거짓일 때 실행되는 코드
  }</code></pre>
</li>
<li>if문의 조건문의 결과에 따라 실행되는 코드가 한 줄이면 {} 생략가능<pre><code class="language-kotlin">  if (조건문) 조건문이 참일 때 실행되는 코드
  else 조건문이 거짓일 때 실행되는 코드</code></pre>
</li>
</ul>
<br>

<p><strong>if문을 사용하면 불(boolean)식의 결과에 따라 두가지 대안 중 하나를 선택할 수 있다.</strong></p>
<pre><code class="language-kotlin">fun main() {
    println(max(1,3))
}

fun max(a: Int, b: Int): Int {
    if (a &gt; b) return a
    else return b
}</code></pre>
<br>

<p><strong>max() 함수를 아래처럼 바꿀 수 있다.</strong></p>
<pre><code class="language-kotlin">fun max(a: Int, b: Int): Int {
    return if(a &gt; b) {
        a
    }
    else {
        b
    }
}</code></pre>
<br>

<p><strong>또한, kotlin은 if을 식으로 사용할 수 있다.</strong></p>
<pre><code class="language-kotlin">fun max(a: Int, b: Int) = if(a &gt; b) a else b</code></pre>
<p><br><br><br><br></p>
<h1 id="when-문">when 문</h1>
<blockquote>
<p>여러 대안 중 하나를 선택하여 해당하는 코드를 실행할 수 있다.</p>
</blockquote>
<ul>
<li><strong>기본구조</strong><pre><code class="language-kotlin">  when(키워드) {
      조건1 -&gt; {실행코드}
      조건2 -&gt; {실행코드}
      else -&gt; {실행코드}
  }</code></pre>
</li>
<li><strong>실행되는 코드가 한 줄이면 {} 생략가능</strong></li>
</ul>
<p><br><br></p>
<p><strong>when 문을 사용하여 해당하는 숫자 출력</strong></p>
<pre><code class="language-kotlin">fun main() {
    selectNumner(2)
}

fun selectNumner(a: Int) {
    when(a) {
        1 -&gt; println(&quot;숫자 1이 입력됐습니다.&quot;)
        2 -&gt; println(&quot;숫자 2가 입력됐습니다.&quot;)
        3 -&gt; println(&quot;숫자 3이 입력됐습니다.&quot;)
        4 -&gt; println(&quot;숫자 4가 입력됐습니다.&quot;)
        else -&gt; println(&quot;설정하지 않은 숫자입니다.&quot;)
    }
}</code></pre>
<br>

<p><strong>when 문을 식으로 사용하기</strong></p>
<pre><code class="language-kotlin">fun selectNumner(a: Int) =  when(a) {
    1 -&gt; println(&quot;숫자 1이 입력됐습니다.&quot;)
    2 -&gt; println(&quot;숫자 2가 입력됐습니다.&quot;)
    3 -&gt; println(&quot;숫자 3이 입력됐습니다.&quot;)
    4 -&gt; println(&quot;숫자 4가 입력됐습니다.&quot;)
    else -&gt; println(&quot;그 밖에 숫자입니다.&quot;)
}</code></pre>
<br>

<p><strong>when 문에서 조건문 사용하기</strong></p>
<pre><code class="language-kotlin">fun selectNumner(a: Int) =  when {
    a == 1 -&gt; println(&quot;숫자 1이 입력됐습니다.&quot;)
    a == 2 -&gt; println(&quot;숫자 2가 입력됐습니다.&quot;)
    a == 3 -&gt; println(&quot;숫자 3이 입력됐습니다.&quot;)
    a == 4 -&gt; println(&quot;숫자 4가 입력됐습니다.&quot;)
    else -&gt; println(&quot;그 밖에 숫자입니다.&quot;)
}
</code></pre>
<p><br><br><br><br></p>
<h1 id="참고">참고</h1>
<ul>
<li>코틀린 완벽 가이드 by 알렉세이 세두노프</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] RecyclerView의 모든것 (기본 사용법, Diffutil로 데이터 변화 시 UI 반영,Drag&Drop으로 아이템 순서 변경,  Swipe 후 아이템 삭제)]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 13 Sep 2022 13:44:36 GMT</pubDate>
            <description><![CDATA[<p>RecyclerView는 이름처럼 리소스를 재활용한다. ListView의 확장판으로 성능 개선과 기능을 추가한것이다. ReCylerView는 ListView의 getView 함수 대신 ViewHolder를 의무적으로 사용해야하는 점이 다르다. 좀 더 자세한 개념은 구글링하면 많이 나온다.</p>
<p>RecyclerView의 기능을 쓸때마다 구글링하면서 찾는게 귀찮아서 이 포스팅을 작성한다. 미래의 나는 이걸 보고 쉽게 했으면 좋겠다. </p>
<br>

<p>우선 간단한 앱을 만들면서 RecyclerView의 기능을 써볼것이다. 아래는 내가 생각한 UI와 기능이다.</p>
<ul>
<li>아이템 추가 시 RecyclerView UI에 반영 </li>
<li>아이템 좌우로 드래그 가능</li>
<li>아이템 스와이프로 위치 변경 </li>
<li>아이템 삭제 기능</li>
</ul>
<img src="https://velog.velcdn.com/images/chris_seed/post/847e2c03-23e5-479c-b3e8-ece84dbda17f/image.png" width="300px">


<p><br><br><br></p>
<h1 id="해당-글에서-나는-에러-및-오류-해결-방법">해당 글에서 나는 에러 및 오류 해결 방법</h1>
<ul>
<li><a href="https://velog.io/@chris_seed/AndroidKotlin-RecyclerView%EC%97%90%EC%84%9C-Index-0%EB%B2%88-%EC%95%84%EC%9D%B4%ED%85%9C%EC%9D%84-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C-%EB%92%A4%EB%B0%94%EB%80%9C-%ED%98%84%EC%83%81-%EB%98%90%EB%8A%94-Item%EC%9D%84-%EB%B9%A0%EB%A5%B4%EA%B2%8C-Drag%ED%95%A0-%EB%95%8C-%EC%88%9C%EC%84%9C%EA%B0%80-%EB%92%A4%EB%B0%94%EB%80%8C%EB%8A%94-%ED%98%84%EC%83%81">RecyclerView에서 Index 0번 아이템을 Drag할 때 순서 뒤바뀜과 스크롤이 빠르게 넘어가는 현상, 또는 Item을 빠르게 Drag할 때 순서가 뒤바뀌는 현상</a> 
(업데이트 날짜 : 2023년 01월 30일 11:09 pm)</li>
</ul>
<p><br><br><br><br></p>
<h1 id="recyclerview-기본-사용법">RecyclerView 기본 사용법</h1>
<blockquote>
<p>리스트를 RecyclerView로 띄우기 &amp;&amp; RecyclerView에 넣을 data class 생성</p>
</blockquote>
<h2 id="결과-화면">결과 화면</h2>
<img src="https://velog.velcdn.com/images/chris_seed/post/88b6a241-8ddf-477b-b481-56a104c8b842/image.png" width="300px" />

<br>

<h2 id="구현-방법">구현 방법</h2>
<br>

<ol>
<li><p>build.gradle(:app)에 recyclerview 의존성 추가</p>
<pre><code class="language-kotlin"> dependencies {
     ...
     implementation &#39;androidx.recyclerview:recyclerview:1.2.1&#39;
 }</code></pre>
 <br>
</li>
<li><p>레이아웃에 RecyclerView 추가</p>
<p> <strong>app/res/layout/activity_main.xml</strong></p>
<pre><code class="language-kotlin"> &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
 &lt;layout&gt;
     &lt;androidx.constraintlayout.widget.ConstraintLayout     
         xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
         xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
         xmlns:tools=&quot;http://schemas.android.com/tools&quot;
         android:layout_width=&quot;match_parent&quot;
         android:layout_height=&quot;match_parent&quot;
         tools:context=&quot;.MainActivity&quot;&gt;

         &lt;androidx.recyclerview.widget.RecyclerView
             android:id=&quot;@+id/recyclerview&quot;
             android:layout_width=&quot;0dp&quot;
             android:layout_height=&quot;0dp&quot;
             android:layout_margin=&quot;10dp&quot;
             app:layout_constraintBottom_toTopOf=&quot;@+id/add_button&quot;
             app:layout_constraintEnd_toEndOf=&quot;parent&quot;
                 app:layout_constraintStart_toStartOf=&quot;parent&quot;
             app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;

         &lt;Button
             android:id=&quot;@+id/add_button&quot;
             android:layout_width=&quot;0dp&quot;
             android:layout_height=&quot;wrap_content&quot;
             android:layout_margin=&quot;10dp&quot;
             android:text=&quot;@string/add_item&quot;
             app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
             app:layout_constraintEnd_toEndOf=&quot;parent&quot;
             app:layout_constraintStart_toStartOf=&quot;parent&quot; /&gt;

     &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
 &lt;/layout&gt;</code></pre>
</li>
</ol>
<br>

<ol start="3">
<li><p>RecyclerView의 아이템이 될 Data Class 생성</p>
<p> <strong>app/java/패키지/datas/User.kt</strong></p>
<pre><code class="language-kotlin"> data class User(
     val id: Int,
     val name: String) {
 }</code></pre>
</li>
</ol>
<br>

<ol start="4">
<li><p>리스트에 그려줄 아이템 레이아웃 파일 추가 </p>
<p> <strong>app/res/layout/user_list_item.xml.xml</strong></p>
<pre><code class="language-kotlin"> &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
 &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_margin=&quot;10dp&quot;
     android:layout_height=&quot;wrap_content&quot;&gt;

     &lt;ImageView
         android:id=&quot;@+id/user_image&quot;
         android:layout_width=&quot;50dp&quot;
         android:src=&quot;@mipmap/ic_launcher&quot;
         android:layout_height=&quot;50dp&quot; /&gt;

     &lt;TextView
         android:id=&quot;@+id/user_name_text&quot;
         android:layout_width=&quot;wrap_content&quot;
         android:textSize=&quot;20sp&quot;
         android:gravity=&quot;center&quot;
         android:layout_marginStart=&quot;10dp&quot;
         android:layout_height=&quot;match_parent&quot; /&gt;

 &lt;/LinearLayout&gt;</code></pre>
</li>
</ol>
<br>

<ol start="5">
<li><p>Adapter 클래스 생성
 <strong>app/java/패키지/adapters/UserAdapter</strong></p>
<pre><code class="language-kotlin"> class UserAdapter(
     private val mContext: Context,
     private val mList: MutableList&lt;User&gt;
 ): RecyclerView.Adapter&lt;UserAdapter.ViewHolder&gt;() {

     inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {

         private val userImage: ImageView =     itemView.findViewById(R.id.user_image)
         private val userNameText: TextView = itemView.findViewById(R.id.user_name_text)

         fun bind(user: User) {

             userNameText.text = user.name

         }
     }

     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false)
         return ViewHolder(view)
     }

     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         val user = mList[position]
         holder.bind(user)
     }

     override fun getItemCount() = mList.size

 }</code></pre>
</li>
</ol>
<br>

<ol start="6">
<li><p>코틀린단에서 RecyclerView에 어댑터 연결 
 <strong>app/java/패키지/MainActivity</strong></p>
<pre><code class="language-kotlin">
 class MainActivity : AppCompatActivity() {
     private lateinit var binding: ActivityMainBinding
     private lateinit var adapter: UserAdapter
     private var userList = mutableListOf&lt;User&gt;()

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

         // RecyclerView에 사용할 데이터 리스트 초기화
         for (i in 1 until 11) {
             val mUser = User(i, &quot;user$i&quot;)
             userList.add(mUser)
         }
     }

     override fun onResume() {
         super.onResume()

         // RecyclerView에 리스트 추가 및 어댑터 연결
         adapter = UserAdapter(this, userList)
         binding.recyclerview.layoutManager = LinearLayoutManager(this)
         binding.recyclerview.adapter = adapter

     }
 }</code></pre>
</li>
</ol>
<br>






<p><br><br><br><br></p>
<h1 id="아이템-추가-후-recyclerview-ui-반영">아이템 추가 후 RecyclerView UI 반영</h1>
<blockquote>
<p>diffUtil을 이용한 RecyclerView UI 반영</p>
</blockquote>
<p>DiffUtil은 RecyclerView의 데이터가 변할 때 UI 반영을 효율적으로 해주는 것이다. 기존에 변한 데이터를 UI에 반영할때는 notifyDataSetChanged(), notifyItemChanged()...등등을 사용했을 것이다.</p>
<p>notifyDataSetChanged()는 전체 항목을 변경시켜 RecyclerView에 아이템이 많을수록 효율성이 떨어진다. 그 밖에 <span style="color:blue"> 단일 항목 변경(notifyItemChanged(), notifyItemInserted(),  notifyItemRemoved(),  notifyItemMoved())</span>과 <span style="color:green">범위 변경(notifyItemRangeChanged(), notifyItemRangeInserted(), notifyItemRangeRemoved())</span>이 있다. </p>
<p>DiffUtil은 정말 똑똑하게도 리스트를 하나씩 비교해서 Boolean값을 반환한다. 반환값이 true이면 두 리스트의 동일한 포지션에는 같은 아이템이 존재하는 것이고, false이면 두 리스트의 동일한 포지션에 다른 아이템이거나, 같은 아이템이지만 내용물이 변경된것이다.</p>
<p>이 클래스를 사용하여 효율적인 UI 반영을 처리해보자.</p>
<br>

<h2 id="결과-화면-1">결과 화면</h2>
<p>추가된 아이템이 잘 반영되는 걸 볼 수 있다.
<img src="https://velog.velcdn.com/images/chris_seed/post/5ff0840c-2e73-4018-b6ed-778b13d61d05/image.gif"></p>
<br>

<h2 id="구현-방법-1">구현 방법</h2>
<ol>
<li><p>RecyclerViewAdpater에 DiffUtil의 ItemCallback을 추가하고, onBindViewHolder에 <code>val user = mList[position]</code>을 <code>val user = differ.currentList[position]</code>으로 변경한다.</p>
<p> <strong>app/java/패키지/adapters/UserAdapter.kt</strong></p>
<pre><code class="language-kotlin"> class UserAdapter(
     private val mContext: Context
 ): RecyclerView.Adapter&lt;UserAdapter.ViewHolder&gt;() {

     // 추가
     private val differCallback = object : DiffUtil.ItemCallback&lt;User&gt;() {
         override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
         // User의 id를 비교해서 같으면 areContentsTheSame으로 이동(id 대신 data 클래스에 식별할 수 있는 변수 사용)
             return oldItem.id == newItem.id
         }

         override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
         // User의 내용을 비교해서 같으면 true -&gt; UI 변경 없음
         // User의 내용을 비교해서 다르면 false -&gt; UI 변경
             return oldItem == newItem
         }
     }

     // 리스트가 많으면 백그라운드에서 실행하는 게 좋은데 AsyncListDiffer은 자동으로 백그라운드에서 실행
     val differ = AsyncListDiffer(this, differCallback)

     inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {

         private val userImage: ImageView = itemView.findViewById(R.id.user_image)
         private val userNameText: TextView = itemView.findViewById(R.id.user_name_text)

         fun bind(user: User) {

             userNameText.text = user.name

         }
     }

     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false)
     return ViewHolder(view)
     }

     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         // 수정 전
         //val user = mList[position]

         // 수정 후
         val user = differ.currentList[position]
         holder.bind(user)
     }
     // 수정 전
     // override fun getItemCount() = mList.size

     // 수정 후
     override fun getItemCount() =  differ.currentList.size

</code></pre>
</li>
</ol>
<pre><code>    }</code></pre><br>

<ol start="2">
<li><p>MainActivity에서 <code>differUtil.submitList()</code>을 사용하여 adapter에 데이터를 추가한다. 또한 아이템 추가 버튼에 클릭 이벤트를 추가하여 아이템이 추가될 때 differUtil을 이용하여 Adapter의 UI를 반영한다. </p>
<pre><code class="language-kotlin"> class MainActivity : AppCompatActivity() {

     private lateinit var binding: ActivityMainBinding
     private lateinit var adapter: UserAdapter
     private var userList = mutableListOf&lt;User&gt;()

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

         // RecyclerView에 사용할 리스트 제공
         for (i in 1 until 11) {
             val mUser = User(i, &quot;user$i&quot;)
             userList.add(mUser)
         }

     }

     override fun onResume() {
         super.onResume()

         // RecyclerView에 리스트 추가 및 어댑터 연결
         adapter = UserAdapter(this)
         binding.recyclerview.layoutManager = LinearLayoutManager(this)
         binding.recyclerview.adapter = adapter

         // DiffUtil 적용 후 데이터 추가
         adapter.differ.submitList(userList)

         // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
         binding.addButton.setOnClickListener {

             // 추가할 데이터 생성
             val mUser = User(userList.size+1, &quot;added user ${userList.size+1}&quot;)

             // differ의 현재 리스트를 받아와서 newList에 넣기
             val newList = adapter.differ.currentList.toMutableList()

             // newList에 생성한 유저 추가
             newList.add(mUser)

             // adapter의 differ.submitList()로 newList 제출
             // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
             adapter.differ.submitList(newList)

             // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
             // userList = adapter.differ.currentList 이렇게 사용하면 안됨
             userList.add(mUser)

             // 추가 메시지 출력
             Toast.makeText(this, &quot;${mUser.name}이 추가되었습니다.&quot;, Toast.LENGTH_SHORT).show()

             // 추가된 포지션으로 스크롤 이동
             binding.recyclerview.scrollToPosition(userList.indexOf(mUser))
         }
     }

 }
</code></pre>
</li>
</ol>
<br>




<p><br><br><br><br></p>
<h1 id="drag하여-아이템-순서-바꾸기">Drag하여 아이템 순서 바꾸기</h1>
<blockquote>
<p>itemTouchHelper를 사용하여 아이템 순서 바꾸기</p>
</blockquote>
<p>여기서부터는 헷갈릴 수 있으니 필요한 부분은 코드를 잘라서 먼저 보여주고 전체 코드를 보여주겠다. 우리는 itemTouchHelper를 이용해서 drag and drop 기능을 추가할것이다.</p>
<h2 id="결과-화면-2">결과 화면</h2>
<img src="https://velog.velcdn.com/images/chris_seed/post/7117ff54-4391-4cc2-97d6-3124dc7ab242/image.gif">

<br>

<h2 id="구현-방법-2">구현 방법</h2>
<ol>
<li><p>ItemTouchHelper.SimpleCallback을 상속받은 클래스를 만든다. (경로는 어디든 상관없음)</p>
<p> <strong>app/java/패키지/ItemTouchSimpleCallback</strong></p>
<pre><code class="language-kotlin">  class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
         ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {

         interface OnItemMoveListener {
             fun onItemMove(from: Int, to: Int)
         }

         private var listener: OnItemMoveListener? = null

         fun setOnItemMoveListener(listener: OnItemMoveListener) {
             this.listener = listener
         }

         override fun onMove(
             recyclerView: RecyclerView,
             viewHolder: RecyclerView.ViewHolder,
             target: RecyclerView.ViewHolder
         ): Boolean {

             // 어댑터 획득
             val adapter = recyclerView.adapter as UserAdapter

             // 현재 포지션 획득
             val fromPosition = viewHolder.absoluteAdapterPosition

             // 옮길 포지션 획득
             val toPosition = target.absoluteAdapterPosition

             // adapter 리스트를 담기위한 변수 생성
             val list = arrayListOf&lt;User&gt;()

             // adapter가 가지고 있는 현재 리스트 획득
             list.addAll(adapter.differ.currentList)

             // 리스트 순서 바꿈
             Collections.swap(list, fromPosition, toPosition)

             // adapter.notifyItemMoved(fromPosition, toPosition)와 같은 역할
             // list를 adapter.differ.submitList()로 데이터 변경 사항 알림
             adapter.differ.submitList(list)

             // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
             listener?.onItemMove(fromPosition, toPosition)

             return true
         }

         override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
         }

         // 드래그 완료 후 UI 
         override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
             super.clearView(recyclerView, viewHolder)

             // 순서 조정 완료 후 투명도 다시 1f로 변경
             viewHolder.itemView.alpha = 1.0f
         }

         // 드래그 중 UI 변화
         override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {

             if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
                 // 순서 변경 시 alpha를 0.5f
                 viewHolder?.itemView?.alpha = 0.5f
             }
             super.onSelectedChanged(viewHolder, actionState)
         }
     }</code></pre>
</li>
</ol>
<br>

<ol start="2">
<li><p>ItemTouchHelper콜백을 작성한 후 Activity에서 RecycleirView와 연결시켜준다. </p>
<p> <strong>app/java/MainActivity</strong></p>
<ul>
<li><p><span style="color:green">ItemTouchHelper Callback과 ItemTouchHelper 변수 선언</span></p>
<pre><code class="language-kotlin">  private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
  private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)</code></pre>
</li>
<li><p><span style="color:green">ItemTouchHelper와 RecyclerView 연결</span></p>
<pre><code class="language-kotlin">          // itemTouchSimpleCallback 인터페이스로 추가 작업
      itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
          override fun onItemMove(from: Int, to: Int) {
              Log.d(&quot;MainActivity&quot;, &quot;from Position : $from, to Position : $to&quot;)                

              // userList에도 값이 변하는 걸 원한다면 Collections.swap으로 변경
              //Collections.swap(userList, from, to)

              // userList != adapter.differ.currentList
              // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.)
              Log.d(&quot;MainActivity&quot;, &quot;userList: $userList&quot;)
              Log.d(&quot;MainActivity&quot;, &quot;differ currentList: ${adapter.differ.currentList}&quot;)
          }
      })

      // itemTouchHelper와 recyclerview 연결
      itemTouchHelper.attachToRecyclerView(binding.recyclerview)</code></pre>
</li>
<li><p><span style="color:green">MainActivity 전체 코드</span></p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

  private lateinit var binding: ActivityMainBinding
  private lateinit var adapter: UserAdapter
  private var userList = mutableListOf&lt;User&gt;()
  private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
  private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)

  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

      // RecyclerView에 사용할 리스트 제공
      for (i in 1 until 11) {
          val mUser = User(i, &quot;user$i&quot;)
          userList.add(mUser)
      }

  }

  override fun onResume() {
      super.onResume()

      // RecyclerView에 리스트 추가 및 어댑터 연결
      adapter = UserAdapter(this)
      binding.recyclerview.layoutManager = LinearLayoutManager(this)
      binding.recyclerview.adapter = adapter

      // DiffUtil 적용 후 데이터 추가
      adapter.differ.submitList(userList)

      // itemTouchSimpleCallback 인터페이스로 추가 작업
      itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
          override fun onItemMove(from: Int, to: Int) {
              Log.d(&quot;MainActivity&quot;, &quot;from Position : $from, to Position : $to&quot;)
              //Collections.swap(userList, from, to)

              // userList != adapter.differ.currentList
              // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.)
              Log.d(&quot;MainActivity&quot;, &quot;userList: $userList&quot;)
              Log.d(&quot;MainActivity&quot;, &quot;differ currentList: ${adapter.differ.currentList}&quot;)
          }
      })

      // itemTouchHelper와 recyclerview 연결
      itemTouchHelper.attachToRecyclerView(binding.recyclerview)

      // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
      binding.addButton.setOnClickListener {

          // 추가할 데이터 생성
          val mUser = User(userList.size+1, &quot;added user ${userList.size+1}&quot;)

          // differ의 현재 리스트를 받아와서 newList에 넣기
          val newList = adapter.differ.currentList.toMutableList()

          // newList에 생성한 유저 추가
          newList.add(mUser)

          // adapter의 differ.submitList()로 newList 제출
          // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
          adapter.differ.submitList(newList)

          // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
          // userList = adapter.differ.currentList 이렇게 사용하면 안됨
          userList.add(mUser)

          // 추가 메시지 출력
          Toast.makeText(this, &quot;${mUser.name}이 추가되었습니다.&quot;, Toast.LENGTH_SHORT).show()

          // 추가된 포지션으로 스크롤 이동
          binding.recyclerview.scrollToPosition(userList.indexOf(mUser))
      }
  }
}</code></pre>
</li>
</ul>
</li>
</ol>
<br>





<p><br><br><br><br></p>
<h1 id="swipe-후-view-고정-시-나타나는-button-클릭으로-아이템-제거">Swipe 후 View 고정 시 나타나는 Button 클릭으로 아이템 제거</h1>
<blockquote>
<p>Swipe 후 View를 고정하게 되면 뒤에 숨겨져있던 삭제 버튼을 눌러 아이템 제거</p>
</blockquote>
<p>작업 순서는 간단하다.</p>
<ol>
<li>swipe 되고 난 후의 삭제 버튼을 누르기 위해 필요한 view 추가 (user_list_item.xml)</li>
<li>삭제 버튼 클릭 시 일어날 이벤트 추가 (UserAdapter.kt)</li>
<li>swipe 동작 추가 (ItemTouchSimpleCallback.kt)</li>
<li>ItemTouchSimpleCallback의 추가 이벤트리스너를 RecyclerView와 연결 (MainActivity.kt)</li>
</ol>
<p>쓰고보니 별로 안간단하다. 설명은 주석을 참고하면 된다!. 수정된 부분을 표시해주고 싶은데 이미 한번 작성해 놓은 글을 날려버려서 의욕이 안난다!</p>
<br>

<h2 id="결과-화면-3">결과 화면</h2>
<img src="https://velog.velcdn.com/images/chris_seed/post/794cd173-b98f-478c-8dd0-c90bf4b1c17e/image.gif">

<br>

<h2 id="구현-방법-3">구현 방법</h2>
<ol>
<li><p>삭제 버튼(나는 TextView로 했음) 추가 및 전체적인 속성 변경 (app/res/layout/user_list_item.xml)</p>
<p> <strong>app/res/layout/user_list_item.xml</strong></p>
<pre><code class="language-kotlin"> &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
 &lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
     android:layout_width=&quot;match_parent&quot;
     android:layout_height=&quot;70dp&quot;
     xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;&gt;

     &lt;TextView
         android:id=&quot;@+id/remove_text_view&quot;
         android:layout_width=&quot;100dp&quot;
         android:layout_height=&quot;0dp&quot;
         android:background=&quot;@android:color/holo_red_light&quot;
         app:layout_constraintEnd_toEndOf=&quot;parent&quot;
         app:layout_constraintTop_toTopOf=&quot;parent&quot;
         app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
         android:text=&quot;삭제&quot;
         android:textSize=&quot;22sp&quot;
         android:gravity=&quot;center&quot; /&gt;

     &lt;LinearLayout
         android:id=&quot;@+id/swipe_view&quot;
         android:layout_width=&quot;match_parent&quot;
         app:layout_constraintStart_toStartOf=&quot;parent&quot;
         app:layout_constraintEnd_toEndOf=&quot;parent&quot;
         android:background=&quot;@color/white&quot;
         app:layout_constraintTop_toTopOf=&quot;parent&quot;
         app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
         android:padding=&quot;10dp&quot;
         android:layout_height=&quot;0dp&quot;&gt;

         &lt;ImageView
             android:id=&quot;@+id/user_image&quot;
             android:layout_width=&quot;50dp&quot;
             android:src=&quot;@mipmap/ic_launcher&quot;
             android:layout_height=&quot;50dp&quot; /&gt;

         &lt;TextView
             android:id=&quot;@+id/user_name_text&quot;
             android:layout_width=&quot;wrap_content&quot;
             android:textSize=&quot;20sp&quot;
             android:gravity=&quot;center&quot;
             android:layout_marginStart=&quot;10dp&quot;
             android:layout_height=&quot;match_parent&quot; /&gt;

     &lt;/LinearLayout&gt;
 &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre>
</li>
</ol>
<br>

<ol start="2">
<li><p>이제 우리는 삭제버튼을 눌렀을 때 일어날 이벤트를 RecyclerView의 Adapter에 써주자(app/java/패키지/adapters/UserAdapter.kt)</p>
<p> <strong>app/java/패키지/adapters/UserAdapter.kt</strong></p>
<pre><code class="language-kotlin"> class UserAdapter(
     private val mContext: Context)
     : RecyclerView.Adapter&lt;UserAdapter.ViewHolder&gt;() {

     inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {

         private val swipeView: LinearLayout = itemView.findViewById(R.id.swipe_view)
         private val userImage: ImageView = itemView.findViewById(R.id.user_image)
         private val userNameText: TextView = itemView.findViewById(R.id.user_name_text)
         private val removeTextView: TextView = itemView.findViewById(R.id.remove_text_view)

         fun bind(user: User) {

             // 재사용 시 Swipe가 되어있다면 Swipe 원상복구
             swipeView.translationX = 0f

             userNameText.text = user.name

             removeTextView.setOnClickListener {
                 val list = arrayListOf&lt;User&gt;()
                 list.addAll(differ.currentList)
                 list.remove(user)

                 // 해당 아이템 삭제 adapter에 알리기기
             differ.submitList(list)
             }

         }
     }

     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false)
         return ViewHolder(view)
     }

     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         // 수정 전
         //val user = mList[position]

         // 수정 후
         val user = differ.currentList[position]
         holder.bind(user)
     }

     override fun getItemCount() =  differ.currentList.size

     // 추가
     private val differCallback = object : DiffUtil.ItemCallback&lt;User&gt;() {
         override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
             return oldItem.id == newItem.id
         }

         override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
             return oldItem == newItem
         }
     }

     val differ = AsyncListDiffer(this, differCallback)

 }</code></pre>
</li>
</ol>
<br>

<ol start="3">
<li><p>천천히 ItemTouchSimpleCallback.kt 전반적으로 수정한다. (app/java/패키지/ItemTouchSimpleCallback.kt)</p>
<p> <strong>app/java/패키지/ItemTouchSimpleCallback.kt</strong></p>
<pre><code class="language-kotlin"> class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback(
     ItemTouchHelper.UP or ItemTouchHelper.DOWN,
     ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
 ) {

     private var currentPosition: Int? = null
     private var previousPosition: Int? = null
     private var currentDx = 0f

     // 삭제 버튼 width를 넣을 값
     private var clamp = 0f

     interface OnItemMoveListener {
         fun onItemMove(from: Int, to: Int)
     }

     private var listener: OnItemMoveListener? = null

     fun setOnItemMoveListener(listener: OnItemMoveListener) {
         this.listener = listener
     }

     override fun onMove(
         recyclerView: RecyclerView,
         viewHolder: RecyclerView.ViewHolder,
         target: RecyclerView.ViewHolder
     ): Boolean {

         // 어댑터 획득
         val adapter = recyclerView.adapter as UserAdapter

         // 현재 포지션 획득
         val fromPosition = viewHolder.absoluteAdapterPosition

         // 옮길 포지션 획득
         val toPosition = target.absoluteAdapterPosition

         // adapter가 가지고 있는 현재 리스트 획득
         val list = arrayListOf&lt;User&gt;()
         list.addAll(adapter.differ.currentList)

         // 리스트 순서 바꿈
         Collections.swap(list, fromPosition, toPosition)

         // adapter.notifyItemMoved(fromPosition, toPosition)
         adapter.differ.submitList(list)

         // 추가적인 조치가 필요할 경우 인터페이스를 통해 해결
         listener?.onItemMove(fromPosition, toPosition)

         return true
     }

     override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
     }

     override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
         super.clearView(recyclerView, viewHolder)

         // 순서 조정 완료 후 투명도 다시 1f로 변경
         viewHolder.itemView.alpha = 1.0f
         getDefaultUIUtil().clearView(getView(viewHolder))
         previousPosition = viewHolder.absoluteAdapterPosition
     }

     override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {

         if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
             // 순서 변경 시 alpha를 0.5f
             viewHolder?.itemView?.alpha = 0.5f
         }

         if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
             viewHolder?.let {
                 // 삭제 버튼 width 획득
                 clamp = getViewWidth(viewHolder)
                 // 현재 뷰홀더
                 currentPosition = viewHolder.bindingAdapterPosition
                 getDefaultUIUtil().onSelected(getView(it))
             }
         }

         super.onSelectedChanged(viewHolder, actionState)
     }

     override fun onChildDraw(
         c: Canvas,
         recyclerView: RecyclerView,
         viewHolder: RecyclerView.ViewHolder,
         dX: Float,
         dY: Float,
         actionState: Int,
         isCurrentlyActive: Boolean
     ) {
         if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
             val view = getView(viewHolder)
             val isClamped = getTag(viewHolder)

             val x = clampViewPositionHorizontal(view, dX, isClamped, isCurrentlyActive)

             currentDx = x

             getDefaultUIUtil().onDraw(
                 c, recyclerView, view, x, dY, actionState, isCurrentlyActive
             )
         }

         if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
             super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
         }
     }

     // 삭제버튼 width 구하는 함수
     private fun getViewWidth(viewHolder: RecyclerView.ViewHolder): Float{
         val viewWidth = (viewHolder as UserAdapter.ViewHolder).itemView.findViewById&lt;TextView&gt;(R.id.remove_text_view).width
         return viewWidth.toFloat()
     }

     // swipe될 뷰 (우리가 스와이프할 시 움직일 화면)
     private fun getView(viewHolder: RecyclerView.ViewHolder): View {
         return (viewHolder as UserAdapter.ViewHolder).itemView.findViewById(R.id.swipe_view)
     }

     // view의 tag로 스와이프 고정됐는지 안됐는지 확인 (고정 == true)
     private fun getTag(viewHolder: RecyclerView.ViewHolder): Boolean {
         return viewHolder.itemView.tag as? Boolean ?: false
     }

     // view의 tag에 스와이프 고정됐으면 true, 안됐으면 false 값 넣기
     private fun setTag(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) {
         viewHolder.itemView.tag = isClamped
     }

     // 스와이프 될 가로(수평평) 길이
 private fun clampViewPositionHorizontal(
         view: View,
         dX: Float,  //
         isClamped: Boolean,
         isCurrentlyActive: Boolean
     ): Float {
         val maxSwipe: Float = -clamp * 1.5f

         val right = 0f

         val x = if (isClamped) {
             if (isCurrentlyActive) dX - clamp else -clamp
         } else dX

         return min(
             max(maxSwipe, x),
             right
         )
     }

     // 사용자가 Swipe 동작으로 간주할 최소 속도
     override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
         return defaultValue * 10
     }

     // 사용자가 스와이프한 것으로 간주할 view 이동 비율
     override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
         setTag(viewHolder, currentDx &lt;= -clamp)
         return 2f
     }

     // 다른 아이템 클릭 시 기존 swipe되어있던 아이템 원상복구
     fun removePreviousClamp(recyclerView: RecyclerView) {
         if (currentPosition == previousPosition)
             return
         previousPosition?.let {
             val viewHolder = recyclerView.findViewHolderForAdapterPosition(it) ?: return
             getView(viewHolder).translationX = 0f
             setTag(viewHolder, false)
             previousPosition = null
         }
     }

 }</code></pre>
</li>
</ol>
<br>

<ol start="4">
<li><p>removePreviousClamp를 적용하기 위해 MainActivity.kt를 수정한다. (app/java/패키지/MainActivity.kt)</p>
<p> <strong>app/java/패키지/MainActivity.kt)</strong></p>
<pre><code class="language-kotlin"> class MainActivity : AppCompatActivity() {

     private lateinit var binding: ActivityMainBinding
     private lateinit var adapter: UserAdapter
     private var userList = mutableListOf&lt;User&gt;()
     private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
     private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)

     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

         // RecyclerView에 사용할 리스트 제공
         for (i in 1 until 11) {
             val mUser = User(i, &quot;user$i&quot;)
             userList.add(mUser)
         }

     }

     override fun onResume() {
         super.onResume()

         // RecyclerView에 리스트 추가 및 어댑터 연결
         adapter = UserAdapter(this)
         binding.recyclerview.layoutManager = LinearLayoutManager(this)
         binding.recyclerview.adapter = adapter

         // DiffUtil 적용 후 데이터 추가
         adapter.differ.submitList(userList)

         // itemTouchSimpleCallback 인터페이스로 추가 작업
         itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
             override fun onItemMove(from: Int, to: Int) {
                 Log.d(&quot;MainActivity&quot;, &quot;from Position : $from, to Position : $to&quot;)
                 //Collections.swap(userList, from, to)

                 // userList != adapter.differ.currentList
                 // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.)
                 Log.d(&quot;MainActivity&quot;, &quot;userList: $userList&quot;)
                 Log.d(&quot;MainActivity&quot;, &quot;differ currentList: ${adapter.differ.currentList}&quot;)
             }
         })

         // itemTouchHelper와 recyclerview 연결
         itemTouchHelper.attachToRecyclerView(binding.recyclerview)

         // RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경
         // 아래 코드가 경고 표시를 주는데 이것은 Annotation @SuppressLint(&quot;ClickableViewAccessibility&quot;)을 함수에 추가하면 됨
         // 또는, performClick 사용
         binding.recyclerview.setOnTouchListener { _, _ -&gt;
             itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview)
             false
         }

         // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
         binding.addButton.setOnClickListener {

             // 추가할 데이터 생성
             val mUser = User(userList.size+1, &quot;added user ${userList.size+1}&quot;)

             // differ의 현재 리스트를 받아와서 newList에 넣기
             val newList = adapter.differ.currentList.toMutableList()

             // newList에 생성한 유저 추가
             newList.add(mUser)

             // adapter의 differ.submitList()로 newList 제출
             // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
             adapter.differ.submitList(newList)

             // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
             // userList = adapter.differ.currentList 이렇게 사용하면 안됨
             userList.add(mUser)

             // 추가 메시지 출력
             Toast.makeText(this, &quot;${mUser.name}이 추가되었습니다.&quot;, Toast.LENGTH_SHORT).show()

             // 추가된 포지션으로 스크롤 이동
             binding.recyclerview.scrollToPosition(userList.indexOf(mUser))
         }
     }

 }</code></pre>
</li>
</ol>
<p><br><br><br><br></p>
<h1 id="mainactivity-코드-정리">MainActivity 코드 정리</h1>
<blockquote>
<p>지저분한 MainActivity의 소스코드를 좀 정리해보자</p>
</blockquote>
<p>onResume()에 있던걸 용도별 함수에 나누어서 onResume()에서는 함수호출로 정리했다.</p>
<pre><code class="language-kotlin">class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: UserAdapter
    private var userList = mutableListOf&lt;User&gt;()
    private val itemTouchSimpleCallback = ItemTouchSimpleCallback()
    private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // RecyclerView에 사용할 리스트 제공
        for (i in 1 until 11) {
            val mUser = User(i, &quot;user$i&quot;)
            userList.add(mUser)
        }

    }

    override fun onResume() {
        super.onResume()

        initRecyclerView()
        setupEvents()

    }

    private fun initRecyclerView() {
        // RecyclerView에 리스트 추가 및 어댑터 연결
        adapter = UserAdapter(this)
        binding.recyclerview.layoutManager = LinearLayoutManager(this)
        binding.recyclerview.adapter = adapter

        // DiffUtil 적용 후 데이터 추가
        adapter.differ.submitList(userList)

        // itemTouchSimpleCallback 인터페이스로 추가 작업
        itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener {
            override fun onItemMove(from: Int, to: Int) {
                // Collections.swap(userList, from, to) 처럼 from, to가 필요하다면 사용
                Log.d(&quot;MainActivity&quot;, &quot;from Position : $from, to Position : $to&quot;)
            }
        })

        // itemTouchHelper와 recyclerview 연결
        itemTouchHelper.attachToRecyclerView(binding.recyclerview)

        // RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경
        binding.recyclerview.setOnTouchListener { _, _ -&gt;
            itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview)
            false
        }
    }

    private fun setupEvents() {
        // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가)
        binding.addButton.setOnClickListener {

            // 추가할 데이터 생성
            val mUser = User(userList.size+1, &quot;added user ${userList.size+1}&quot;)

            // differ의 현재 리스트를 받아와서 newList에 넣기
            val newList = adapter.differ.currentList.toMutableList()

            // newList에 생성한 유저 추가
            newList.add(mUser)

            // adapter의 differ.submitList()로 newList 제출
            // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영
            adapter.differ.submitList(newList)

            // userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.)
            // userList = adapter.differ.currentList 이렇게 사용하면 안됨
            userList.add(mUser)

            // 추가 메시지 출력
            Toast.makeText(this, &quot;${mUser.name}이 추가되었습니다.&quot;, Toast.LENGTH_SHORT).show()

            // 추가된 포지션으로 스크롤 이동
            binding.recyclerview.scrollToPosition(newList.indexOf(mUser))
        }
    }


}</code></pre>
<p><br><br><br><br></p>
<h1 id="diffutil-사용-시-ui-반영이-안되는-경우">DiffUtil 사용 시 UI 반영이 안되는 경우</h1>
<p>혹시 DiffUtil.submitList()를 사용했는 데 UI에 업데이트가 안된다면 이 글이 유용할 것이다.</p>
<p>DiffUtil.submitList()는 입력받은 리스트를 참조하는 것이지 그 리스트를 따로 가지고 있는 것이 아니다. 이게 무슨 말이냐면 처음에 DiffUtil.submitList()에 listA를 넣는다. 그렇다면 현재 Diffutil은 listA를 바라보고 있다. </p>
<p>우리는 이제 Item을 추가하기 위해 listA에 add.(아이템)을 하고 그것을 DiffUtil.submitList()에 넣을 가능성이 크다(내가 그랬다!나는 멍청했다). 그렇다면 DiffUtil의 areItemsTheSame()과 areContentsTheSame()은 반환값을 true로 준다. 반환값이 true면 이전 리스트와 새로운 리스트의 동일한 포지션에 있는 아이템이 같다는 의미로 adapter에 UI 반영을 해주지않는다. </p>
<p>왜 그럴까?</p>
<p>바로 DifferUtil이 참조하고 있는 listA와 새로 들어온 listA는 같은 것이기 때문이다!</p>
<p>DifferUtil은 submitList()로 데이터가 들어오면 참조하고 있던 oldList와 비교하는데 이 oldList는 listA다. 하지만 listA는 이미 우리가 listA.add(아이템)으로 변경해주었기 때문에 oldList와 newList가 같은 상황인것이다. </p>
<p>해결방법은 새로운 변수에 differ.currentList()를 넣어서 데이터를 추가하고 submitList()에 넣는것이다. </p>
<p>나중에 이것과 관련된 내용을 자세히 공부하는 포스팅을 하게되면 링크를 추가하겠다! </p>
<p><br><br><br><br></p>
<h1 id="참고">참고</h1>
<ul>
<li><p><a href="https://goodgoodminki.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%8A%A4%EC%99%80%EC%9D%B4%ED%94%84-%EB%A9%94%EB%89%B4-%EA%B5%AC%EC%84%B1-Android-Kotlin-Swipe-Menu">https://goodgoodminki.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%8A%A4%EC%99%80%EC%9D%B4%ED%94%84-%EB%A9%94%EB%89%B4-%EA%B5%AC%EC%84%B1-Android-Kotlin-Swipe-Menu</a></p>
</li>
<li><p><a href="https://velog.io/@jeongminji4490/Android-RecyclerView-Item-Swipe-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Kotlin">https://velog.io/@jeongminji4490/Android-RecyclerView-Item-Swipe-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Kotlin</a></p>
</li>
</ul>
<p><br><br><br><br></p>
<h1 id="마치며">마치며</h1>
<p>위의 단계별 내용을 Github에 commit했다. 혹시 필요하다면 아래 주소로 가면 된다.</p>
<p><a href="https://github.com/park-chris/RecyclerView">https://github.com/park-chris/RecyclerView</a></p>
<p><br><br><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 부모 View와 자식 View의 TouchEvent 중첩]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-%EB%B6%80%EB%AA%A8-View%EC%99%80-%EC%9E%90%EC%8B%9D-View%EC%9D%98-TouchEvent-%EC%A4%91%EC%B2%A9</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-%EB%B6%80%EB%AA%A8-View%EC%99%80-%EC%9E%90%EC%8B%9D-View%EC%9D%98-TouchEvent-%EC%A4%91%EC%B2%A9</guid>
            <pubDate>Thu, 01 Sep 2022 12:45:23 GMT</pubDate>
            <description><![CDATA[<p>부모 View와 자식 View의 eventListener 중첩은 앱을 개발하는 사람들은 한번쯤은 겪어볼만한 일이다. 한번은 무슨... 꽤 자주 겪는다. 그때마다 구글링하기가 귀찮아서 기록한다.</p>
<p><br><br><br></p>
<h1 id="touchevent-동작-원리">TouchEvent 동작 원리</h1>
<p>아래 해결방법을 적용하기 위해서 이 동작 원리를 알아야한다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/6bf2dfb2-97ee-4a4f-81bf-a74c4176ff49/image.png" width="50%" align="left" />

<img src="https://velog.velcdn.com/images/chris_seed/post/fe72fd05-5b21-4b89-95e0-4aa8a48f986b/image.png" width="50%" />
<span style="font-size:10pt">사진 출처 - https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038</span>


<p><br><br></p>
<p>왼쪽 사진을 먼저 보면 Activity안에 ViewGroup A, ViewGroup A 안에 ViewGroup B, ViewGroup B안에 View가 있다. 이럴 경우 TouchEvent는 Activity -&gt; ViewGroup A -&gt; ViewGroup B -&gt; View 순으로 event가 발생함을 알리고, 다시 거꾸로 View -&gt; ViewGroup B -&gt; ViewGroup A -&gt; Activity 순으로 event가 동작하게 된다. </p>
<p>위의 사진으로 알 수 있듯이, TouchEvent가 발생하면 바로 동작이 되는게 아니고 TouchEvent가 발생하였다고 알리는 dispatchTouchEvent() 메소드가 먼저 작동한다. event가 발생했다는 것을 ViewGroup은 onInterceptTouchEvent()로 이 event를 하위 View(자식 뷰)에게 전달할지, 아닐지를 결정할 수 있다. onInterceptTouchEvent()의 반환값이 true면 ViewGroup은 이 event를 intercept(채가다)해간다. 이 말은 하위 View(자식 View)에게 event를 전달하지 않고 바로 event를 동작한다는 의미다.</p>
<p>이 내용을 이해했다면 아래 해결방법에서의 예시가 이해간다.</p>
<p><br><br><br><br></p>
<h1 id="해결방법">해결방법</h1>
<p>부모 View(ViewGroup)에 <code>onInterceptTouchEvent()</code>를 오버라이딩하여, 어떤 특정 이벤트가 발생 시 자식 View에게 이 이벤트를 전달할 지 안할지 결정한다.
반환값이 false면 자식 View에게 이벤트를 전달하는 것을 의미한다.</p>
<br>
아래 코드는 Android Developer에서 onInterceptTouchEvent의 예시를 보여준다.

<p><strong>onInterceptTouchEvent()</strong></p>
<pre><code class="language-kotlin">    class MyViewGroup @JvmOverloads constructor(
            context: Context,
            private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
    ) : ViewGroup(context) {

        ...

        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            /*
             * This method JUST determines whether we want to intercept the motion.
             * If we return true, onTouchEvent will be called and we do the actual
             * scrolling there.
             */
            return when (ev.actionMasked) {
                // Always handle the case of the touch gesture being complete.
                MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -&gt; {
                    // Release the scroll.
                    mIsScrolling = false
                    false // Do not intercept touch event, let the child handle it
                }
                MotionEvent.ACTION_MOVE -&gt; {
                    if (mIsScrolling) {
                        // We&#39;re currently scrolling, so yes, intercept the
                        // touch event!
                        true
                    } else {

                        // If the user has dragged her finger horizontally more than
                        // the touch slop, start the scroll

                        // left as an exercise for the reader
                        val xDiff: Int = calculateDistanceX(ev)

                        // Touch slop should be calculated using ViewConfiguration
                        // constants.
                        if (xDiff &gt; mTouchSlop) {
                            // Start scrolling!
                            mIsScrolling = true
                            true
                        } else {
                            false
                        }
                    }
                }
                ...
                else -&gt; {
                    // In general, we don&#39;t want to intercept touch events. They should be
                    // handled by the child view.
                    false
                }
            }
        }

        override fun onTouchEvent(event: MotionEvent): Boolean {
            // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE,
            // scroll this container).
            // This method will only be called if the touch event was intercepted in
            // onInterceptTouchEvent
            ...
        }
    }
</code></pre>
<p>위의 예시를 난 아래처럼 적용했다. </p>
<p>모든 코드를 볼 필요는 없다. 수정 전,후로 onInterceptTouchEvent()에 추가된 when 구절이 해결방법이다. </p>
<br>

<p><strong>CustomMotionLayout.kt(수정 전)</strong></p>
<pre><code class="language-kotlin">package com.dn.digitalnutrition.customs

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dn.digitalnutrition.R

class CustomMotionLayout(context: Context, attributes: AttributeSet? = null):
MotionLayout(context, attributes){


    private var motionTouchStarted = false // 정확한 위치에서만 true
    private val itemContainerView by lazy {
        findViewById&lt;View&gt;(R.id.item_container)
    }

    private val hitRect = Rect()

    init {
        setTransitionListener(object : TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {}

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {}

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                motionTouchStarted = false
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {}
        })
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -&gt; {
                motionTouchStarted = false
                return super.onTouchEvent(event)
            }
        }

        if (!motionTouchStarted) {
            itemContainerView.getHitRect(hitRect)
            motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
        }

        return super.onTouchEvent(event) &amp;&amp; motionTouchStarted
    }

    private val gestureListener by lazy {
        object : GestureDetector.SimpleOnGestureListener() {
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                itemContainerView.getHitRect(hitRect)
                return hitRect.contains(e1.x.toInt(), e1.y.toInt())
            }
        }
    }

    private val gestureDetector by lazy {
        GestureDetector(context, gestureListener)
    }

    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

}</code></pre>
<br>

<p><strong>CustomMotionLayout.kt(수정 후)</strong></p>
<pre><code class="language-kotlin">package com.dn.digitalnutrition.customs

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dn.digitalnutrition.R

class CustomMotionLayout(context: Context, attributes: AttributeSet? = null):
MotionLayout(context, attributes){


    private var motionTouchStarted = false // 정확한 위치에서만 true
    private val itemContainerView by lazy {
        findViewById&lt;View&gt;(R.id.item_container)
    }

    private val hitRect = Rect()

    init {
        setTransitionListener(object : TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {}

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {}

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                motionTouchStarted = false
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {}
        })
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.actionMasked) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, -&gt; {
                motionTouchStarted = false
                return super.onTouchEvent(event)
            }
        }

        if (!motionTouchStarted) {
            itemContainerView.getHitRect(hitRect)
            motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
        }

        return super.onTouchEvent(event) &amp;&amp; motionTouchStarted
    }

    private val gestureListener by lazy {
        object : GestureDetector.SimpleOnGestureListener() {
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                itemContainerView.getHitRect(hitRect)
                return hitRect.contains(e1.x.toInt(), e1.y.toInt())
            }
        }
    }

    private val gestureDetector by lazy {
        GestureDetector(context, gestureListener)
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {
            MotionEvent.ACTION_MOVE, MotionEvent.ACTION_SCROLL-&gt;
            return false
        }
        return gestureDetector.onTouchEvent(event)
    }

}</code></pre>
<p><br><br><br><br></p>
<h1 id="참고">참고</h1>
<ul>
<li>Android Developers
<a href="https://developer.android.com/training/gestures/viewgroup?hl=ko#delegate">https://developer.android.com/training/gestures/viewgroup?hl=ko#delegate</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] MotionLayout.setTransitionListener 이벤트 후 UI 작동안하는 에러]]></title>
            <link>https://velog.io/@chris_seed/Android-MotionLayout.setTransitionListener-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%9B%84-UI-%EC%9E%91%EB%8F%99%EC%95%88%ED%95%98%EB%8A%94-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@chris_seed/Android-MotionLayout.setTransitionListener-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%9B%84-UI-%EC%9E%91%EB%8F%99%EC%95%88%ED%95%98%EB%8A%94-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Thu, 01 Sep 2022 00:40:05 GMT</pubDate>
            <description><![CDATA[<h1 id="에러">에러</h1>
<p>MotionLayout의 setTransitionListener를 사용한 후 UI가 안보이는 에러가 발생했다.  UI가 안보일뿐만 아니라 해당 UI의 기능들도 작동하지않았다.</p>
<p>구현하고자 했던 기능은 Fragment에서 MotionLayout의 이벤트가 발생할때 MainActivity의 MotionLayout의 이벤트도 일어나게 하는 것이었다. </p>
<p>아래 코드가 setTransitionListener를 사용했을 때 에러가 났던 코드다.</p>
<br>

<p><strong>Fragment.kt</strong></p>
<pre><code class="language-kotlin">
        binding.motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {
            }

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {
                // MainActivity 모션 레이아웃에 값을 전달
                binding.let {
                    (activity as MainActivity).also { mainActivity -&gt;
                        mainActivity.findViewById&lt;MotionLayout&gt;(mainBinding.mainMotionLayout.id).progress = abs(progress)
                    }
                }
            }

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {

            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {
            }

        })</code></pre>
<p><br><br><br><br></p>
<h1 id="원인">원인</h1>
<p>fragment에서 일어난 transition은 완료가 되었지만, fragment에서 호출한 MainActivity에서의 transition이 완료가 안되었기에 발생한 에러다. </p>
<p>확인하기 위해서 각 motionLayout에 setTransitionListener를 달아주고 로그로 transition이 제대로 종료되는지 확인해본다.</p>
<p>아래 코드는 데이터바인딩된 MainActivity에 쓴 코드다.</p>
<br>

<p><strong>MainActivity.kt</strong></p>
<pre><code class="language-kotlin">      binding.mainMotionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {
                Log.d(&quot;MainActivity&quot;, &quot;onTransitionStarted&quot;)
            }

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {
                Log.d(&quot;MainActivity&quot;, &quot;onTransitionChange&quot;)

            }

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                Log.d(&quot;MainActivity&quot;, &quot;onTransitionCompleted&quot;)

            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {
                Log.d(&quot;MainActivity&quot;, &quot;onTransitionTrigger&quot;)

            }

        })</code></pre>
<p><br><br><br><br></p>
<h1 id="해결">해결</h1>
<p>해결하는 방법은 MainActivity의 MotionLayout transition을 호출하는 Fragment의 MotionLayout setTransitionListener에서 호출한 MAinActivity의 transition을 임의로 종료해주는 것이다.</p>
<p>다양한 방법이 있겠지만, 나는 Fragment의 setTransitionListener에서 상속받은 onTransitionCompleted()에서 transition의 process를 동일하게 해주었다. </p>
<p>아래 코드가 위 내용으로 해결한 부분이다.</p>
<p>이렇게 한 이유는 MainActivity와 Framgnet의 transition의 start와 end의 같아야했기 때문이다. 이런게 필요없다면  <code>transitionToStart()</code>나 <code>transitionToEnd()</code>를 onTransitionCompleted에 넣어줘도 된다.</p>
<br>

<p><strong>Fragment.kt</strong></p>
<pre><code class="language-kotlin">
        binding.motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {
                Log.d(TAG, &quot;onTransitionStarted&quot;)
            }

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {
                // MainActivity 모션 레이아웃에 값을 전달
                binding.let {
                    // Fragment 는 자기 단독으로 존재할 수 없기 때문에 activity가 존재할 수밖에 없고
                    // Activity를 가져오면 해당 Fragment가 attacj 되어있는 액티비티를 가져온다.
                    (activity as MainActivity).also { mainActivity -&gt;
                        mainActivity.findViewById&lt;MotionLayout&gt;(R.id.main_motion_layout).progress = abs(progress)
                    }
                }
                Log.d(TAG, &quot;onTransitionChange&quot;)

            }

            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                (activity as MainActivity).also { mainActivity -&gt;
                    mainActivity.findViewById&lt;MotionLayout&gt;(mainBinding.mainMotionLayout.id).progress = motionLayout.progress
                }
                Log.d(TAG, &quot;SoundPlayerFragment : ${binding.motionLayout.currentState},&quot; +
                        &quot;MainActivity : ${(activity as MainActivity).also { mainActivity -&gt; mainActivity.findViewById&lt;MotionLayout&gt;(mainBinding.mainMotionLayout.id).currentState }}&quot;)
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {
                Log.d(TAG, &quot;onTransitionTrigger&quot;)
            }

        })</code></pre>
<p><br><br><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 스크롤 시 배경 이미지 점점 사라지게 하기]]></title>
            <link>https://velog.io/@chris_seed/AndroidKotlin-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EC%8B%9C-%EB%B0%B0%EA%B2%BD-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%90%EC%A0%90-%EC%82%AC%EB%9D%BC%EC%A7%80%EA%B2%8C-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chris_seed/AndroidKotlin-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EC%8B%9C-%EB%B0%B0%EA%B2%BD-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%90%EC%A0%90-%EC%82%AC%EB%9D%BC%EC%A7%80%EA%B2%8C-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Aug 2022 08:01:19 GMT</pubDate>
            <description><![CDATA[<p>오늘은 화면 스크롤 시 배경이미지를 점점 투명하게 만드는 것을 해볼것이다. 이번에 앱을 만들때 필요했는데 Kotlin 자료가 많이 없어서 Java로 작성된 블로그를 참고했다. 우선 애니메이션으로 할 수 있겠지만 여기서는 스크롤이벤트를 이용할 것이다.</p>
<h1 id="결과물">결과물</h1>
<img src="https://velog.velcdn.com/images/chris_seed/post/990dcf76-a67f-4c27-9913-68e88ecf928e/image.gif" width="300px" />



<p><br><br><br><br></p>
<h1 id="구현-방법">구현 방법</h1>
<p>나는 ScrollView를 커스텀할것이다. 우선 아래 레이아웃을 보면 위에 사라지게 만들 top_image가 있고 그 아래에 &quot;안녕하세요.&quot;를 입력받은 TextView가 있다. TextView 아래에는 스크롤을 하기위해 높이가 있는 것으로 채운것이므로 신경쓰지않아도 된다.</p>
<img src="https://velog.velcdn.com/images/chris_seed/post/b3e635e3-3f39-489a-bd1d-cc6af2b698a2/image.png" width="300" />



<br>

<p>자, 그럼 이제 구현해보자!</p>
<h2 id="customscrollviewkt">CustomScrollView.kt</h2>
<p>우선, 제일 중요한 ScrollView를 커스텀해보자. 스크롤이 변경될때 이벤트 리스너를 추가하여 Activity나 Fragment단에서 처리할 수 있게 했다.</p>
<pre><code class="language-kotlin">package com.crystal.customs

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.widget.ScrollView

class CustomScrollView: ScrollView {

    var SCROLL_UP = 0
    var SCROLL_DOWN = 1

    private var onScrollListener: OnScrollListener? = null

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attr: AttributeSet?) : this(context, attr, 0)
    constructor(context: Context, attr: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attr,
        defStyleAttr
    )

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)

        val direction = if (oldt &gt; t) SCROLL_DOWN else SCROLL_UP

        onScrollListener?.onScroll(direction, t.toFloat())

    }

    fun setOnScrollListener(listener: OnScrollListener) {
        onScrollListener = listener
    }


    interface OnScrollListener {
        fun onScroll(direction: Int, scrollY: Float)
    }


}</code></pre>
<p><br><br></p>
<h2 id="fragment_homexml">fragment_home.xml</h2>
<p>fragment의 layout이다. 보면 layout 바로 아래의 ConstraintLayout의 ImageView가 우리의 배경화면이다.그리고 배경화면과 우리가 만든 스크롤뷰의 부분이 겹치게 둔다. 이건 취향껏 하자. </p>
<pre><code class="language-kotlin">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;layout &gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    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;
    tools:context=&quot;.fragments.HomeFragment&quot;&gt;

    &lt;ImageView
        android:id=&quot;@+id/top_image&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;300dp&quot;
        android:contentDescription=&quot;@string/description_home_image&quot;
        android:scaleType=&quot;fitXY&quot;
        android:src=&quot;@drawable/night_sky&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        tools:layout_editor_absoluteX=&quot;0dp&quot; /&gt;

    &lt;com.crystal.customs.CustomScrollView
        android:id=&quot;@+id/scroll_view&quot;
        android:layout_width=&quot;match_parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        android:layout_height=&quot;0dp&quot;&gt;

        &lt;androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width=&quot;match_parent&quot;
            android:padding=&quot;10dp&quot;
            android:layout_marginTop=&quot;300dp&quot;
            android:layout_height=&quot;wrap_content&quot;&gt;

            &lt;TextView
                android:id=&quot;@+id/greetings_text_view&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:textSize=&quot;22sp&quot;
                android:text=&quot;안녕하세요.&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot;
                app:layout_constraintStart_toStartOf=&quot;parent&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview1&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/greetings_text_view&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview2&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/imageview1&quot;
                tools:layout_editor_absoluteX=&quot;0dp&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview3&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/imageview2&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview4&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/imageview3&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview5&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/imageview4&quot; /&gt;

            &lt;ImageView
                android:id=&quot;@+id/imageview6&quot;
                android:layout_width=&quot;match_parent&quot;
                android:layout_height=&quot;300dp&quot;
                app:layout_constraintTop_toBottomOf=&quot;@+id/imageview5&quot; /&gt;


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


    &lt;/com.crystal.customs.CustomScrollView&gt;

&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;
&lt;/layout&gt;</code></pre>
<p><br><br><br><br></p>
<h2 id="homefragmentkt">HomeFragment.kt</h2>
<pre><code class="language-kotlin">package com.crystal.fragments

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.crystal.R
import com.crystal.customs.CustomScrollView
import com.crystal.databinding.FragmentHomeBinding

private const val TAG = &quot;HomeFragment&quot;
class HomeFragment : Fragment() {

    private lateinit var binding: FragmentHomeBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_home, container, false)

        return binding.root

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupEvents()
        scrollEvent()
    }

    private fun setupEvents() {

    }

    private fun scrollEvent() {

        binding.scrollView.overScrollMode = View.OVER_SCROLL_NEVER

        // ScrollView에서 받는 이벤트 처리
        // 1: 완전 불투명
        // 스크롤 위치에 따라 alpha 값이 변경되므로, 방향은 상관이 없다.
        binding.scrollView.setOnScrollListener(object : CustomScrollView.OnScrollListener {
            override fun onScroll(direction: Int, scrollY: Float) {

            // statusBar 높이 구하기
            var statusBarHeight = 0
            val resId = resources.getIdentifier(&quot;status_bar_height&quot;, &quot;dimen&quot;, &quot;android&quot;)
            if (resId &gt; 0) {
                statusBarHeight = resources.getDimensionPixelSize(resId)
            }

            // top_image 높이 구하기, 나는 끝까지 안올리고 100% 불투명도 만들기위해 statusbar 높이를 뺐다.
            val backgroundImgHeight = binding.topImage.height - statusBarHeight

            val alpha = ((backgroundImgHeight - scrollY) / backgroundImgHeight)

            binding.topImage.alpha = alpha


            }

        })
    }

}</code></pre>
<p><br><br><br><br></p>
<p>난 위에 일부분을 배경이미지로 뒀지만 화면 전체를 배경이미지로 두고 전체 스크롤 시 최상단에서 최하단까지 투명도를 원하는 사람도 있을거라 생각한다. 그럼 내가 참고한 아래 링크로 가면 된다. 나도 저기 링크를 커스텀했다!</p>
<p><a href="https://bbulog.tistory.com/29">https://bbulog.tistory.com/29</a></p>
<p><br><br><br><br></p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://bbulog.tistory.com/29">https://bbulog.tistory.com/29</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>