<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩 끝에 낙이 온다</title>
    <link>https://behappyaftercoding.tistory.com/</link>
    <description>코딩 안에 부처님 계신다</description>
    <language>ko</language>
    <pubDate>Tue, 30 Jun 2026 09:41:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>CoBool</managingEditor>
    <item>
      <title>[이펙티브 코틀린]가변성을 제한하라</title>
      <link>https://behappyaftercoding.tistory.com/64</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;요소가 시간의 변화에 따라 변화하는 경우의 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그램을 이해하고 디버깅하기 힘들어짐&lt;/li&gt;
&lt;li&gt;코드의 실행을 추론하기 어려워짐.&lt;/li&gt;
&lt;li&gt;멀티스레드 프로그래밍의 경우 동기화가 필요하다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경점이 많을 수록 충돌점이 많아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트하기 어렵다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경이 많을 수록 많은 조합을 테스트해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상태 변경을 다른 부분에 알려야 할 때도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가변성 제한 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 전용 프로퍼티(val)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완전히 변경 불가능하지 않다.&lt;/li&gt;
&lt;li&gt;다른 프로퍼티를 활용하는 사용자 정의 게터로도 정의 가능하다.&lt;/li&gt;
&lt;li&gt;정의 옆에 상태가 바로 적히므로 코드의 실행을 예측하는 것이 간단&lt;/li&gt;
&lt;li&gt;스마트 캐스트를 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가변 컬렉션과 읽기 전용 컬렉션 구분하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 전용 컬렉션을 가변 컬렉션으로 다운캐스팅하면 안 된다.&lt;/li&gt;
&lt;li&gt;읽기 전용 컬렉션 사용 시 장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번 정의된 상태가 유지되므로 코드를 이해하기 쉽다&lt;/li&gt;
&lt;li&gt;공유했을 때도 충돌이 이뤄지지 않으므로 병렬 처리가 안전하다.&lt;/li&gt;
&lt;li&gt;객체에 대한 참조가 변경되지 않으므로 쉽게 캐시할 수 있다.&lt;/li&gt;
&lt;li&gt;방어적 복사본을 만들 필요가 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 복사할 때 깊은 복사를 따로 하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다른 객체를 만들 때 활용하기 좋다.&lt;/li&gt;
&lt;li&gt;실행을 더 쉽게 예측 가능하다.&lt;/li&gt;
&lt;li&gt;set 또는 map의 키로 사용 가능.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키 값을 기반으로한 자료구조는 요소의 변경이 일어나면 찾을 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 클래스의 copy
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;copy를 사용하여 immutable 객체를 수정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 가능 지점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mutable 컬렉션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex)val list1 = MutableList&amp;lt;Int&amp;gt;&lt;/li&gt;
&lt;li&gt;리스트 구현 내부에 변경 가능 지점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티스레드 처리가 이뤄질 경우 동기화되어 있는지 확실히 알 수 없어 위험하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mutable 프로퍼티
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex)var list2 = List&amp;lt;Int&amp;gt;&lt;/li&gt;
&lt;li&gt;프로퍼티 자체가 변경 가능 지점.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티스레드 처리의 안정성이 더 좋음.&lt;/li&gt;
&lt;li&gt;사용자 정의 세터를 활용하여 변경을 추적 가능.&lt;/li&gt;
&lt;li&gt;Delegates.observable을 사용 시 변경 시에 로그를 출력할 수 있다.&lt;/li&gt;
&lt;li&gt;객체 변경을 제어하기가 더 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프로퍼티와 컬렉션 둘 다 mutable인 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 지점 모두에 대한 동기화를 구현해야 함.&lt;/li&gt;
&lt;li&gt;모호성이 발생하여 +=를 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 가능 지점 노출하지 말기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;돌발적인 수정이 일어날 때 위험할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 방지하기 위한 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방어적 복제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리턴되는 mutable 객체를 복제하는 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;읽기 전용 슈퍼클래스로 업캐스트하여 가변성을 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드에 적용해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ViewModel에서 사용되던 MutableStateFlow를 예로 들어보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class HomeViewModel:ViewModel(){
...
val nickname: MutableStateFlow&amp;lt;String&amp;gt; = MutableStateFlow(&quot;&quot;)
    val emoji: MutableStateFlow&amp;lt;String&amp;gt; = MutableStateFlow(&quot;&quot;)
    val weatherEmoji: MutableStateFlow&amp;lt;String&amp;gt; = MutableStateFlow(&quot;&quot;)
    val weatherDegree: MutableStateFlow&amp;lt;String&amp;gt; = MutableStateFlow(&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로퍼티들이 val로 정의되어 있으나, 사실상 안의 값은 수정이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가변성을 가진 프로퍼티가 외부에 노출되어 있으므로, 캡슐화가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;...
    private val _nickname = MutableStateFlow(&quot;&quot;)
    private val _emoji = MutableStateFlow(&quot;&quot;)
    private val _weatherEmoji = MutableStateFlow(&quot;&quot;)
    private val _weatherDegree = MutableStateFlow(&quot;&quot;)
    val nickname: StateFlow&amp;lt;String&amp;gt;
        get() = _nickname
    val emoji: StateFlow&amp;lt;String&amp;gt;
        get() = _emoji
    val weatherEmoji: StateFlow&amp;lt;String&amp;gt;
        get() = _weatherEmoji
    val weatherDegree: StateFlow&amp;lt;String&amp;gt;
        get() = _weatherDegree
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 MutableStateFlow 변수는 private 처리하여 외부에서는 접근을 차단하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel 내부에서는 자유롭게 수정가능하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 외부에 노출할 용도로 MutableStateFlow를 업캐스팅하여 수정 불가능하도록 StateFlow 변수를 getter로 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 값은 외부에 노출할 수 있으나, 내부의 변수를 수정하지 못하도록 캡슐화를 진행하였다.&lt;/p&gt;</description>
      <category>전공 도서 리뷰</category>
      <category>MVVM</category>
      <category>viewmodel</category>
      <category>가변성 제한</category>
      <category>이펙티브 코틀린</category>
      <category>캡슐화</category>
      <category>코틀린</category>
      <category>프로퍼티</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/64</guid>
      <comments>https://behappyaftercoding.tistory.com/64#entry64comment</comments>
      <pubDate>Sat, 4 Jun 2022 00:36:08 +0900</pubDate>
    </item>
    <item>
      <title>MVVM 적용기(2)</title>
      <link>https://behappyaftercoding.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 MVVM에 대한 대략적인 개념과 그것을 구현하기 위한 컴포넌트들에 대해 소개하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 실제 코드를 통해 내가 MVVM을 어떻게 구현했는 지에 대해 보여주려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사이드 프로젝트로 시작한 어플에 MVVM을 적용했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 Repository에 Room을 사용해서 내부데이터를 저장한다고 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용할 어플에서는 그 정도로 많은 양의 데이터를 저장할 필요가 없었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 MVVM을 구현하기 위해서 Room을 사용하는 것 자체가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소 잡는데 닭 잡는 칼을 쓰는 모양새가 되는 것 같아서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 사용하던 SharedPreperence를 그대로 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 데이터를 요청하는 로직도 Model의 역할로서 같은 Repository에 포함시키도록 했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;object MisoRepository {
        fun checkRegistered(
            socialId: String,
            socialType: String,
            onSuccessful: (
                Call&amp;lt;GeneralResponseDto&amp;gt;,
                Response&amp;lt;GeneralResponseDto&amp;gt;
            ) -&amp;gt; Unit,
            onFail: (
                Call&amp;lt;GeneralResponseDto&amp;gt;,
                Response&amp;lt;GeneralResponseDto&amp;gt;
            ) -&amp;gt; Unit,
            onError: (
                Call&amp;lt;GeneralResponseDto&amp;gt;,
                Throwable
            ) -&amp;gt; Unit?,
        ) {
            val callCheckRegistered = TransportManager.getRetrofitApiObject&amp;lt;GeneralResponseDto&amp;gt;()
                .checkRegistered(socialId, socialType)

            TransportManager.requestApi(callCheckRegistered,
                { call, response -&amp;gt;
                    if (response.isSuccessful)
                        onSuccessful(call, response)
                    else
                        onFail(call, response)
                },
                { call, throwable -&amp;gt;
                    onError(call, throwable)
                })
        }
....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만든 Repository의 일부 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 통신하기 위해 사용한 API 요청 코드를 대신 수행해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retrofit의 객체를 만드는 과정은 많은 보일러플레이트 코드를 만들어내므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통되는 코드는 다음과 같이&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class TransportManager {
    companion object {
        fun &amp;lt;T&amp;gt; getRetrofitApiObject(): MisoWeatherAPI {
            var gson = GsonBuilder().setLenient().create()
            val retrofit = Retrofit.Builder()
                .baseUrl(MisoActivity.MISOWEATHER_BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build()
            val api = retrofit.create(MisoWeatherAPI::class.java)
            return api
        }

        fun &amp;lt;T&amp;gt; requestApi(
            callApi: Call&amp;lt;T&amp;gt;,
            onResponse: (Call&amp;lt;T&amp;gt;, Response&amp;lt;T&amp;gt;) -&amp;gt; Unit,
            onFailure: (Call&amp;lt;T&amp;gt;, Throwable) -&amp;gt; Unit
        ) {
            callApi.enqueue(object : Callback&amp;lt;T&amp;gt; {
                override fun onResponse(
                    call: Call&amp;lt;T&amp;gt;,
                    response: Response&amp;lt;T&amp;gt;
                ) {
                    onResponse(call, response)
                }

                override fun onFailure(call: Call&amp;lt;T&amp;gt;, t: Throwable) {
                    onFailure(call, t)
                }
            })
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransportManager라는 클래스에 제네릭으로 받아 대신 만들어주도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 SharedPreperence를 사용하기 위해서&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class MisoRepository private constructor() {
    companion object {
        private var instance: MisoRepository? = null
        private lateinit var context: Context
        private lateinit var prefs: SharedPreferences
        private lateinit var pairList: ArrayList&amp;lt;Pair&amp;lt;String, String&amp;gt;&amp;gt;
        fun getInstance(_context: Context): MisoRepository {
            return instance ?: synchronized(this) {
                instance ?: MisoRepository().also {
                    context = _context
                    instance = it
                    prefs = context.getSharedPreferences(&quot;misoweather&quot;, Context.MODE_PRIVATE)
                    pairList = ArrayList()
                }
            }
        }
    }

    fun addPreferencePair(first: String, second: String) {
        val pair = Pair(first, second)
        pairList.add(pair)
    }

    fun removePreference(pref: String) {
        val pair = Pair(pref, &quot;&quot;)
        pairList.add(pair)
    }

    fun removePreference(vararg pref: String) {
        for (i in 0..pref.size - 1) {
            val pair = Pair(pref[i], &quot;&quot;)
            pairList.add(pair)
        }
    }

    fun getPreference(pref: String): String? {
        return prefs!!.getString(pref, &quot;&quot;)
    }

    fun savePreferences() {
        var edit = prefs!!.edit()
        for (i in 0..pairList.size - 1) {
            val pair = pairList.get(i)
            edit.putString(pair.first, pair.second)
        }
        edit.apply()
        pairList.clear()
    }
....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드로 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedPreference에 접근을 위해 Context를 매개변수로 받는 싱글톤 생성자를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리포지토리는 어떤 곳이든 동일한 객체가 반환되어야 하므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤으로 구현하는 것이 정석이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 항목을 받아 저장하는 메소드와,항목의 내용을 지우는 메소드들도 추가했다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;repository = MisoRepository.getInstance(applicationContext)
repository.getBriefForecast(
            regionId,
            { call, response -&amp;gt;
                forecastBriefResponse.value = response
            },
            { call, response -&amp;gt;
                forecastBriefResponse.value = response
            },
            { call, t -&amp;gt;
                forecastBriefResponse.value = null
            }
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 호출 부분은 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 입장에서는 불러올 데이터를 가져오는 함수를 호출하고,각각 성공했을 때,실패했을 때,예외가 발생했을 때에 대한 메소드를 넣어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Repository가 만들어졌으니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 뷰의 데이터를 저장할 ViewModel을 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 안드로이드에서 ViewModel하면 기본적으로 따라오는 것이 LiveData이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel을 구현하려면 LiveData도 함께 알아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LiveData는 관찰 가능한 데이터 홀더 클래스로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 데이터의 변경을 감지할 수 있는 observer를 붙일 수 있어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 데이터와 UI 상의 데이터를 일치시킬 수 있는 장점이 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class HomeViewModel(private val repository: MisoRepository) : ViewModel() {
    val memberInfoResponse: MutableLiveData&amp;lt;Response&amp;lt;MemberInfoResponseDto&amp;gt;?&amp;gt; = MutableLiveData()
    val forecastBriefResponse: MutableLiveData&amp;lt;Response&amp;lt;ForecastBriefResponseDto&amp;gt;?&amp;gt; =
        MutableLiveData()
    val commentListResponse: MutableLiveData&amp;lt;Response&amp;lt;CommentListResponseDto&amp;gt;?&amp;gt; = MutableLiveData()
    val surveyResultResponse: MutableLiveData&amp;lt;Response&amp;lt;SurveyResultResponseDto&amp;gt;?&amp;gt; =
        MutableLiveData()

    fun getUserInfo(serverToken: String) {
        repository.getUserInfo(
            serverToken,
            { call, response -&amp;gt;
                memberInfoResponse.value = response
            },
            { call, response -&amp;gt;
                memberInfoResponse.value = response
            },
            { call, throwable -&amp;gt;
                memberInfoResponse.value = null
            })
    }

    fun getBriefForecast(regionId: Int) {
        repository.getBriefForecast(
            regionId,
            { call, response -&amp;gt;
                forecastBriefResponse.value = response
            },
            { call, response -&amp;gt;
                forecastBriefResponse.value = response
            },
            { call, t -&amp;gt;
                forecastBriefResponse.value = null
            }
        )
    }

    fun getCommentList(commentId: Int?, size: Int) {
        repository.getCommentList(
            commentId,
            size,
            { call, response -&amp;gt;
                commentListResponse.value = response
            },
            { call, response -&amp;gt;
                commentListResponse.value = response
            },
            { call, throwable -&amp;gt; }
        )
    }

    fun getSurveyResult(shortBigScale: String) {
        repository.getSurveyResults(
            shortBigScale,
            { call, reponse -&amp;gt;
                surveyResultResponse.value = reponse!!
            },
            { call, reponse -&amp;gt;
                surveyResultResponse.value = reponse!!
            },
            { call, t -&amp;gt;

            },
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인화면의 날씨정보들을 서버로부터 불러와 데이터를 저장하는 역할을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 보는 것과 마찬가지로,UI에서 저장할 정보들을 LiveData 객체로 선언한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 메소드에서 응답을 받는 경우 선언된 LiveData의 객체들 또한 변경시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이정도만 해도,ViewModel로의 역할은 수행할 수 있다고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;repository = MisoRepository.getInstance(applicationContext)
        viewModel = HomeViewModel(repository)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Activity 호출 부에서는 이렇게 Repository를 싱글톤으로 넣어 구현하였다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;viewModel.getBriefForecast(getPreference(&quot;defaultRegionId&quot;)!!.toInt())
viewModel.forecastBriefResponse.observe(this, {
                if (it == null) {
                    Log.i(&quot;getBriefForecast&quot;, &quot;실패&quot;)
                    repeatRequest()
                } else {
                    if (it.isSuccessful) {
                        try {
                            Log.i(&quot;결과&quot;, &quot;성공&quot;)
                            val forecastBriefResponseDto = it.body()!!
                            var forecast = forecastBriefResponseDto.data.forecast
                            var region = forecastBriefResponseDto.data.region
                            addPreferencePair(&quot;bigScale&quot;, region.bigScale)
                            addPreferencePair(
                                &quot;midScale&quot;,
                                if (region.midScale.equals(&quot;선택 안 함&quot;)) &quot;전체&quot; else region.midScale
                            )
                            addPreferencePair(
                                &quot;smallScale&quot;,
                                if (region.smallScale.equals(&quot;선택 안 함&quot;)) &quot;전체&quot; else region.smallScale
                            )
                            savePreferences()
                            txtLocation.text =
                                region.bigScale + &quot; &quot; + getPreference(&quot;midScale&quot;) + &quot; &quot; +
                                        if (getPreference(&quot;midScale&quot;).equals(&quot;전체&quot;)) &quot;&quot; else getPreference(
                                            &quot;smallScale&quot;
                                        )
                            txtWeatherEmoji.setText(forecast.sky)
                            txtWeatherDegree.setText(forecast.temperature + &quot;˚&quot;)
                            setupSurveyResult()
                        } catch (e: Exception) {
                            repeatRequest()
                            e.printStackTrace()
                            Log.i(&quot;getBriefForecast&quot;, &quot;excepted&quot;)
                        }
                    } else {
                        repeatRequest()
                    }
                }
            })
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위와 같이 데이터를 뿌려야할 항목별로 초기화를 하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 데이터를 가져올 함수를 호출한뒤,가져올 LiveData 변수에 observe 함수를 이용하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 변경될 때에 실행할 코드를 진행하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드로 View와 ViewModel 간에 데이터를 요청하고 업데이트하는 과정은 어느정도 구현되었다고 볼 수 있지만, 아직은 SharedPreference를 뷰인 액티비티에서 접근하고 있기에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel이 SharedPreperence의 접근도 함께 담당하도록 ViewModel에 Repository를 생성시에 함께 넣는 것으로 변경하였다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class HomeViewModel(private val repository: MisoRepository) : ViewModel() {
    val memberInfoResponse: MutableLiveData&amp;lt;Response&amp;lt;MemberInfoResponseDto&amp;gt;?&amp;gt; = MutableLiveData()
    val forecastBriefResponse: MutableLiveData&amp;lt;Response&amp;lt;ForecastBriefResponseDto&amp;gt;?&amp;gt; =
        MutableLiveData()
    val commentListResponse: MutableLiveData&amp;lt;Response&amp;lt;CommentListResponseDto&amp;gt;?&amp;gt; = MutableLiveData()
    val surveyResultResponse: MutableLiveData&amp;lt;Response&amp;lt;SurveyResultResponseDto&amp;gt;?&amp;gt; =
        MutableLiveData()
    val isSurveyed: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val lastSurveyedDate: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val defaultRegionId: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val misoToken: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val bigScale: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val midScale: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val smallScale: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()
    val logoutResponseString: MutableLiveData&amp;lt;String?&amp;gt; = MutableLiveData()

    fun updateProperties() {
        setupBigScale()
        setupMidScale()
        setupSmallScale()
        setupDefaultRegionId()
        setupDefaultRegionId()
        setupSurveyed()
        setupMisoToken()
        setupLastSurveyedDate()
    }

    fun setupSurveyed() {
        isSurveyed.value = repository.getPreference(&quot;isSurveyed&quot;)
    }

    fun setupLastSurveyedDate() {
        lastSurveyedDate.value = repository.getPreference(&quot;LastSurveyedDate&quot;)
    }

    fun setupDefaultRegionId() {
        defaultRegionId.value = repository.getPreference(&quot;defaultRegionId&quot;)
    }

    fun setupMisoToken() {
        misoToken.value = repository.getPreference(&quot;misoToken&quot;)
    }

    fun setupBigScale() {
        bigScale.value = repository.getPreference(&quot;bigScaleRegion&quot;)
    }

    fun setupMidScale() {
        midScale.value = repository.getPreference(&quot;MidScaleRegion&quot;)
    }

    fun setupSmallScale() {
        smallScale.value = repository.getPreference(&quot;SmallScaleRegion&quot;)
    }
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 외부 API를 호출하여 데이터를 가져오는 로직이외에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedPreperence를 호출하여 내부 데이터를 저장하는 메소드도 추가하였다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun initializeProperties() {
        fun checkinitializedAll()
        {
            if (
                this::isSurveyed.isInitialized &amp;amp;&amp;amp;
                this::lastSurveyedDate.isInitialized &amp;amp;&amp;amp;
                this::defaultRegionId.isInitialized &amp;amp;&amp;amp;
                this::misoToken.isInitialized &amp;amp;&amp;amp;
                this::bigScale.isInitialized &amp;amp;&amp;amp;
                this::midScale.isInitialized &amp;amp;&amp;amp;
                this::smallScale.isInitialized
            )
                setupData()
        }
        viewModel.updateProperties()
        viewModel.isSurveyed.observe(this, {
            isSurveyed = it!!
            checkinitializedAll()
        })
        viewModel.lastSurveyedDate.observe(this, {
            lastSurveyedDate = it!!
            checkinitializedAll()
        })
        viewModel.defaultRegionId.observe(this, {
            defaultRegionId = it!!
            checkinitializedAll()
        })
        viewModel.misoToken.observe(this, {
            misoToken = it!!
            checkinitializedAll()
        })
        viewModel.bigScale.observe(this, {
            bigScale = it!!
            checkinitializedAll()
        })
        viewModel.midScale.observe(this, {
            midScale = it!!
            checkinitializedAll()
        })
        viewModel.smallScale.observe(this, {
            smallScale = it!!
            checkinitializedAll()
        })

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 액티비티에는 실행 초기에 먼저 내부 SharedPreperence 데이터부터 먼저 초기화를 시켜,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel의 데이터와 값이 일치하는 변수를 가지도록 해 액티비티에서 따로 Repository에 접근할 필요가 없도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각각 실행되는 데이터 초기화 요청은 비동기적으로 실행되므로 각 변수들이 전부 초기화된 이후에 API를 호출하는 로직을 실행하도록 했는데, 위 부분이 중복된 코드가 많이 발생하나 어떻게 해결해야 할 지를 우선 고민중에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현함으로서 대략적인 MVVM 구현은 끝났다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 불러오는 역할은 오로지 ViewModel에서 담당하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액티비티는 업데이트된 데이터를 UI에 뿌려주기만 하면 되므로 각 클래스가 담당해야 할 역할은 분명해졌고 유지보수는 편리해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM을 적용하는 것에 조금이나마 도움이 됐기 바란다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.yena.io/studynote/2019/03/27/Android-MVVM-AAC-2.html&quot;&gt;https://blog.yena.io/studynote/2019/03/27/Android-MVVM-AAC-2.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>AAC</category>
      <category>livedata</category>
      <category>MVVM</category>
      <category>Repository</category>
      <category>viewmodel</category>
      <category>디자인패턴</category>
      <category>아키텍처패턴</category>
      <category>안드로이드</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/63</guid>
      <comments>https://behappyaftercoding.tistory.com/63#entry63comment</comments>
      <pubDate>Sat, 2 Apr 2022 21:26:17 +0900</pubDate>
    </item>
    <item>
      <title>MVVM 적용기(1)</title>
      <link>https://behappyaftercoding.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 기존 회사에서 처음으로 시도해봤던 아키텍처 패턴은 MVP 패턴으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 소스에 대한 접근은 Model이 담당하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 데이터를 보여주는 부분(Activity,Fragment)는 View가 담당하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 사이를 Presenter가 매개하는 형식의 패턴이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 패턴을 직접 업무에서 적용하려고 했을 때 발생한 문제는 다음과 같았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 View와 Presenter를 정의한 인터페이스인 Contract 등을 따로 정의해야 했고, 매번 두 아키텍처 컴포넌트를 변경해야 할 일이 생길 때마다 해당 인터페이스를 무조건적으로 변경해야 하므로 일을 두 번하게 되었다.&lt;/li&gt;
&lt;li&gt;View에는 데이터를 표시하는 일만 담당시키고, 복잡한 로직은 Presenter에게 위임시켜야 이상적인 MVP 패턴이라고 할 수 있으나, 로직을 분리시키는 과정에서 안드로이드 프레임워크에 의존적인 로직을 Presenter로 분리시키기가 어려워, 사실상 Presenter가 View와 책임 분담이 제대로 되지 않았다.&lt;/li&gt;
&lt;li&gt;Presenter가 둘 사이에서 많은 일들을 해야하기 때문에, 많은 책임을 안게 되어 클래스가 지나치게 비대해져갔다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 많은 어려움을 느낀 것이 View와 Presenter 로직의 분리였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 로직은 액티비티의 Context가 필요한 경우가 많거나 Intent를 이용해야 해서, 사실상 특정한 같은 기능을 실행하지만 View와 Presenter가 그 기능을 구현하기 위해 로직을 분담해야만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 눈길이 갔던 것이 MVVM이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내게는 생소했지만 최근의 구직 시장에서도 MVVM을 구현할 줄 아는 인재의 수요가 많은 것 같았고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다도 구글에서도 공식적으로 MVVM을 구현하기 위한 AAC와 Jetpack 컴포넌트들을 내놓으면서, 진입장벽도 많이 낮아졌다는 느낌이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFF6pn/btryeyjQcBI/CZmshH9HGkHmsBz9XezU91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFF6pn/btryeyjQcBI/CZmshH9HGkHmsBz9XezU91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFF6pn/btryeyjQcBI/CZmshH9HGkHmsBz9XezU91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFF6pn%2FbtryeyjQcBI%2FCZmshH9HGkHmsBz9XezU91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;573&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVVM 패턴의 흐름을 표현하고 있는 그림이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림으로 표현된 MVVM 패턴을 간략하게 설명해보면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View는 MVP 패턴에서와 마찬가지로 데이터를 불러와 사용자에게 표시해주는 역할을 하고, 사용자로부터 동작이 들어오면 ViewModel에 데이터 변경을 요청한다.&lt;/li&gt;
&lt;li&gt;ViewModel은 View에서 들어온 요청을 받아 데이터를 저장하고 있는 Model에 다시 요청한다&lt;/li&gt;
&lt;li&gt;요청을 받은 Model은 데이터를 가공하여 ViewModel로 다시 전달하고,&lt;/li&gt;
&lt;li&gt;ViewModel은 데이터를 받아 저장한다.&lt;/li&gt;
&lt;li&gt;View는 ViewModel의 데이터를 관찰하는 Observer를 가지고 있어, 변경된 데이터가 있으면 바로 사용자에게 보여준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View는 ViewModel 객체로 가져 데이터를 요청하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 데이터를 요청하고 저장하기만 하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 바라보고 있는 View는 그대로 데이터를 뿌려주기만 하면 된다고 보면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 구현하기 위해 구글은 AAC(Android Architecture Component)라는 것을 제공한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YyNT4/btryezJI7iM/GwsjsoavJU9Vt1GfkbYO31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YyNT4/btryezJI7iM/GwsjsoavJU9Vt1GfkbYO31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YyNT4/btryezJI7iM/GwsjsoavJU9Vt1GfkbYO31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYyNT4%2FbtryezJI7iM%2FGwsjsoavJU9Vt1GfkbYO31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;720&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AAC는 각 안드로이드 컴포넌트의 생명주기를 고려하여 만들어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 보이는 Repository가 Model의 역할을 한다고 보면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model이 데이터에 접근하여 제공하는 역할을 하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 DB는 외부 서버에 접근하는 경우가 많으므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 데이터는 Room에 저장하고, 외부 데이터는 Retrofit 등의 API 통신 프레임워크등을 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 두 접근을 Repository가 책임 진다고 보면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에는 이 구조를 실제 코드로 구현하는 과정에 대해 포스팅해보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 MVVM을 처음 적용해보므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정에 부족함이 있다고 하더라도 양해 바란다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@hwi_chance/Android-안드로이드-AAC&quot;&gt;https://velog.io/@hwi_chance/Android-안드로이드-AAC&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://beomy.tistory.com/43&quot;&gt;https://beomy.tistory.com/43&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>AAC</category>
      <category>MVVM</category>
      <category>디자인패턴</category>
      <category>아키텍처패턴</category>
      <category>안드로이드</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/62</guid>
      <comments>https://behappyaftercoding.tistory.com/62#entry62comment</comments>
      <pubDate>Sat, 2 Apr 2022 21:24:22 +0900</pubDate>
    </item>
    <item>
      <title>[Android]Firebase로 채팅 앱 만들기(6)</title>
      <link>https://behappyaftercoding.tistory.com/61</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 각 채팅방을 선택했을 때 이동할 채팅방 화면을 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPSnMq/btrx60lEbuK/fM4hANJSw3t5L67eNoY24k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPSnMq/btrx60lEbuK/fM4hANJSw3t5L67eNoY24k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPSnMq/btrx60lEbuK/fM4hANJSw3t5L67eNoY24k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPSnMq%2Fbtrx60lEbuK%2FfM4hANJSw3t5L67eNoY24k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;641&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 채팅방에 필요한 UI와, 메시지의 배경 레이아웃을 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:id=&quot;@+id/background&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;@color/white&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/txt_TItle&quot;
        style=&quot;@style/boldBlack&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;30dp&quot;
        android:text=&quot;홍길동&quot;
        android:textSize=&quot;18dp&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;ImageButton
        android:id=&quot;@+id/imgbtn_quit&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginStart=&quot;20dp&quot;
        android:layout_marginLeft=&quot;20dp&quot;
        android:background=&quot;@android:color/transparent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/txt_TItle&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/txt_TItle&quot;
        app:srcCompat=&quot;@drawable/btn_quit&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/textView2&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginStart=&quot;10dp&quot;
        android:layout_marginLeft=&quot;10dp&quot;
        android:layout_marginTop=&quot;60dp&quot;
        android:layout_marginEnd=&quot;10dp&quot;
        android:layout_marginRight=&quot;10dp&quot;
        android:text=&quot;오늘&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/divider_right&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/divider_left&quot;
        app:layout_constraintTop_toBottomOf=&quot;@+id/txt_TItle&quot; /&amp;gt;

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/divider_left&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;1dp&quot;
        android:layout_marginEnd=&quot;10dp&quot;
        android:layout_marginRight=&quot;10dp&quot;
        android:background=&quot;#B6B6B6&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/textView2&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/textView2&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/textView2&quot;&amp;gt;

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

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/divider_right&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;1dp&quot;
        android:layout_marginStart=&quot;10dp&quot;
        android:layout_marginLeft=&quot;10dp&quot;
        android:background=&quot;#B6B6B6&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/textView2&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/textView2&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/textView2&quot;&amp;gt;

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

    &amp;lt;EditText
        android:id=&quot;@+id/edt_message&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;0dp&quot;
        android:background=&quot;@android:color/transparent&quot;
        android:hint=&quot;메시지 입력&quot;
        android:paddingLeft=&quot;20dp&quot;
        android:paddingRight=&quot;20dp&quot;
        android:textColor=&quot;@color/black&quot;
        android:textSize=&quot;18dp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/btn_submit&quot;
        app:layout_constraintHeight_percent=&quot;0.1&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/divider_bottom&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;1dp&quot;
        android:background=&quot;#B6B6B6&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/edt_message&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;&amp;gt;

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

    &amp;lt;Button
        android:id=&quot;@+id/btn_submit&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;0dp&quot;
        android:text=&quot;전송&quot;
        android:textColor=&quot;@color/black&quot;
        app:backgroundTint=&quot;@android:color/transparent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/edt_message&quot; /&amp;gt;

    &amp;lt;androidx.recyclerview.widget.RecyclerView
        android:id=&quot;@+id/recycler_messages&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        android:layout_marginStart=&quot;20dp&quot;
        android:layout_marginTop=&quot;20dp&quot;
        android:layout_marginEnd=&quot;20dp&quot;
        android:layout_marginBottom=&quot;20dp&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/edt_message&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toBottomOf=&quot;@+id/textView2&quot;
        tools:listitem=&quot;@layout/list_talk_item_mine&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 화면에 필요한 레이아웃에 사용되는 XML이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:id=&quot;@+id/background&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:layout_marginTop=&quot;20dp&quot;
    android:layout_marginBottom=&quot;20dp&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/txt_message&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:autoLink=&quot;web&quot;
        android:background=&quot;@drawable/background_talk_mine&quot;
        android:gravity=&quot;start|center_vertical&quot;
        android:linksClickable=&quot;true&quot;
        android:padding=&quot;10dp&quot;
        android:text=&quot;안녕하세요.&quot;
        android:textColor=&quot;@color/white&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/txt_date&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginEnd=&quot;10dp&quot;
        android:layout_marginRight=&quot;10dp&quot;
        android:text=&quot;오전 10:23&quot;
        android:textSize=&quot;12dp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/txt_message&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/txt_message&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/txt_message&quot;
        app:layout_constraintVertical_bias=&quot;0.8&quot; /&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/txt_isShown&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginEnd=&quot;10dp&quot;
        android:text=&quot;1&quot;
        android:textColor=&quot;#FFBE3C&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/txt_date&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/txt_date&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/txt_date&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOUzCq/btrx5A847Ep/8cPuv6onZJsk0SYBkmBvJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOUzCq/btrx5A847Ep/8cPuv6onZJsk0SYBkmBvJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOUzCq/btrx5A847Ep/8cPuv6onZJsk0SYBkmBvJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOUzCq%2Fbtrx5A847Ep%2F8cPuv6onZJsk0SYBkmBvJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;342&quot; height=&quot;68&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅에 사용되는 메시지의 레이아웃이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;shape xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&amp;gt;
    &amp;lt;solid android:color=&quot;#6365FF&quot;/&amp;gt;
    &amp;lt;corners android:radius=&quot;15dp&quot;/&amp;gt;
&amp;lt;/shape&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;background의 배경은 drawable에 corner를 설정하여 둥근 테두리 배경을 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나/상대방 구분은 같은 배경에 색상을 변경하여 구분하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RequiresApi(Build.VERSION_CODES.O)
class ChatRoomActivity : AppCompatActivity() {
    lateinit var binding: ActivityChatroomBinding
    lateinit var btn_exit: ImageButton
    lateinit var btn_submit: Button
    lateinit var txt_title: TextView
    lateinit var edt_message: EditText
    lateinit var firebaseDatabase: DatabaseReference
    lateinit var recycler_talks: RecyclerView
    lateinit var chatRoom: ChatRoom
    lateinit var opponentUser: User
    lateinit var chatRoomKey: String
    lateinit var myUid: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityChatroomBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initializeProperty()
        initializeView()
        initializeListener()
        setupChatRooms()
    }

    fun initializeProperty() {  //변수 초기화
        myUid = FirebaseAuth.getInstance().currentUser?.uid!!              //현재 로그인한 유저 id
        firebaseDatabase = FirebaseDatabase.getInstance().reference!!

        chatRoom = (intent.getSerializableExtra(&quot;ChatRoom&quot;)) as ChatRoom      //채팅방 정보
        chatRoomKey = intent.getStringExtra(&quot;ChatRoomKey&quot;)!!            //채팅방 키
        opponentUser = (intent.getSerializableExtra(&quot;Opponent&quot;)) as User    //상대방 유저 정보
    }

    fun initializeView() {    //뷰 초기화
        btn_exit = binding.imgbtnQuit
        edt_message = binding.edtMessage
        recycler_talks = binding.recyclerMessages
        btn_submit = binding.btnSubmit
        txt_title = binding.txtTItle
        txt_title.text = opponentUser!!.name ?: &quot;&quot;
    }

    fun initializeListener() {   //버튼 클릭 시 리스너 초기화
        btn_exit.setOnClickListener()
        {
            startActivity(Intent(this@ChatRoomActivity, MainActivity::class.java))
        }
        btn_submit.setOnClickListener()
        {
            putMessage()
        }
    }

    fun setupChatRooms() {              //채팅방 목록 초기화 및 표시
        if (chatRoomKey.isNullOrBlank())
            setupChatRoomKey()
        else
            setupRecycler()
    }

    fun setupChatRoomKey() {            //chatRoomKey 없을 경우 초기화 후 목록 초기화
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
            .child(&quot;chatRooms&quot;).orderByChild(&quot;users/${opponentUser.uid}&quot;).equalTo(true)    //상대방의 Uid가 포함된 목록이 있는지 확인
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    for (data in snapshot.children) {
                        chatRoomKey = data.key!!          //chatRoomKey 초기화
                        setupRecycler()                  //목록 업데이트
                        break
                    }
                }
            })
    }

    fun putMessage() {       //메시지 전송
        try {
            var message = Message(myUid, getDateTimeString(), edt_message.text.toString())    //메시지 정보 초기화
            Log.i(&quot;ChatRoomKey&quot;, chatRoomKey)
            FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;).child(&quot;chatRooms&quot;)
                .child(chatRoomKey).child(&quot;messages&quot;)                   //현재 채팅방에 메시지 추가
                .push().setValue(message).addOnSuccessListener {
                    Log.i(&quot;putMessage&quot;, &quot;메시지 전송에 성공하였습니다.&quot;)
                    edt_message.text.clear()
                }.addOnCanceledListener {
                    Log.i(&quot;putMessage&quot;, &quot;메시지 전송에 실패하였습니다&quot;)
                }
        } catch (e: Exception) {
            e.printStackTrace()
            Log.i(&quot;putMessage&quot;, &quot;메시지 전송 중 오류가 발생하였습니다.&quot;)
        }
    }

    fun getDateTimeString(): String {          //메시지 보낸 시각 정보 반환
        try {
            var localDateTime = LocalDateTime.now()
            localDateTime.atZone(TimeZone.getDefault().toZoneId())
            var dateTimeFormatter = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;)
            return localDateTime.format(dateTimeFormatter).toString()
        } catch (e: Exception) {
            e.printStackTrace()
            throw Exception(&quot;getTimeError&quot;)
        }
    }

    fun setupRecycler() {            //목록 초기화 및 업데이트
        recycler_talks.layoutManager = LinearLayoutManager(this)
        recycler_talks.adapter = RecyclerMessagesAdapter(this, chatRoomKey, opponentUser.uid)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 레이아웃을 표시해줄 액티비티의 전체 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 뷰들을 초기화 해준 뒤에 채팅방의 메시지 목록을 불러와 셋 업해준 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X 버튼을 누를 시와 전송 버튼을 누를 시 메시지 전송을 실행할 리스너를 등록해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun setupChatRoomKey() {            //chatRoomKey 없을 경우 초기화 후 목록 초기화
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
            .child(&quot;chatRooms&quot;).orderByChild(&quot;users/${opponentUser.uid}&quot;).equalTo(true)    //상대방의 Uid가 포함된 목록이 있는지 확인
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    for (data in snapshot.children) {
                        chatRoomKey = data.key!!          //chatRoomKey 초기화
                        setupRecycler()                  //목록 업데이트
                        break
                    }
                }
            })
    }

fun setupRecycler() {            //목록 초기화 및 업데이트
        recycler_talks.layoutManager = LinearLayoutManager(this)
        recycler_talks.adapter = RecyclerMessagesAdapter(this, chatRoomKey, opponentUser.uid)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대방의 Uid를 포함하는 채팅방을 찾아서 chatRoomKey를 얻고, 그걸 토대로 해당 채팅방의 정보들을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, 목록을 표시할 리사이클러뷰에 chatRoomKey와 상대방 유저의 uid를 함께 넘겨서 초기화해준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun putMessage() {       //메시지 전송
        try {
            var message = Message(myUid, getDateTimeString(), edt_message.text.toString())    //메시지 정보 초기화
            Log.i(&quot;ChatRoomKey&quot;, chatRoomKey)
            FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;).child(&quot;chatRooms&quot;)
                .child(chatRoomKey).child(&quot;messages&quot;)                   //현재 채팅방에 메시지 추가
                .push().setValue(message).addOnSuccessListener {
                    Log.i(&quot;putMessage&quot;, &quot;메시지 전송에 성공하였습니다.&quot;)
                    edt_message.text.clear()
                }.addOnCanceledListener {
                    Log.i(&quot;putMessage&quot;, &quot;메시지 전송에 실패하였습니다&quot;)
                }
        } catch (e: Exception) {
            e.printStackTrace()
            Log.i(&quot;putMessage&quot;, &quot;메시지 전송 중 오류가 발생하였습니다.&quot;)
        }
    }

    fun getDateTimeString(): String {          //메시지 보낸 시각 정보 반환
        try {
            var localDateTime = LocalDateTime.now()
            localDateTime.atZone(TimeZone.getDefault().toZoneId())
            var dateTimeFormatter = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;)
            return localDateTime.format(dateTimeFormatter).toString()
        } catch (e: Exception) {
            e.printStackTrace()
            throw Exception(&quot;getTimeError&quot;)
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 전송 시 실행될 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getDateTimeString으로 메시지를 보내는 현재 시각을 구하고,입력란의 텍스트를 Message 객체에 넣어 채팅방 데이터의 messages 노드에 저장한 뒤 입력란을 비운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 채팅방에서 사용되는 버튼에 대한 처리들은 구현되었으니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방에서 주고받은 메시지를 표시하여야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cA7Rzk/btrx6IyL4y4/1T8ClQs4jDOANGP048Kyyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cA7Rzk/btrx6IyL4y4/1T8ClQs4jDOANGP048Kyyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cA7Rzk/btrx6IyL4y4/1T8ClQs4jDOANGP048Kyyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcA7Rzk%2Fbtrx6IyL4y4%2F1T8ClQs4jDOANGP048Kyyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;641&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 형태도 마찬가지로 리사이클러뷰로 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 상대방과 나의 메시지는 다른 레이아웃으로 구분되어야 하므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 리사이클러뷰에 두 개의 ViewHolder를 써야만 했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class RecyclerMessagesAdapter(
    val context: Context,
    var chatRoomKey: String?,
    val opponentUid: String?
) :
    RecyclerView.Adapter&amp;lt;RecyclerView.ViewHolder&amp;gt;() {
    var messages: ArrayList&amp;lt;Message&amp;gt; = arrayListOf()     //메시지 목록
    var messageKeys: ArrayList&amp;lt;String&amp;gt; = arrayListOf()   //메시지 키 목록
    val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()
    val recyclerView = (context as ChatRoomActivity).recycler_talks   //목록이 표시될 리사이클러 뷰

    init {
        setupMessages()
    }

    fun setupMessages() {
        getMessages()
    }

    fun getMessages() {
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
            .child(&quot;chatRooms&quot;).child(chatRoomKey!!).child(&quot;messages&quot;)   //전체 메시지 목록 가져오기
            .addValueEventListener(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    messages.clear()
                    for (data in snapshot.children) {
                        messages.add(data.getValue&amp;lt;Message&amp;gt;()!!)         //메시지 목록에 추가
                        messageKeys.add(data.key!!)                        //메시지 키 목록에 추가
                    }
                    notifyDataSetChanged()          //화면 업데이트
                    recyclerView.scrollToPosition(messages.size - 1)    //스크롤 최 하단으로 내리기
                }
            })
    }

    override fun getItemViewType(position: Int): Int {               //메시지의 id에 따라 내 메시지/상대 메시지 구분
        return if (messages[position].senderUid.equals(myUid)) 1 else 0
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            1 -&amp;gt; {            //메시지가 내 메시지인 경우
                val view =
                    LayoutInflater.from(context)
                        .inflate(R.layout.list_talk_item_mine, parent, false)   //내 메시지 레이아웃으로 초기화

                MyMessageViewHolder(ListTalkItemMineBinding.bind(view))
            }
            else -&amp;gt; {      //메시지가 상대 메시지인 경우
                val view =
                    LayoutInflater.from(context)
                        .inflate(R.layout.list_talk_item_others, parent, false)  //상대 메시지 레이아웃으로 초기화
                OtherMessageViewHolder(ListTalkItemOthersBinding.bind(view))
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (messages[position].senderUid.equals(myUid)) {       //레이아웃 항목 초기화
            (holder as MyMessageViewHolder).bind(position)
        } else {
            (holder as OtherMessageViewHolder).bind(position)
        }
    }

    override fun getItemCount(): Int {
        return messages.size
    }

    inner class OtherMessageViewHolder(itemView: ListTalkItemOthersBinding) :         //상대 메시지 뷰홀더
        RecyclerView.ViewHolder(itemView.root) {
        var background = itemView.background
        var txtMessage = itemView.txtMessage
        var txtDate = itemView.txtDate
        var txtIsShown = itemView.txtIsShown

        fun bind(position: Int) {           //메시지 UI 항목 초기화
            var message = messages[position]
            var sendDate = message.sended_date

            txtMessage.text = message.content

            txtDate.text = getDateText(sendDate)

            if (message.confirmed.equals(true))           //확인 여부 표시
                txtIsShown.visibility = View.GONE
            else
                txtIsShown.visibility = View.VISIBLE

            setShown(position)             //해당 메시지 확인하여 서버로 전송
        }

        fun getDateText(sendDate: String): String {    //메시지 전송 시각 생성

            var dateText = &quot;&quot;
            var timeString = &quot;&quot;
            if (sendDate.isNotBlank()) {
                timeString = sendDate.substring(8, 12)
                var hour = timeString.substring(0, 2)
                var minute = timeString.substring(2, 4)

                var timeformat = &quot;%02d:%02d&quot;

                if (hour.toInt() &amp;gt; 11) {
                    dateText += &quot;오후 &quot;
                    dateText += timeformat.format(hour.toInt() - 12, minute.toInt())
                } else {
                    dateText += &quot;오전 &quot;
                    dateText += timeformat.format(hour.toInt(), minute.toInt())
                }
            }
            return dateText
        }

        fun setShown(position: Int) {          //메시지 확인하여 서버로 전송
            FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
                .child(&quot;chatRooms&quot;).child(chatRoomKey!!).child(&quot;messages&quot;)
                .child(messageKeys[position]).child(&quot;confirmed&quot;).setValue(true)
                .addOnSuccessListener {
                    Log.i(&quot;checkShown&quot;, &quot;성공&quot;)
                }
        }
    }

    inner class MyMessageViewHolder(itemView: ListTalkItemMineBinding) :       // 내 메시지용 ViewHolder
        RecyclerView.ViewHolder(itemView.root) {
        var background = itemView.background
        var txtMessage = itemView.txtMessage
        var txtDate = itemView.txtDate
        var txtIsShown = itemView.txtIsShown

        fun bind(position: Int) {            //메시지 UI 레이아웃 초기화
            var message = messages[position]
            var sendDate = message.sended_date
            txtMessage.text = message.content

            txtDate.text = getDateText(sendDate)

            if (message.confirmed.equals(true))
                txtIsShown.visibility = View.GONE
            else
                txtIsShown.visibility = View.VISIBLE
        }

        fun getDateText(sendDate: String): String {        //메시지 전송 시각 생성
            var dateText = &quot;&quot;
            var timeString = &quot;&quot;
            if (sendDate.isNotBlank()) {
                timeString = sendDate.substring(8, 12)
                var hour = timeString.substring(0, 2)
                var minute = timeString.substring(2, 4)

                var timeformat = &quot;%02d:%02d&quot;

                if (hour.toInt() &amp;gt; 11) {
                    dateText += &quot;오후 &quot;
                    dateText += timeformat.format(hour.toInt() - 12, minute.toInt())
                } else {
                    dateText += &quot;오전 &quot;
                    dateText += timeformat.format(hour.toInt(), minute.toInt())
                }
            }
            return dateText
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 표시하는 어댑터의 전체 코드이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class RecyclerMessagesAdapter(
    val context: Context,
    var chatRoomKey: String?,
    val opponentUid: String?
) :
    RecyclerView.Adapter&amp;lt;RecyclerView.ViewHolder&amp;gt;() {
    var messages: ArrayList&amp;lt;Message&amp;gt; = arrayListOf()     //메시지 목록
    var messageKeys: ArrayList&amp;lt;String&amp;gt; = arrayListOf()   //메시지 키 목록
    val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()
    val recyclerView = (context as ChatRoomActivity).recycler_talks   //목록이 표시될 리사이클러 뷰

    init {
        setupMessages()
    }

    fun setupMessages() {
        getMessages()
    }

    fun getMessages() {
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
            .child(&quot;chatRooms&quot;).child(chatRoomKey!!).child(&quot;messages&quot;)   //전체 메시지 목록 가져오기
            .addValueEventListener(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    messages.clear()
                    for (data in snapshot.children) {
                        messages.add(data.getValue&amp;lt;Message&amp;gt;()!!)         //메시지 목록에 추가
                        messageKeys.add(data.key!!)                        //메시지 키 목록에 추가
                    }
                    notifyDataSetChanged()          //화면 업데이트
                    recyclerView.scrollToPosition(messages.size - 1)    //스크롤 최 하단으로 내리기
                }
            })
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자로부터 건네받은 채팅방의 키와 상대방의 Uid로 채팅방의 메시지 목록을 불러와 각 항목과 항목의 키들을 리스트에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메시지가 추가될 때마다 화면을 업데이트 한 뒤 스크롤을 맨 아래로 당기도록 한다.(초기화 될 때마다 위로 올라가므로)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;inner class OtherMessageViewHolder(itemView: ListTalkItemOthersBinding) :         //상대 메시지 뷰홀더
        RecyclerView.ViewHolder(itemView.root) {
        var background = itemView.background
        var txtMessage = itemView.txtMessage
        var txtDate = itemView.txtDate
        var txtIsShown = itemView.txtIsShown

        fun bind(position: Int) {           //메시지 UI 항목 초기화
            var message = messages[position]
            var sendDate = message.sended_date

            txtMessage.text = message.content

            txtDate.text = getDateText(sendDate)

            if (message.confirmed.equals(true))           //확인 여부 표시
                txtIsShown.visibility = View.GONE
            else
                txtIsShown.visibility = View.VISIBLE

            setShown(position)             //해당 메시지 확인하여 서버로 전송
        }

        fun getDateText(sendDate: String): String {    //메시지 전송 시각 생성

            var dateText = &quot;&quot;
            var timeString = &quot;&quot;
            if (sendDate.isNotBlank()) {
                timeString = sendDate.substring(8, 12)
                var hour = timeString.substring(0, 2)
                var minute = timeString.substring(2, 4)

                var timeformat = &quot;%02d:%02d&quot;

                if (hour.toInt() &amp;gt; 11) {
                    dateText += &quot;오후 &quot;
                    dateText += timeformat.format(hour.toInt() - 12, minute.toInt())
                } else {
                    dateText += &quot;오전 &quot;
                    dateText += timeformat.format(hour.toInt(), minute.toInt())
                }
            }
            return dateText
        }

        fun setShown(position: Int) {          //메시지 확인하여 서버로 전송
            FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
                .child(&quot;chatRooms&quot;).child(chatRoomKey!!).child(&quot;messages&quot;)
                .child(messageKeys[position]).child(&quot;confirmed&quot;).setValue(true)
                .addOnSuccessListener {
                    Log.i(&quot;checkShown&quot;, &quot;성공&quot;)
                }
        }
    }

    inner class MyMessageViewHolder(itemView: ListTalkItemMineBinding) :       // 내 메시지용 ViewHolder
        RecyclerView.ViewHolder(itemView.root) {
        var background = itemView.background
        var txtMessage = itemView.txtMessage
        var txtDate = itemView.txtDate
        var txtIsShown = itemView.txtIsShown

        fun bind(position: Int) {            //메시지 UI 레이아웃 초기화
            var message = messages[position]
            var sendDate = message.sended_date
            txtMessage.text = message.content

            txtDate.text = getDateText(sendDate)

            if (message.confirmed.equals(true))
                txtIsShown.visibility = View.GONE
            else
                txtIsShown.visibility = View.VISIBLE
        }

        fun getDateText(sendDate: String): String {        //메시지 전송 시각 생성
            var dateText = &quot;&quot;
            var timeString = &quot;&quot;
            if (sendDate.isNotBlank()) {
                timeString = sendDate.substring(8, 12)
                var hour = timeString.substring(0, 2)
                var minute = timeString.substring(2, 4)

                var timeformat = &quot;%02d:%02d&quot;

                if (hour.toInt() &amp;gt; 11) {
                    dateText += &quot;오후 &quot;
                    dateText += timeformat.format(hour.toInt() - 12, minute.toInt())
                } else {
                    dateText += &quot;오전 &quot;
                    dateText += timeformat.format(hour.toInt(), minute.toInt())
                }
            }
            return dateText
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 상대방 메시지와 내 메시지를 구분할 ViewHolder를 따로 정의해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;override fun getItemViewType(position: Int): Int {               //메시지의 id에 따라 내 메시지/상대 메시지 구분
        return if (messages[position].senderUid.equals(myUid)) 1 else 0
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            1 -&amp;gt; {            //메시지가 내 메시지인 경우
                val view =
                    LayoutInflater.from(context)
                        .inflate(R.layout.list_talk_item_mine, parent, false)   //내 메시지 레이아웃으로 초기화

                MyMessageViewHolder(ListTalkItemMineBinding.bind(view))
            }
            else -&amp;gt; {      //메시지가 상대 메시지인 경우
                val view =
                    LayoutInflater.from(context)
                        .inflate(R.layout.list_talk_item_others, parent, false)  //상대 메시지 레이아웃으로 초기화
                OtherMessageViewHolder(ListTalkItemOthersBinding.bind(view))
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (messages[position].senderUid.equals(myUid)) {       //레이아웃 항목 초기화
            (holder as MyMessageViewHolder).bind(position)
        } else {
            (holder as OtherMessageViewHolder).bind(position)
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 포지션의 메시지의 송신자 id와 현재 로그인한 유저의 id를 비교하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일치하면 1,아니면 0으로 viewType을 반환하도록 하면,OnCreateViewHolder에서 받아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1이면 내 메시지를 표시하는 ViewHolder,0이면 상대 메시지를 표시하는 ViewHolder로 바인딩 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt; fun bind(position: Int) {           //메시지 UI 항목 초기화
            var message = messages[position]
            var sendDate = message.sended_date

            txtMessage.text = message.content

            txtDate.text = getDateText(sendDate)

            if (message.confirmed.equals(true))           //확인 여부 표시
                txtIsShown.visibility = View.GONE
            else
                txtIsShown.visibility = View.VISIBLE

            setShown(position)             //해당 메시지 확인하여 서버로 전송
        }

        fun getDateText(sendDate: String): String {    //메시지 전송 시각 생성

            var dateText = &quot;&quot;
            var timeString = &quot;&quot;
            if (sendDate.isNotBlank()) {
                timeString = sendDate.substring(8, 12)
                var hour = timeString.substring(0, 2)
                var minute = timeString.substring(2, 4)

                var timeformat = &quot;%02d:%02d&quot;

                if (hour.toInt() &amp;gt; 11) {
                    dateText += &quot;오후 &quot;
                    dateText += timeformat.format(hour.toInt() - 12, minute.toInt())
                } else {
                    dateText += &quot;오전 &quot;
                    dateText += timeformat.format(hour.toInt(), minute.toInt())
                }
            }
            return dateText
        }

        fun setShown(position: Int) {          //메시지 확인하여 서버로 전송
            FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)
                .child(&quot;chatRooms&quot;).child(chatRoomKey!!).child(&quot;messages&quot;)
                .child(messageKeys[position]).child(&quot;confirmed&quot;).setValue(true)
                .addOnSuccessListener {
                    Log.i(&quot;checkShown&quot;, &quot;성공&quot;)
                }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 viewHolder에는 위와 같은 함수가 정의되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰의 항목들을 초기화 하고, 메시지 옆에 표시될 시각을 계산하여 텍스트를 초기화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메시지가 초기화되는 순간 상대방의 메시지를 확인했다는 것이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 상대 메시지를 읽음 처리하도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B6rvx/btrx07gdeZV/gaaQnudurrdiIakGyMink1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B6rvx/btrx07gdeZV/gaaQnudurrdiIakGyMink1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B6rvx/btrx07gdeZV/gaaQnudurrdiIakGyMink1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/B6rvx/btrx07gdeZV/gaaQnudurrdiIakGyMink1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;644&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 작동되는 모습이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 채팅방에 필요한 UI와, 메시지의 배경 레이아웃을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 화면에 필요한 레이아웃에 사용되는 XML이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/61</guid>
      <comments>https://behappyaftercoding.tistory.com/61#entry61comment</comments>
      <pubDate>Thu, 31 Mar 2022 15:03:20 +0900</pubDate>
    </item>
    <item>
      <title>[Android]Firebase로 채팅 앱 만들기(5)</title>
      <link>https://behappyaftercoding.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 새로 채팅할 상대를 추가할 수 있는 검색창을 만들어보도록 하겠다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class AddChatRoomActivity : AppCompatActivity() {
    lateinit var binding:ActivityAddChatroomBinding
    lateinit var btn_exit: ImageButton
    lateinit var edt_opponent:EditText
    lateinit var firebaseDatabase:DatabaseReference
    lateinit var recycler_people:RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAddChatroomBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initializeView()
        initializeListener()
        setupRecycler()
    }

    fun initializeView()   //뷰 초기화
    {
        firebaseDatabase = FirebaseDatabase.getInstance().reference!!
        btn_exit = binding.imgbtnBack
        edt_opponent = binding.edtOpponentName
        recycler_people = binding.recyclerPeoples
    }
    fun initializeListener()   //버튼 클릭 시 리스너 초기화
    {
        btn_exit.setOnClickListener()
        {
            startActivity(Intent(this@AddChatRoomActivity, MainActivity::class.java))
        }

        edt_opponent.addTextChangedListener(object :TextWatcher                  //검색 창에 입력된 글자가 변경될 때마다 검색 내용 업데이트
        {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            }

            override fun afterTextChanged(s: Editable?) {
                var adapter = recycler_people?.adapter as RecyclerUsersAdapter
                adapter.searchItem(s.toString())                  //입력된 검색어로 검색 진행 및 업데이트
            }
        })
    }
    fun setupRecycler()   //사용자 목록 초기화 및 업데이트
    {
       recycler_people.layoutManager = LinearLayoutManager(this)
        recycler_people.adapter = RecyclerUsersAdapter(this)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 대화를 시작할 친구를 검색할 액티비티의 전체 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 화면에 필요한 뷰들을 초기화한 후,&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;edt_opponent.addTextChangedListener(object :TextWatcher                  //검색 창에 입력된 글자가 변경될 때마다 검색 내용 업데이트
        {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            }

            override fun afterTextChanged(s: Editable?) {
                var adapter = recycler_people?.adapter as RecyclerUsersAdapter
                adapter.searchItem(s.toString())                  //입력된 검색어로 검색 진행 및 업데이트
            }
        })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beIfV0/btrx2SP9whG/yUheaW2xtLryKZ50HKAP8K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beIfV0/btrx2SP9whG/yUheaW2xtLryKZ50HKAP8K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beIfV0/btrx2SP9whG/yUheaW2xtLryKZ50HKAP8K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/beIfV0/btrx2SP9whG/yUheaW2xtLryKZ50HKAP8K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;637&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색어 입력란에 단어와 일치하는 사용자를 바로 검색하게 하기 위해 리스너를 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 각 사용자들의 목록을 불러올 Recycler를 만들어야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class RecyclerUsersAdapter(val context: Context) :
    RecyclerView.Adapter&amp;lt;RecyclerUsersAdapter.ViewHolder&amp;gt;() {
    var users: ArrayList&amp;lt;User&amp;gt; =arrayListOf()        //검색어로 일치한 사용자 목록
    val allUsers: ArrayList&amp;lt;User&amp;gt; =arrayListOf()    //전체 사용자 목록
    lateinit var currnentUser: User

    init {
        setupAllUserList()
    }

    fun setupAllUserList() {        //전체 사용자 목록 불러오기
        val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()        //현재 사용자 아이디
        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)   //사용자 데이터 요청
            .addValueEventListener(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {
                }

                override fun onDataChange(snapshot: DataSnapshot) {
                    users.clear()
                    for (data in snapshot.children) {
                        val item = data.getValue&amp;lt;User&amp;gt;()
                        if (item?.uid.equals(myUid)) {
                            currnentUser = item!!             //전체 사용자 목록에서 현재 사용자는 제외
                            continue
                        }
                        allUsers.add(item!!)              //전체 사용자 목록에 추가
                    }
                    users = allUsers.clone() as ArrayList&amp;lt;User&amp;gt;
                    notifyDataSetChanged()              //화면 업데이트
                }
            })
    }

    fun searchItem(target: String) {            //검색
        if (target.equals(&quot;&quot;)) {      //검색어 없는 경우 전체 목록 표시
            users = allUsers.clone() as ArrayList&amp;lt;User&amp;gt;
        } else {
            var matchedList = allUsers.filter{ it.name!!.contains(target)}//검색어 포함된 항목 불러오기
            users.clear()
            matchedList.forEach{users.add(it)}
}
        notifyDataSetChanged()          //화면 업데이트
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.txt_name.text= users[position].name
        holder.txt_email.text= users[position].email

        holder.background.setOnClickListener()
{
addChatRoom(position)        //해당 사용자 선택 시
}
}

    fun addChatRoom(position: Int) {     //채팅방 추가
        val opponent = users[position]   //채팅할 상대방 정보
        var database = FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)    //넣을 database reference 세팅
        var chatRoom = ChatRoom(         //추가할 채팅방 정보 세팅
mapOf(currnentUser.uid!!totrue, opponent.uid!!totrue),
            null
        )
        var myUid = FirebaseAuth.getInstance().uid//내 Uid
        database.child(&quot;chatRooms&quot;)
            .orderByChild(&quot;users/${opponent.uid}&quot;).equalTo(true)       //상대방 Uid가 포함된 채팅방이 있는 지 확인
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    if (snapshot.value== null) {              //채팅방이 없는 경우
                        database.child(&quot;chatRooms&quot;).push().setValue(chatRoom).addOnSuccessListener{// 채팅방 새로 생성 후 이동
                            goToChatRoom(chatRoom, opponent)
}
} else {
                        context.startActivity(Intent(context, MainActivity::class.java))
                        goToChatRoom(chatRoom, opponent)                    //해당 채팅방으로 이동
                    }

                }
            })
    }

    fun goToChatRoom(chatRoom: ChatRoom, opponentUid: User) {       //채팅방으로 이동
        var intent = Intent(context, ChatRoomActivity::class.java)
        intent.putExtra(&quot;ChatRoom&quot;, chatRoom)       //채팅방 정보
        intent.putExtra(&quot;Opponent&quot;, opponentUid)    //상대방 정보
        intent.putExtra(&quot;ChatRoomKey&quot;, &quot;&quot;)   //채팅방 키
        context.startActivity(intent)
        (context as AppCompatActivity).finish()
    }

    override fun getItemCount(): Int {
        return users.size
    }

    inner class ViewHolder(itemView: ListPersonItemBinding) :
        RecyclerView.ViewHolder(itemView.root) {
        var background = itemView.background
        var txt_name = itemView.txtName
        var txt_email = itemView.txtEmail
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 목록을 불러오는 Recycler의 전체 코드다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt; fun setupAllUserList() {        //전체 사용자 목록 불러오기
        val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()        //현재 사용자 아이디
        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)   //사용자 데이터 요청
            .addValueEventListener(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {
                }

                override fun onDataChange(snapshot: DataSnapshot) {
                    users.clear()
                    for (data in snapshot.children) {
                        val item = data.getValue&amp;lt;User&amp;gt;()
                        if (item?.uid.equals(myUid)) {
                            currnentUser = item!!             //전체 사용자 목록에서 현재 사용자는 제외
                            continue
                        }
                        allUsers.add(item!!)              //전체 사용자 목록에 추가
                    }
                    users = allUsers.clone() as ArrayList&amp;lt;User&amp;gt;
                    notifyDataSetChanged()              //화면 업데이트
                }
            })
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자들의 정보가 포함된 데이터베이스인 User 데이터베이스에 접근하여 목록을 가져와 저장한 뒤,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용자의 uid와 비교하여 자신은 제외한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt; fun searchItem(target: String) {            //검색
        if (target.equals(&quot;&quot;)) {      //검색어 없는 경우 전체 목록 표시
            users = allUsers.clone() as ArrayList&amp;lt;User&amp;gt;
        } else {
            var matchedList = allUsers.filter{ it.name!!.contains(target)}//검색어 포함된 항목 불러오기
            users.clear()
            matchedList.forEach{users.add(it)}
}
        notifyDataSetChanged()          //화면 업데이트
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력란에 검색어를 넣는 경우 위 코드를 실행하여 불러온 전체 목록에서 검색어를 포함하는 목록만 남겨 리스트를 업데이트 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt; override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.txt_name.text= users[position].name
        holder.txt_email.text= users[position].email

        holder.background.setOnClickListener()
{
addChatRoom(position)        //해당 사용자 선택 시
}
}

    fun addChatRoom(position: Int) {     //채팅방 추가
        val opponent = users[position]   //채팅할 상대방 정보
        var database = FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)    //넣을 database reference 세팅
        var chatRoom = ChatRoom(         //추가할 채팅방 정보 세팅
mapOf(currnentUser.uid!!totrue, opponent.uid!!totrue),
            null
        )
        var myUid = FirebaseAuth.getInstance().uid//내 Uid
        database.child(&quot;chatRooms&quot;)
            .orderByChild(&quot;users/${opponent.uid}&quot;).equalTo(true)       //상대방 Uid가 포함된 채팅방이 있는 지 확인
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    if (snapshot.value== null) {              //채팅방이 없는 경우
                        database.child(&quot;chatRooms&quot;).push().setValue(chatRoom).addOnSuccessListener{// 채팅방 새로 생성 후 이동
                            goToChatRoom(chatRoom, opponent)
}
} else {
                        context.startActivity(Intent(context, MainActivity::class.java))
                        goToChatRoom(chatRoom, opponent)                    //해당 채팅방으로 이동
                    }

                }
            })
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목을 선택 시 채팅방 목록 중에서 해당 사용자의 Uid가 포함된 채팅방을 찾아 해당 채팅방 화면으로 이동하고, 없으면 새로 데이터베이스에 채팅방을 삽입한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP57CI/btrx4S3b6ou/lvPVYs9o84J8zcWyS62aXk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP57CI/btrx4S3b6ou/lvPVYs9o84J8zcWyS62aXk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP57CI/btrx4S3b6ou/lvPVYs9o84J8zcWyS62aXk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cP57CI/btrx4S3b6ou/lvPVYs9o84J8zcWyS62aXk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;643&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 화면이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/60</guid>
      <comments>https://behappyaftercoding.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 31 Mar 2022 14:58:52 +0900</pubDate>
    </item>
    <item>
      <title>[Android]Firebase로 채팅 앱 만들기(4)</title>
      <link>https://behappyaftercoding.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 페이지와 로그인 페이지도 만들어졌으니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 진입할 홈 화면을 만들 차례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 카카오톡처럼 채팅방 목록과 함께 새 채팅방을 추가할 수 있는 버튼과 로그아웃 버튼을 표시했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;@color/white&quot;
    tools:context=&quot;.main.MainActivity&quot;&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/txt_TItle&quot;
        style=&quot;@style/boldBlack&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginTop=&quot;30dp&quot;
        android:text=&quot;메시지&quot;
        android:textSize=&quot;18dp&quot;
        app:layout_constraintEnd_toStartOf=&quot;@+id/btn_new_message&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/btn_signout&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/btn_new_message&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_marginEnd=&quot;20dp&quot;
        android:layout_marginRight=&quot;20dp&quot;
        android:background=&quot;@android:color/transparent&quot;
        android:text=&quot;새 메시지&quot;
        android:textColor=&quot;#3F51B5&quot;
        android:textSize=&quot;18dp&quot;
        android:textStyle=&quot;normal&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/txt_TItle&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/txt_TItle&quot; /&amp;gt;

    &amp;lt;Button
        android:id=&quot;@+id/btn_signout&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:background=&quot;@android:color/transparent&quot;
        android:text=&quot;로그아웃&quot;
        android:textColor=&quot;#8D8D8E&quot;
        android:textSize=&quot;18dp&quot;
        android:textStyle=&quot;normal&quot;
        app:layout_constraintBottom_toBottomOf=&quot;@+id/txt_TItle&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;@+id/txt_TItle&quot; /&amp;gt;

    &amp;lt;androidx.recyclerview.widget.RecyclerView
        android:id=&quot;@+id/recycler_chatrooms&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        android:layout_marginTop=&quot;40dp&quot;
        android:layout_marginBottom=&quot;40dp&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toBottomOf=&quot;@+id/txt_TItle&quot;
        tools:listitem=&quot;@layout/list_chatroom_item&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 레이아웃 코드는 위와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.miso.chatapplication.main

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.*
import com.miso.chatapplication.LoginActivity
import com.miso.chatapplication.addChatRoom.AddChatRoomActivity
import com.miso.chatapplication.databinding.ActivityMainBinding
import com.miso.chatapplication.model.ChatRoom

@RequiresApi(Build.VERSION_CODES.O)
class MainActivity : AppCompatActivity() {
    lateinit var btnAddchatRoom: Button
    lateinit var btnSignout: Button
    lateinit var binding: ActivityMainBinding
    lateinit var firebaseDatabase: DatabaseReference
    lateinit var recycler_chatroom: RecyclerView

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

    fun initializeView() { //뷰 초기화
        try {
            firebaseDatabase = FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;)!!
            btnSignout = binding.btnSignout
            btnAddchatRoom = binding.btnNewMessage
            recycler_chatroom = binding.recyclerChatrooms
        }catch (e:Exception)
        {
            e.printStackTrace()
            Toast.makeText(this,&quot;화면 초기화 중 오류가 발생하였습니다.&quot;,Toast.LENGTH_LONG).show()
        }
    }
    fun initializeListener()  //버튼 클릭 시 리스너 초기화
    {
        btnSignout.setOnClickListener()
        {
            signOut()
        }
        btnAddchatRoom.setOnClickListener()  //새 메시지 화면으로 이동
        {
            startActivity(Intent(this@MainActivity, AddChatRoomActivity::class.java))
            finish()
        }
    }

    fun setupRecycler() {
        recycler_chatroom.layoutManager = LinearLayoutManager(this)
        recycler_chatroom.adapter = RecyclerChatRoomsAdapter(this)
    }

    fun signOut()    //로그아웃 실행
    {
        try {
            val builder = AlertDialog.Builder(this)
                .setTitle(&quot;로그아웃&quot;)
                .setMessage(&quot;로그아웃 하시겠습니까?&quot;)
                .setPositiveButton(&quot;확인&quot;
                ) { dialog, id -&amp;gt;
                    try {
                        FirebaseAuth.getInstance().signOut()             //로그아웃
                        startActivity(Intent(this@MainActivity, LoginActivity::class.java))
                        dialog.dismiss()
                        finish()
                    } catch (e: Exception) {
                        e.printStackTrace()
                        dialog.dismiss()
                        Toast.makeText(this, &quot;로그아웃 중 오류가 발생하였습니다.&quot;, Toast.LENGTH_LONG).show()
                    }
                }
                .setNegativeButton(&quot;취소&quot;          //다이얼로그 닫기
                ) { dialog, id -&amp;gt;
                    dialog.dismiss()
                }
            builder.show()
        }catch (e:Exception)
        {
            e.printStackTrace()
            Toast.makeText(this,&quot;로그아웃 중 오류가 발생하였습니다.&quot;,Toast.LENGTH_LONG).show()
        }
    }

    override fun onBackPressed() {
        signOut()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액티비티 전체 코드는 위와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun signOut()    //로그아웃 실행
    {
        try {
            val builder = AlertDialog.Builder(this)
                .setTitle(&quot;로그아웃&quot;)
                .setMessage(&quot;로그아웃 하시겠습니까?&quot;)
                .setPositiveButton(&quot;확인&quot;
                ) { dialog, id -&amp;gt;
                    try {
                        FirebaseAuth.getInstance().signOut()             //로그아웃
                        startActivity(Intent(this@MainActivity, LoginActivity::class.java))
                        dialog.dismiss()
                        finish()
                    } catch (e: Exception) {
                        e.printStackTrace()
                        dialog.dismiss()
                        Toast.makeText(this, &quot;로그아웃 중 오류가 발생하였습니다.&quot;, Toast.LENGTH_LONG).show()
                    }
                }
                .setNegativeButton(&quot;취소&quot;          //다이얼로그 닫기
                ) { dialog, id -&amp;gt;
                    dialog.dismiss()
                }
            builder.show()
        }catch (e:Exception)
        {
            e.printStackTrace()
            Toast.makeText(this,&quot;로그아웃 중 오류가 발생하였습니다.&quot;,Toast.LENGTH_LONG).show()
        }
    }

    override fun onBackPressed() {
        signOut()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 버튼을 눌렀을 시 다이얼로그를 띄우도록 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 버튼을 누를 시 로그아웃을 실행한 후 로그인 화면으로 이동하도록 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단의 백 버튼을 눌렀을 때에도 동일하게 동작되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방을 불러와 목록으로 표시하는 로직은 별도의 RecyclerAdapter를 만들어 수행하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에,각 채팅방에서 사용할 정보를 담을 채팅방 객체를 만들어야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package com.miso.chatapplication.model

import android.os.Parcelable
import java.io.Serializable

data class ChatRoom(
    val users: Map&amp;lt;String, Boolean&amp;gt;? = HashMap(),
    var messages: Map&amp;lt;String,Message&amp;gt;? = HashMap()
) : Serializable {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방의 정보를 저장하는 ChatRoom 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase RealtimeDatabase는 JSON 형태로 데이터를 저장하기 떄문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map형태로 데이터를 전달하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 일전에 값을 List형태로 전달해보았으나 런타임 오류로 실패했기에,Map을 전달하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방에 포함된 사용자는 users에 저장하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 채팅방에서 오간 메시지는 messages에 저장하기로 하였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package com.miso.chatapplication.model

import java.io.Serializable

data class Message(
    var senderUid: String = &quot;&quot;,
    var sended_date: String = &quot;&quot;,
    var content: String = &quot;&quot;,
    var confirmed:Boolean=false
) : Serializable {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방에서 오간 메시지를 저장하는 객체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차례대로 보낸 사람의 uid,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보낸 시각,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지의 내용,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대방의 확인 여부를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 data class는 인텐트로 전달될 필요가 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Serializable을 구현하도록 하였다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RequiresApi(Build.VERSION_CODES.O)
class RecyclerChatRoomsAdapter(val context: Context) :
    RecyclerView.Adapter&amp;lt;RecyclerChatRoomsAdapter.ViewHolder&amp;gt;() {
    var chatRooms: ArrayList&amp;lt;ChatRoom&amp;gt; = arrayListOf()   //채팅방 목록
    var chatRoomKeys: ArrayList&amp;lt;String&amp;gt; = arrayListOf()  //채팅방 키 목록
    val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()   //현재 사용자 Uid

    init {
        setupAllUserList()
    }

    fun setupAllUserList() {     //전체 채팅방 목록 초기화 및 업데이트
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;).child(&quot;chatRooms&quot;)
            .orderByChild(&quot;users/$myUid&quot;).equalTo(true)
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    chatRooms.clear()
                    for (data in snapshot.children) {
                        chatRooms.add(data.getValue&amp;lt;ChatRoom&amp;gt;()!!)
                        chatRoomKeys.add(data.key!!)
                    }
                    notifyDataSetChanged()
                }
            })

    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        var userIdList = chatRooms[position].users!!.keys    //채팅방에 포함된 사용자 키 목록
        var opponent = userIdList.first { !it.equals(myUid) }  //상대방 사용자 키
        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;).orderByChild(&quot;uid&quot;)   //상대방 사용자 키를 포함하는 채팅방 불러오기
            .equalTo(opponent)
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    for (data in snapshot.children) {
                        holder.chatRoomKey = data.key.toString()!!             //채팅방 키 초기화
                        holder.opponentUser = data.getValue&amp;lt;User&amp;gt;()!!         //상대방 정보 초기화
                        holder.txt_name.text = data.getValue&amp;lt;User&amp;gt;()!!.name.toString()     //상대방 이름 초괴화
                    }
                }
            })
        holder.background.setOnClickListener()               //채팅방 항목 선택 시
        {
            try {
                var intent = Intent(context, ChatRoomActivity::class.java)
                intent.putExtra(&quot;ChatRoom&quot;, chatRooms.get(position))      //채팅방 정보
                intent.putExtra(&quot;Opponent&quot;, holder.opponentUser)          //상대방 사용자 정보
                intent.putExtra(&quot;ChatRoomKey&quot;, chatRoomKeys[position])     //채팅방 키 정보
                context.startActivity(intent)                            //해당 채팅방으로 이동
                (context as AppCompatActivity).finish()
            }catch (e:Exception)
            {
                e.printStackTrace()
                Toast.makeText(context,&quot;채팅방 이동 중 문제가 발생하였습니다.&quot;,Toast.LENGTH_SHORT).show()
            }
        }

        if (chatRooms[position].messages!!.size &amp;gt; 0) {         //채팅방 메시지가 존재하는 경우
            setupLastMessageAndDate(holder, position)        //마지막 메시지 및 시각 초기화
            setupMessageCount(holder, position)
        }
    }

    fun setupLastMessageAndDate(holder: ViewHolder, position: Int) { //마지막 메시지 및 시각 초기화
        try {
            var lastMessage =
                chatRooms[position].messages!!.values.sortedWith(compareBy({ it.sended_date }))    //메시지 목록에서 시각을 비교하여 가장 마지막 메시지  가져오기
                    .last()
            holder.txt_message.text = lastMessage.content                 //마지막 메시지 표시
            holder.txt_date.text = getLastMessageTimeString(lastMessage.sended_date)   //마지막으로 전송된 시각 표시
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    fun setupMessageCount(holder: ViewHolder, position: Int) {            //확인되지 않은 메시지 개수 표시
        try {
            var unconfirmedCount =
                chatRooms[position].messages!!.filter {
                    !it.value.confirmed &amp;amp;&amp;amp; !it.value.senderUid.equals(               //메시지 중 확인되지 않은 메시지 개수 가져오기
                        myUid
                    )
                }.size
            if (unconfirmedCount &amp;gt; 0) {              //확인되지 않은 메시지가 있을 경우
                holder.txt_chatCount.visibility = View.VISIBLE           //개수 표시
                holder.txt_chatCount.text = unconfirmedCount.toString()
            } else
                holder.txt_chatCount.visibility = View.GONE
        } catch (e: Exception) {
            e.printStackTrace()
            holder.txt_chatCount.visibility = View.GONE
        }
    }

    fun getLastMessageTimeString(lastTimeString: String): String {           //마지막 메시지가 전송된 시각 구하기
        try {
            var currentTime = LocalDateTime.now().atZone(TimeZone.getDefault().toZoneId()) //현재 시각
            var dateTimeFormatter = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;)

            var messageMonth = lastTimeString.substring(4, 6).toInt()                   //마지막 메시지 시각 월,일,시,분
            var messageDate = lastTimeString.substring(6, 8).toInt()
            var messageHour = lastTimeString.substring(8, 10).toInt()
            var messageMinute = lastTimeString.substring(10, 12).toInt()

            var formattedCurrentTimeString = currentTime.format(dateTimeFormatter)     //현 시각 월,일,시,분
            var currentMonth = formattedCurrentTimeString.substring(4, 6).toInt()
            var currentDate = formattedCurrentTimeString.substring(6, 8).toInt()
            var currentHour = formattedCurrentTimeString.substring(8, 10).toInt()
            var currentMinute = formattedCurrentTimeString.substring(10, 12).toInt()

            var monthAgo = currentMonth - messageMonth                           //현 시각과 마지막 메시지 시각과의 차이. 월,일,시,분
            var dayAgo = currentDate - messageDate
            var hourAgo = currentHour - messageHour
            var minuteAgo = currentMinute - messageMinute

            if (monthAgo &amp;gt; 0)                                         //1개월 이상 차이 나는 경우
                return monthAgo.toString() + &quot;개월 전&quot;
            else {
                if (dayAgo &amp;gt; 0) {                                  //1일 이상 차이 나는 경우
                    if (dayAgo == 1)
                        return &quot;어제&quot;
                    else
                        return dayAgo.toString() + &quot;일 전&quot;
                } else {
                    if (hourAgo &amp;gt; 0)
                        return hourAgo.toString() + &quot;시간 전&quot;     //1시간 이상 차이 나는 경우
                    else {
                        if (minuteAgo &amp;gt; 0)                       //1분 이상 차이 나는 경우
                            return minuteAgo.toString() + &quot;분 전&quot;
                        else
                            return &quot;방금&quot;
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return &quot;&quot;
        }
    }

    override fun getItemCount(): Int {
        return chatRooms.size
    }

    inner class ViewHolder(itemView: ListChatroomItemBinding) :
        RecyclerView.ViewHolder(itemView.root) {
        var opponentUser = User(&quot;&quot;, &quot;&quot;)
        var chatRoomKey = &quot;&quot;
        var background = itemView.background
        var txt_name = itemView.txtName
        var txt_message = itemView.txtMessage
        var txt_date = itemView.txtMessageDate
        var txt_chatCount = itemView.txtChatCount
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방을 불러오는 RecyclerChatRoomsAdapter의 전체 코드는 위와 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;var chatRooms: ArrayList&amp;lt;ChatRoom&amp;gt; = arrayListOf()   //채팅방 목록
    var chatRoomKeys: ArrayList&amp;lt;String&amp;gt; = arrayListOf()  //채팅방 키 목록
    val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString()   //현재 사용자 Uid

    init {
        setupAllUserList()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 첫째로 채팅방과 각 채팅방의 키를 받아올 채팅방 키를 저장할 ArrayList들과,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 로그인한 사용자의 Uid를 저장할 변수를 선언하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성과 동시에 전체 채팅방 목록을 초기화하도록 하였다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun setupAllUserList() {     //전체 채팅방 목록 초기화 및 업데이트
        FirebaseDatabase.getInstance().getReference(&quot;ChatRoom&quot;).child(&quot;chatRooms&quot;)
            .orderByChild(&quot;users/$myUid&quot;).equalTo(true)
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    chatRooms.clear()
                    for (data in snapshot.children) {
                        chatRooms.add(data.getValue&amp;lt;ChatRoom&amp;gt;()!!)
                        chatRoomKeys.add(data.key!!)
                    }
                    notifyDataSetChanged()
                }
            })

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방 데이터를 저장하고 있는 ChatRoom 항목에서 자신이 포함된 채팅방들만 불러오기 위하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;myUid를 child로 가지고 있는 항목만 추려온 뒤, chatRooms와 chatRoomKey에 각각 항목을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        var userIdList = chatRooms[position].users!!.keys    //채팅방에 포함된 사용자 키 목록
        var opponent = userIdList.first { !it.equals(myUid) }  //상대방 사용자 키
        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;).orderByChild(&quot;uid&quot;)   //상대방 사용자 키를 포함하는 채팅방 불러오기
            .equalTo(opponent)
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {}
                override fun onDataChange(snapshot: DataSnapshot) {
                    for (data in snapshot.children) {
                        holder.chatRoomKey = data.key.toString()!!             //채팅방 키 초기화
                        holder.opponentUser = data.getValue&amp;lt;User&amp;gt;()!!         //상대방 정보 초기화
                        holder.txt_name.text = data.getValue&amp;lt;User&amp;gt;()!!.name.toString()     //상대방 이름 초괴화
                    }
                }
            })
        holder.background.setOnClickListener()               //채팅방 항목 선택 시
        {
            try {
                var intent = Intent(context, ChatRoomActivity::class.java)
                intent.putExtra(&quot;ChatRoom&quot;, chatRooms.get(position))      //채팅방 정보
                intent.putExtra(&quot;Opponent&quot;, holder.opponentUser)          //상대방 사용자 정보
                intent.putExtra(&quot;ChatRoomKey&quot;, chatRoomKeys[position])     //채팅방 키 정보
                context.startActivity(intent)                            //해당 채팅방으로 이동
                (context as AppCompatActivity).finish()
            }catch (e:Exception)
            {
                e.printStackTrace()
                Toast.makeText(context,&quot;채팅방 이동 중 문제가 발생하였습니다.&quot;,Toast.LENGTH_SHORT).show()
            }
        }

        if (chatRooms[position].messages!!.size &amp;gt; 0) {         //채팅방 메시지가 존재하는 경우
            setupLastMessageAndDate(holder, position)        //마지막 메시지 및 시각 초기화
            setupMessageCount(holder, position)
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화된 각 채팅방에서 상대방 사용자의 키를 얻어내고, 사용자 목록이 포함된 Users 데이터베이스에서 상대방 사용자를 찾아 정보를 불러온 뒤, 상대방의 이름을 초기화하여 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 채팅방 항목을 선택 시, 해당 상대방과의 채팅방 정보, 상대방 사용자의 정보, 키를 Intent로 넘겨 채팅방 화면으로 전환하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 채팅방에 메시지가 존재하는 경우, 마지막 메시지와 메시지를 전송한 시각도 함께 표시해준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun setupLastMessageAndDate(holder: ViewHolder, position: Int) { //마지막 메시지 및 시각 초기화
        try {
            var lastMessage =
                chatRooms[position].messages!!.values.sortedWith(compareBy({ it.sended_date }))    //메시지 목록에서 시각을 비교하여 가장 마지막 메시지  가져오기
                    .last()
            holder.txt_message.text = lastMessage.content                 //마지막 메시지 표시
            holder.txt_date.text = getLastMessageTimeString(lastMessage.sended_date)   //마지막으로 전송된 시각 표시
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 채팅방에 포함된 메시지들을 비교해서 시각이 가장 큰 메시지를 뽑아 텍스트 표시&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun getLastMessageTimeString(lastTimeString: String): String {           //마지막 메시지가 전송된 시각 구하기
        try {
            var currentTime = LocalDateTime.now().atZone(TimeZone.getDefault().toZoneId()) //현재 시각
            var dateTimeFormatter = DateTimeFormatter.ofPattern(&quot;yyyyMMddHHmmss&quot;)

            var messageMonth = lastTimeString.substring(4, 6).toInt()                   //마지막 메시지 시각 월,일,시,분
            var messageDate = lastTimeString.substring(6, 8).toInt()
            var messageHour = lastTimeString.substring(8, 10).toInt()
            var messageMinute = lastTimeString.substring(10, 12).toInt()

            var formattedCurrentTimeString = currentTime.format(dateTimeFormatter)     //현 시각 월,일,시,분
            var currentMonth = formattedCurrentTimeString.substring(4, 6).toInt()
            var currentDate = formattedCurrentTimeString.substring(6, 8).toInt()
            var currentHour = formattedCurrentTimeString.substring(8, 10).toInt()
            var currentMinute = formattedCurrentTimeString.substring(10, 12).toInt()

            var monthAgo = currentMonth - messageMonth                           //현 시각과 마지막 메시지 시각과의 차이. 월,일,시,분
            var dayAgo = currentDate - messageDate
            var hourAgo = currentHour - messageHour
            var minuteAgo = currentMinute - messageMinute

            if (monthAgo &amp;gt; 0)                                         //1개월 이상 차이 나는 경우
                return monthAgo.toString() + &quot;개월 전&quot;
            else {
                if (dayAgo &amp;gt; 0) {                                  //1일 이상 차이 나는 경우
                    if (dayAgo == 1)
                        return &quot;어제&quot;
                    else
                        return dayAgo.toString() + &quot;일 전&quot;
                } else {
                    if (hourAgo &amp;gt; 0)
                        return hourAgo.toString() + &quot;시간 전&quot;     //1시간 이상 차이 나는 경우
                    else {
                        if (minuteAgo &amp;gt; 0)                       //1분 이상 차이 나는 경우
                            return minuteAgo.toString() + &quot;분 전&quot;
                        else
                            return &quot;방금&quot;
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return &quot;&quot;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 시각을 오늘 날짜와 비교하여 마지막으로 전송된 메시지를 다음과 같이 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex)3분 전, 10분 전, 어제,3일 전..&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun setupMessageCount(holder: ViewHolder, position: Int) {            //확인되지 않은 메시지 개수 표시
        try {
            var unconfirmedCount =
                chatRooms[position].messages!!.filter {
                    !it.value.confirmed &amp;amp;&amp;amp; !it.value.senderUid.equals(               //메시지 중 확인되지 않은 메시지 개수 가져오기
                        myUid
                    )
                }.size
            if (unconfirmedCount &amp;gt; 0) {              //확인되지 않은 메시지가 있을 경우
                holder.txt_chatCount.visibility = View.VISIBLE           //개수 표시
                holder.txt_chatCount.text = unconfirmedCount.toString()
            } else
                holder.txt_chatCount.visibility = View.GONE
        } catch (e: Exception) {
            e.printStackTrace()
            holder.txt_chatCount.visibility = View.GONE
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방의 메시지 중에서 확인되지 않은 메시지의 개수를 세어 표시해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckBWJs/btrx30NUxHA/r33vC83cgQHkLMkRkWdftK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckBWJs/btrx30NUxHA/r33vC83cgQHkLMkRkWdftK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckBWJs/btrx30NUxHA/r33vC83cgQHkLMkRkWdftK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckBWJs%2Fbtrx30NUxHA%2Fr33vC83cgQHkLMkRkWdftK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;663&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 화면이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/59</guid>
      <comments>https://behappyaftercoding.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 31 Mar 2022 14:55:10 +0900</pubDate>
    </item>
    <item>
      <title>[Android]Firebase로 채팅 앱 만들기(3)</title>
      <link>https://behappyaftercoding.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 페이지에서는 채팅 앱에서 참여할 사용자를 추가할 수 있는 회원가입 화면을 만들어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 만들어진 회원의 계정으로 로그인할 수 있는 로그인 페이지를 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그인 레이아웃을 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;#3660DC&quot;&amp;gt;

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/constraintLayout&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        android:background=&quot;@color/white&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintHeight_percent=&quot;0.6&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edt_email&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_marginBottom=&quot;20dp&quot;
            android:ems=&quot;10&quot;
            android:hint=&quot;Email&quot;
            android:inputType=&quot;textPersonName&quot;
            android:textColor=&quot;@color/black&quot;
            android:textColorHint=&quot;#B8B8B8&quot;
            app:layout_constraintBottom_toTopOf=&quot;@id/edt_password&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot;
            app:layout_constraintVertical_chainStyle=&quot;packed&quot; /&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edt_password&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:ems=&quot;10&quot;
            android:hint=&quot;비밀번호&quot;
            android:inputType=&quot;textPersonName|textPassword&quot;
            android:textColor=&quot;@color/black&quot;
            android:textColorHint=&quot;#B8B8B8&quot;
            app:layout_constraintBottom_toTopOf=&quot;@id/btn_sign_in&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintHorizontal_bias=&quot;0.502&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toBottomOf=&quot;@+id/edt_email&quot; /&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/btn_signup&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;회원가입&quot;
            android:textColor=&quot;@color/white&quot;
            app:backgroundTint=&quot;#3660DC&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;@+id/edt_password&quot;
            app:layout_constraintStart_toStartOf=&quot;@+id/edt_password&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/btn_sign_in&quot; /&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/btn_sign_in&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_marginTop=&quot;20dp&quot;
            android:text=&quot;로그인&quot;
            android:textColor=&quot;@color/white&quot;
            app:backgroundTint=&quot;#3660DC&quot;
            app:layout_constraintBottom_toTopOf=&quot;@id/btn_signup&quot;
            app:layout_constraintEnd_toEndOf=&quot;@+id/edt_password&quot;
            app:layout_constraintStart_toStartOf=&quot;@+id/edt_password&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/edt_password&quot; /&amp;gt;
    &amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/textView3&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;ChatingApp&quot;
        android:textColor=&quot;#FFFFFF&quot;
        android:textSize=&quot;60dp&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/constraintLayout&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 레이아웃 xml은 위와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.miso.chatapplication

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.SignInButton
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.GoogleAuthProvider
import com.miso.chatapplication.databinding.ActivityLoginBinding
import com.miso.chatapplication.main.MainActivity

class LoginActivity : AppCompatActivity() {
    lateinit var auth: FirebaseAuth
    lateinit var btn_signUp: Button
    lateinit var btn_signIn: Button
    lateinit var edt_email: EditText
    lateinit var edt_password: EditText
    lateinit var binding: ActivityLoginBinding
    lateinit var preference: SharedPreferences
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initProperty()
        initializeView()
        initializeListener()
    }

    fun initProperty() {    //초기 변수 세팅
        auth = FirebaseAuth.getInstance()     //Firebase 계정 관련 변수
        preference = getSharedPreferences(&quot;setting&quot;, MODE_PRIVATE)    //로그인 정보 저장용 SharedPreference
    }

    fun initializeView() {   //뷰 초기화
        btn_signUp = binding.btnSignup
        btn_signIn = binding.btnSignIn
        edt_email = binding.edtEmail
        edt_password = binding.edtPassword

        edt_email.setText(preference.getString(&quot;email&quot;, &quot;&quot;))     //마지막으로 로그인 한 이메일 세팅
        edt_password.setText(preference.getString(&quot;password&quot;, &quot;&quot;))   //마지막으로 로그인 한 패스워드 세팅
    }

    fun initializeListener() {        //버튼 클릭 시 리스너 세팅
        btn_signIn.setOnClickListener()
        {
            signInWithEmailAndPassword()
        }
        btn_signUp.setOnClickListener()
        {
            startActivity(Intent(this, SignUpActivity::class.java))
        }
    }

    fun signInWithEmailAndPassword() {
        if (edt_email.text.toString().isNullOrBlank() &amp;amp;&amp;amp;   //아이디 또는 패스워드가 입력되었는 지 유효성 검사
            edt_password.text.toString().isNullOrBlank())
            Toast.makeText(this, &quot;아이디 또는 패스워드를 입력해주세요&quot;, Toast.LENGTH_SHORT).show()
        else {
            auth.signInWithEmailAndPassword(edt_email.text.toString(), edt_password.text.toString())    //로그인 실행
                .addOnCompleteListener(this) { task -&amp;gt;
                    if (task.isSuccessful) {
                        Log.d(&quot;로그인&quot;, &quot;성공&quot;)
                        val user = auth.currentUser
                        updateUI(user)
                        finish()
                    } else {
                        Toast.makeText(this, &quot;로그인에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()
                    }
                }
        }
    }

    private fun updateUI(user: FirebaseUser?) { //로그인 성공 시 화면 이동
        if (user != null) {
            try {
                var preference = getSharedPreferences(&quot;setting&quot;, MODE_PRIVATE).edit()    //이메일 및 패스워드 저장
                preference.putString(&quot;email&quot;, edt_email.text.toString())
                preference.putString(&quot;password&quot;, edt_password.text.toString())
                preference.apply()
                val intent = Intent(this, MainActivity::class.java)
                startActivity(intent)
                finish()
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this, &quot;로그인에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 액티비티의 전체 코드는 위와 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun signInWithEmailAndPassword() {
        if (edt_email.text.toString().isNullOrBlank() &amp;amp;&amp;amp;   //아이디 또는 패스워드가 입력되었는 지 유효성 검사
            edt_password.text.toString().isNullOrBlank())
            Toast.makeText(this, &quot;아이디 또는 패스워드를 입력해주세요&quot;, Toast.LENGTH_SHORT).show()
        else {
            auth.signInWithEmailAndPassword(edt_email.text.toString(), edt_password.text.toString())    //로그인 실행
                .addOnCompleteListener(this) { task -&amp;gt;
                    if (task.isSuccessful) {
                        Log.d(&quot;로그인&quot;, &quot;성공&quot;)
                        val user = auth.currentUser
                        updateUI(user)
                        finish()
                    } else {
                        Toast.makeText(this, &quot;로그인에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()
                    }
                }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 버튼을 누를 시 실행되는 로그인 요청 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 회원가입과 같이 firebase authentification에 접근하기 위하여&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;auth = FirebaseAuth.getInstance()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용하여 auth 변수를 초기화 해준후,&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;auth.signInWithEmailAndPassword(edt_email.text.toString(), edt_password.text.toString())    //로그인 실행
                .addOnCompleteListener(this) { task -&amp;gt;
                    if (task.isSuccessful) {
                        Log.d(&quot;로그인&quot;, &quot;성공&quot;)
                        val user = auth.currentUser
                        updateUI(user)
                        finish()
                    } else {
                        Toast.makeText(this, &quot;로그인에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()
                    }
                }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수를 사용하여 로그인을 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 코드와 마찬가지로 task.isSuccessful로 성공 여부를 구분하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 시 다음 화면으로 진입하게 해주는 코드를 넣으면 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private fun updateUI(user: FirebaseUser?) { //로그인 성공 시 화면 이동
        if (user != null) {
            try {
                var preference = getSharedPreferences(&quot;setting&quot;, MODE_PRIVATE).edit()    //이메일 및 패스워드 저장
                preference.putString(&quot;email&quot;, edt_email.text.toString())
                preference.putString(&quot;password&quot;, edt_password.text.toString())
                preference.apply()
                val intent = Intent(this, MainActivity::class.java)
                startActivity(intent)
                finish()
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this, &quot;로그인에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우 채팅방을 보여줄 메인화면에서 사용할 주요 변수를 SharedPreference를 사용하여 저장하고,이후 이동하는 다음 액티비티에서 꺼내쓸 수 있게 하였다.&lt;/p&gt;
&lt;h1&gt;완성화면&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNmp0/btrx1IN085L/kqDKoxi3eUhJu5JGqTKfYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNmp0/btrx1IN085L/kqDKoxi3eUhJu5JGqTKfYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNmp0/btrx1IN085L/kqDKoxi3eUhJu5JGqTKfYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNmp0%2Fbtrx1IN085L%2FkqDKoxi3eUhJu5JGqTKfYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;645&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/58</guid>
      <comments>https://behappyaftercoding.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 31 Mar 2022 14:53:34 +0900</pubDate>
    </item>
    <item>
      <title>[Android]Firebase로 채팅 앱 만들기(2)</title>
      <link>https://behappyaftercoding.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 페이지에서는 안드로이드와 Firebase 간의 연동 설정을 끝냈으니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이젠 회원가입 페이지를 만들어보도록 하자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;#3660DC&quot;&amp;gt;

    &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
        android:id=&quot;@+id/constraintLayout&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;0dp&quot;
        android:background=&quot;@color/white&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintHeight_percent=&quot;0.6&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edt_opponent_name&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_marginBottom=&quot;20dp&quot;
            android:ems=&quot;10&quot;
            android:hint=&quot;이름&quot;
            android:inputType=&quot;textPersonName&quot;
            android:textColor=&quot;@color/black&quot;
            android:textColorHint=&quot;#B8B8B8&quot;
            app:layout_constraintBottom_toTopOf=&quot;@id/edt_email&quot;
            app:layout_constraㅁintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toTopOf=&quot;parent&quot;
            app:layout_constraintVertical_chainStyle=&quot;packed&quot; /&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edt_email&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_marginBottom=&quot;20dp&quot;
            android:ems=&quot;10&quot;
            android:hint=&quot;Email&quot;
            android:inputType=&quot;textPersonName&quot;
            android:textColor=&quot;@color/black&quot;
            android:textColorHint=&quot;#B8B8B8&quot;
            app:layout_constraintBottom_toTopOf=&quot;@id/edt_password&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/edt_opponent_name&quot;
            app:layout_constraintVertical_chainStyle=&quot;packed&quot; /&amp;gt;

        &amp;lt;EditText
            android:id=&quot;@+id/edt_password&quot;
            android:layout_width=&quot;wrap_content&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:ems=&quot;10&quot;
            android:hint=&quot;비밀번호&quot;
            android:inputType=&quot;textPersonName|textPassword&quot;
            android:textColor=&quot;@color/black&quot;
            android:textColorHint=&quot;#B8B8B8&quot;
            app:layout_constraintBottom_toTopOf=&quot;@+id/btn_signup&quot;
            app:layout_constraintEnd_toEndOf=&quot;parent&quot;
            app:layout_constraintHorizontal_bias=&quot;0.502&quot;
            app:layout_constraintStart_toStartOf=&quot;parent&quot;
            app:layout_constraintTop_toBottomOf=&quot;@+id/edt_email&quot; /&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/btn_signup&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;회원가입&quot;
            android:textColor=&quot;@color/white&quot;
            app:backgroundTint=&quot;#3660DC&quot;
            app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
            app:layout_constraintEnd_toEndOf=&quot;@+id/edt_password&quot;
            app:layout_constraintStart_toStartOf=&quot;@+id/edt_password&quot;
            app:layout_constraintTop_toBottomOf=&quot;@id/edt_password&quot; /&amp;gt;
    &amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/textView3&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;ChatingApp&quot;
        android:textColor=&quot;#FFFFFF&quot;
        android:textSize=&quot;60dp&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/constraintLayout&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;
&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 회원가입에 쓸 레이아웃 페이지를 만들어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfaYqw/btrx6IMf5Za/IQmNIL64v7bDcAw6EG293k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfaYqw/btrx6IMf5Za/IQmNIL64v7bDcAw6EG293k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfaYqw/btrx6IMf5Za/IQmNIL64v7bDcAw6EG293k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfaYqw%2Fbtrx6IMf5Za%2FIQmNIL64v7bDcAw6EG293k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;826&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성한 모습은 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 회원가입을 진행할 Activity를 만들어주자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.miso.chatapplication

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
import com.miso.chatapplication.databinding.ActivitySignupBinding
import com.miso.chatapplication.main.MainActivity
import com.miso.chatapplication.model.User

class SignUpActivity : AppCompatActivity() {
    lateinit var auth: FirebaseAuth
    lateinit var btn_signUp: Button
    lateinit var edt_email: EditText
    lateinit var edt_password: EditText
    lateinit var edt_name: EditText

    lateinit var binding: ActivitySignupBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySignupBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initializeView()
        initializeListener()
    }

    fun initializeView() {  //뷰 초기화
        auth = FirebaseAuth.getInstance()
        btn_signUp = binding.btnSignup
        edt_email = binding.edtEmail
        edt_password = binding.edtPassword
        edt_name = binding.edtOpponentName
    }

    fun initializeListener() {   //버튼 클릭 시 리스너 초기화
        btn_signUp.setOnClickListener()
        {
            signUp()
        }
    }

    fun signUp() {     //회원 가입 실행
        var email = edt_email.text.toString()           //각 입력란 값 String으로 변환
        var password = edt_password.text.toString()
        var name = edt_name.text.toString()

        auth.createUserWithEmailAndPassword(email, password)      //FirebaseAuth에 회원가입 성공 시
            .addOnCompleteListener(this) { task -&amp;gt;
                if (task.isSuccessful) {     //회원 가입 성공 시
                    try {
                        val user = auth.currentUser
                        val userId = user?.uid
                        val userIdSt = userId.toString()
                        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)
                            .child(userId.toString()).setValue(User(name, userIdSt, email))             //Firebase RealtimeDatabase에 User 정보 ㅊ추가
                        Toast.makeText(this, &quot;회원가입이 완료되었습니다.&quot;, Toast.LENGTH_SHORT).show()
                        Log.e(&quot;UserId&quot;, &quot;$userId&quot;)
                        startActivity(Intent(this@SignUpActivity, MainActivity::class.java))
                    } catch (e: Exception) {
                        e.printStackTrace()
                        Toast.makeText(this, &quot;화면 이동 중 문제가 발생하였습니다.&quot;, Toast.LENGTH_SHORT).show()
                    }
                } else
                    Toast.makeText(this, &quot;회원가입에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()

            }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드는 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 및 패스워드를 사용한 로그인을 구현하려면 서버가 필수적인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Authentification에서는 이러한 인증 절차를 간편하게 대신 수행해준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;lateinit var auth:FirebaseAuth
auth = FirebaseAuth.getInstance()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 통하여 FirebaseAuth 객체를 초기화해주면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 인증 절차를 진행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fun signUp() {     //회원 가입 실행
        var email = edt_email.text.toString()           //각 입력란 값 String으로 변환
        var password = edt_password.text.toString()
        var name = edt_name.text.toString()

        auth.createUserWithEmailAndPassword(email, password)      //FirebaseAuth에 회원가입 성공 시
            .addOnCompleteListener(this) { task -&amp;gt;
                if (task.isSuccessful) {     //회원 가입 성공 시
                    try {
                        val user = auth.currentUser
                        val userId = user?.uid
                        val userIdSt = userId.toString()
                        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)
                            .child(userId.toString()).setValue(User(name, userIdSt, email))             //Firebase RealtimeDatabase에 User 정보 ㅊ추가
                        Toast.makeText(this, &quot;회원가입이 완료되었습니다.&quot;, Toast.LENGTH_SHORT).show()
                        Log.e(&quot;UserId&quot;, &quot;$userId&quot;)
                        startActivity(Intent(this@SignUpActivity, MainActivity::class.java))
                    } catch (e: Exception) {
                        e.printStackTrace()
                        Toast.makeText(this, &quot;화면 이동 중 문제가 발생하였습니다.&quot;, Toast.LENGTH_SHORT).show()
                    }
                } else
                    Toast.makeText(this, &quot;회원가입에 실패하였습니다.&quot;, Toast.LENGTH_SHORT).show()

            }

    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 회원가입을 진행하기 위해 위와 같은 함수를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 각 이름,이메일,패스워드에 해당하는 입력란의 텍스트들을 가져오고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createUserWithEmailAndPassword(email, password)라는 함수를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 요청을 진행했다.단순히 이 함수만으로도 회원가입은 가능하겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 성공했는지에 대한 여부도 확인해야 하므로,&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;.addOnCompleteListener(this) { task -&amp;gt;
if(task.isSuccesful())
...
else
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 리스너도 추가해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task.isSuccesful()이 참이면, 회원등록에 성공했다는 뜻이고,그렇지 않으면 실패했다는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val user = auth.currentUser
                        val userId = user?.uid
                        val userIdSt = userId.toString()
                        FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)
                            .child(userId.toString()).setValue(User(name, userIdSt, email))             //Firebase RealtimeDatabase에 User 정보 ㅊ추가
                        Toast.makeText(this, &quot;회원가입이 완료되었습니다.&quot;, Toast.LENGTH_SHORT).show()
                        Log.e(&quot;UserId&quot;, &quot;$userId&quot;)
                        startActivity(Intent(this@SignUpActivity, MainActivity::class.java))
                   
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입에 성공했으면,해당 회원 정보를 가져와 데이터베이스에도 함께 등록해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FirebaseDatabase.getInstance().getReference(&quot;User&quot;).child(&quot;users&quot;)
                            .child(userId.toString()).setValue(User(name, userIdSt, email))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirebaseDatabase.getInstance()를 통해 ReatimeDatabase에 접근하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getReference(String)에 자신이 접근하고 싶은 노드 이름을 입력하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.child(String)에 자식 이름을 넣어 자식에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나같은 경우 users라는 자식에 내가 만든 회원의 userId를 넣어주었고,값으로 사용자 정보를 저장하는 객체인 User 클래스를 사용하여 넣어주었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package com.miso.chatapplication.model

import java.io.Serializable

data class User(val name:String?=&quot;&quot;,
                val uid:String?=&quot;&quot;,
                val email:String?=&quot;&quot;):Serializable {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User의 내용은 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입이 위와 같은 절차를 이루어 성공하면, MainActivity로 이동하게 해두었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/57</guid>
      <comments>https://behappyaftercoding.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 31 Mar 2022 14:50:23 +0900</pubDate>
    </item>
    <item>
      <title>[Android] Firebase로 채팅 앱 만들기(1)</title>
      <link>https://behappyaftercoding.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이직을 위해 구직활동을 하던 중 평소 눈여겨 보고 있던 회사에서 입사 제안이 와서 프로젝트 과제를 수행하게 되었다. 그 과제의 요구조건은 1:1 채팅 앱을 만들어보라는 것이었는데, 서버가 있다는 가정 하에서 진행을 해보라는 것이 내게는 참 막막했다. 그래서 어떻게 구현할 지 곰곰히 생각해보다가, 예전에 졸업작품을 만들 때 썼던 Firebase가 떠올라 검색을 해보니 Realtime Database를 통해 채팅 앱을 만들 수 있는 것 같아 나도 시도해보게 되었다. 그 결과 꽤나 보기좋은 형태의 결과가 나왔고, 이 과정을 글로 공유하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본래 회원가입 기능은 기능 요구사항에 포함되어 있지는 않았으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들어두는 것이 추후 프로젝트 진행에도 편리할 것 같아 만들어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처음 프로젝트를 시작하기 전, Firebase와 본인의 프로젝트를 연결해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0LjKR/btrx07f2tQd/jF6XMTscHewVjQpiz0Px51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0LjKR/btrx07f2tQd/jF6XMTscHewVjQpiz0Px51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0LjKR/btrx07f2tQd/jF6XMTscHewVjQpiz0Px51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0LjKR%2Fbtrx07f2tQd%2FjF6XMTscHewVjQpiz0Px51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;545&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;시작하기&amp;rsquo;를 눌러 이동해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cekNAK/btrx0KZFTCq/bjvtH0Owjg9ANpkg23tHrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cekNAK/btrx0KZFTCq/bjvtH0Owjg9ANpkg23tHrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cekNAK/btrx0KZFTCq/bjvtH0Owjg9ANpkg23tHrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcekNAK%2Fbtrx0KZFTCq%2FbjvtH0Owjg9ANpkg23tHrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;515&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인의 구글 계정으로 로그인해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcHoQ/btrx17Nqbw4/1lC3kTM0SdYaW8T522qGJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcHoQ/btrx17Nqbw4/1lC3kTM0SdYaW8T522qGJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcHoQ/btrx17Nqbw4/1lC3kTM0SdYaW8T522qGJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcHoQ%2Fbtrx17Nqbw4%2F1lC3kTM0SdYaW8T522qGJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1009&quot; height=&quot;611&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 현재 연동된 프로젝트가 표시된다. 새 프로젝트를 만들기 위해 프로젝트 추가 버튼을 누른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7efeO/btrx30G2QQy/j3gq7b2y60xh6MfjAwKDF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7efeO/btrx30G2QQy/j3gq7b2y60xh6MfjAwKDF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7efeO/btrx30G2QQy/j3gq7b2y60xh6MfjAwKDF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7efeO%2Fbtrx30G2QQy%2Fj3gq7b2y60xh6MfjAwKDF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;634&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들 프로젝트의 이름을 간단하게 입력해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brtDtI/btrx1HnUaDS/3FGC2agHlXL6kAMzOknQr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brtDtI/btrx1HnUaDS/3FGC2agHlXL6kAMzOknQr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brtDtI/btrx1HnUaDS/3FGC2agHlXL6kAMzOknQr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrtDtI%2Fbtrx1HnUaDS%2F3FGC2agHlXL6kAMzOknQr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;791&quot; height=&quot;687&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속을 눌러준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9jEIE/btrx3ZVD03e/l4sCbIpBYHnSRsyzWytk80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9jEIE/btrx3ZVD03e/l4sCbIpBYHnSRsyzWytk80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9jEIE/btrx3ZVD03e/l4sCbIpBYHnSRsyzWytk80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9jEIE%2Fbtrx3ZVD03e%2Fl4sCbIpBYHnSRsyzWytk80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;649&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애널리틱스 계정을 위와 같이 설정해주고,프로젝트 만들기를 선택해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5ud5r/btrx5z9Z3PH/u5KzawbWRN7oK0SHOS158K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5ud5r/btrx5z9Z3PH/u5KzawbWRN7oK0SHOS158K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5ud5r/btrx5z9Z3PH/u5KzawbWRN7oK0SHOS158K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5ud5r%2Fbtrx5z9Z3PH%2Fu5KzawbWRN7oK0SHOS158K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;461&quot; height=&quot;345&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금만 기다리면 프로젝트가 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4qM95/btrx1IfYKgi/hRXZRddgHIB3kyqe9PNpv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4qM95/btrx1IfYKgi/hRXZRddgHIB3kyqe9PNpv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4qM95/btrx1IfYKgi/hRXZRddgHIB3kyqe9PNpv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4qM95%2Fbtrx1IfYKgi%2FhRXZRddgHIB3kyqe9PNpv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;536&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 만들어졌다. 이제 본격적인 연동을 위해 안드로이드 아이콘을 선택해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbtyO/btrx1IG6NDE/d12qSnFoC0KeJhkmKOWM2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbtyO/btrx1IG6NDE/d12qSnFoC0KeJhkmKOWM2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbtyO/btrx1IG6NDE/d12qSnFoC0KeJhkmKOWM2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbtyO%2Fbtrx1IG6NDE%2Fd12qSnFoC0KeJhkmKOWM2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;837&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 패키지 네임을 입력하고, 앱 닉네임을 정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버그 서명 인증서는 뒤에서 설명할 예정이다. 다음을 누른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPgJ2r/btrx30ttnKz/vozb8PkwexwbQOaoZiv0t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPgJ2r/btrx30ttnKz/vozb8PkwexwbQOaoZiv0t0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPgJ2r/btrx30ttnKz/vozb8PkwexwbQOaoZiv0t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPgJ2r%2Fbtrx30ttnKz%2Fvozb8PkwexwbQOaoZiv0t0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;640&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 google-services.json을 다운로드하여 프로젝트의 app 폴더에 넣어주도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWfoBv/btrx1HVIprT/KP1TEG1ZVlHKVr9zC1mNNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWfoBv/btrx1HVIprT/KP1TEG1ZVlHKVr9zC1mNNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWfoBv/btrx1HVIprT/KP1TEG1ZVlHKVr9zC1mNNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWfoBv%2Fbtrx1HVIprT%2FKP1TEG1ZVlHKVr9zC1mNNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;293&quot; height=&quot;694&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 위와 같이 내 프로젝트 폴더에 넣었다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;buildscript {

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
  }

  dependencies {
    // ...

    // Add the following line:
    classpath 'com.google.gms:google-services:4.3.10'  // Google Services plugin
  }
}

allprojects {
  // ...

  repositories {
    // Check that you have the following line (if not, add it):
    google()  // Google's Maven repository
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 프로젝트 수준의 build.gradle 파일에 위와 같은 내용을 추가하고,&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;dependencies {
  // ...

  // Import the Firebase BoM
  implementation platform('com.google.firebase:firebase-bom:29.0.4')

  // When using the BoM, you don't specify versions in Firebase library dependencies

  // Declare the dependency for the Firebase SDK for Google Analytics
  implementation 'com.google.firebase:firebase-analytics-ktx'

  // Declare the dependencies for any other desired Firebase products
  // For example, declare the dependencies for Firebase Authentication and Cloud Firestore
  implementation 'com.google.firebase:firebase-auth-ktx'
  implementation 'com.google.firebase:firebase-firestore-ktx'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 수준의 build.gradle 파일에 위와 같은 내용을 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 프로젝트 상단에 표시된 &amp;lsquo;Sync now&amp;rsquo;라는 버튼을 눌러주면 연동은 완료된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/zoown13/222094002924&quot;&gt;https://m.blog.naver.com/zoown13/222094002924&lt;/a&gt;&lt;a href=&quot;https://cionman.tistory.com/72&quot;&gt;https://cionman.tistory.com/72&lt;/a&gt;&lt;a href=&quot;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&quot;&gt;https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;amp;blogId=traeumen927&amp;amp;logNo=221493556497&lt;/a&gt;&lt;a href=&quot;https://lasbe.tistory.com/19&quot;&gt;https://lasbe.tistory.com/19&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>Firebase</category>
      <category>realtime database</category>
      <category>안드로이드</category>
      <category>채팅 앱</category>
      <category>파이어베이스</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/56</guid>
      <comments>https://behappyaftercoding.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 31 Mar 2022 14:20:08 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 안드로이드 스튜디오 디자인 뷰가 보이지 않는 경우</title>
      <link>https://behappyaftercoding.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xj5cW/btrxUYQm3E8/17X6BZtb0K0RHQZyxuPN60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xj5cW/btrxUYQm3E8/17X6BZtb0K0RHQZyxuPN60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xj5cW/btrxUYQm3E8/17X6BZtb0K0RHQZyxuPN60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxj5cW%2FbtrxUYQm3E8%2F17X6BZtb0K0RHQZyxuPN60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;781&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;781&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 스튜디오를 사용하던 중 UI XML의 디자인 뷰가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Clean Project를 수행해주었더니,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IKEDz/btrxWCy1bCx/0rNFh8zIrDKFxDaKBNw5uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IKEDz/btrxWCy1bCx/0rNFh8zIrDKFxDaKBNw5uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IKEDz/btrxWCy1bCx/0rNFh8zIrDKFxDaKBNw5uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIKEDz%2FbtrxWCy1bCx%2F0rNFh8zIrDKFxDaKBNw5uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;761&quot; height=&quot;577&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 원상복구 되었다.&lt;/p&gt;</description>
      <category>Andorid</category>
      <category>IDE 오류</category>
      <category>Render problem</category>
      <category>디자인 뷰 안보임</category>
      <category>안드로이드 스튜디오 오류</category>
      <author>CoBool</author>
      <guid isPermaLink="true">https://behappyaftercoding.tistory.com/55</guid>
      <comments>https://behappyaftercoding.tistory.com/55#entry55comment</comments>
      <pubDate>Wed, 30 Mar 2022 00:00:57 +0900</pubDate>
    </item>
  </channel>
</rss>