📱 안드로이드 Android ~ Kotlin

[Android/kotlin] 구글 Firebase Realtime Database 사용한 채팅 앱 만들기

핑크빛연어 2022. 6. 24. 10:52

 

Firebase Realtime Database 를 이용해서 Android Kotlin 으로 간단한 채팅 앱을 만들어보았어요!

먼저 Firebase Realtime Database 는 이전 포스팅을 참고해주세요~

https://eunoia3jy.tistory.com/174

 

[Android/kotlin] 구글 Firebase Realtime Database 사용하기

Realtime Database Firebase의 Realtime Database는 NoSQL기반 cloud-hosted database입니다. 실시간으로 모든 클라이언트에서 데이터가 동기화 되어 사용할 수 있습니다. https://firebase.google...

eunoia3jy.tistory.com

 

 

🎨 시나리오

 ◽ 초기 진입 시 모든 채팅 메세지 목록이 같은 색상으로 표시됩니다.

 ◽ 채팅 메세지를 보내기 전, 로그인 다이얼로그를 통해 로그인합니다.

 ◽ 로그인 후 나의 메세지는 프로필/이름/메세지/시간이 우측, 색상이 푸른계열로 나오고,

    다른 사람의 메세지는 프로필/이름/메세지/시간이 좌측, 색상이 붉은계열로 분리되어 표시됩니다.

 ◽ 메세지 send 시 로그인이 되어있는지, 메세지가 입력되었는지 체크 후 전송합니다.

 ◽ 로그아웃 후에는 초기 화면처럼 모든 채팅 메세지가 다른 사람의 메세지 형태로 표시됩니다.

 ◽ 데이터가 저장되는 데이터베이스의 path 는 chat/message 로 구성하고,

    데이터는 content(메세지 내용), name(사용자이름), timestamp(전송시간), uid 를 응답받도록 하였습니다.

 

 

🚨 작성한 파일 목록 입니다.

1. build.gradle(:app)
2. AndroidManifest.xml
3. data > entity - ChatMessageEntity

4. util > CommonUtil
5. presentation 

    5-1) ChatMainState
    5-2) ChatMainViewModel
    5-3) ChatMainActivity

    5-4) ChatMainAdapter

6. resource

    6-1) res > layout > activity_chat_main.xml

    6-2) res > menu > menu_chat.xml

    6-3) res > layout > dialog_login.xml

    6-4) res > layout > item_chat_list.xml

 

 

💡 프로젝트 구조

 

 

 

1. build.gradle(:app)

viewBinding 을 사용하기 위해 viewBinding 의 enabled 를 true 로 설정합니다.
실시간 데이터베이스 SDK 추가를 위해서 dependencies 안에 Firebase database 에 대한 의존성을 추가해주고,
Coroutine, glide 에 대한 의존성도 추가해줍니다.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'com.google.gms.google-services'
}

android {
    ...
    buildFeatures {
        viewBinding = true
    }
    ...
}

dependencies {
    ...
    //Firebase    
    implementation platform('com.google.firebase:firebase-bom:30.0.1')
    implementation 'com.google.firebase:firebase-database-ktx'

    //Coroutine
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3")
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.3"

    //glide
    implementation 'com.github.bumptech.glide:glide:4.9.0'
    ...
}

 

 

2. AndroidManifest.xml

AndroidManifest 에서 인터넷 권한 을 추가해야 합니다.
<uses-permission android:name="android.permission.INTERNET"/>

저는 MainActivity 대신 처음 띄워주는 액티비티를 ChatMainActivity 로 설정하여, <intent-filter> 를 추가해주었어요.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.eun.myappkotlin02">

    <uses-permission android:name="android.permission.INTERNET" />
  
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyAppKotlin02"
        android:networkSecurityConfig="@xml/network_security_config" >
        <activity
            android:name=".realtime.chat.presentation.ChatMainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

 

3. data > entity - ChatMessageEntity

Realtime Database 의 데이터 규칙을 사용하기 쉽도록 응답받는 데이터에 맞추어 VO 를 구현한
채팅 메세지 정보 Entity 입니다.

package com.eun.myappkotlin02.realtime.chat.data.entity

data class ChatMessageEntity(
    var name: String?,
    var uid: String?,
    var content: String?,
    var timestamp: String?
)

 

 

4. util - CommonUtil

데이터베이스 URLPath들, DatabaseReferenceRealtime Database 의 정보를 
공통으로 사용하기 위해 CommonUtil object 를 만들어 선언하였습니다.

userName 도 activity 와 adapter 에서 모두 사용가능하도록 CommonUtil 에 추가해주었습니다.

package com.eun.myappkotlin02.realtime.chat.util

import com.google.firebase.database.DatabaseReference
import java.text.SimpleDateFormat

object CommonUtil {

    const val CHAT_DB_URL = "https://....firebasedatabase.app"
    const val CHAT_PATH= "chat"
    const val CHAT_PATH_CHILD = "messages"
    lateinit var CHAT_REF: DatabaseReference

    var userName = ""  // 로그인 사용자 이름

    fun getTime(timeStamp: Long): String {
        val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")  // 시간이 24시간으로
//        val format = SimpleDateFormat("yyyy-MM-dd hh:mm:ss")  // 시간이 12시간으로
        return format.format(timeStamp)
    }

}

 

 

5. presentation

화면 조작, 사용자의 이벤트를 처리하기 위한 레이어, presentation 입니다.

UI(Activity, Fragment ..), Presenter, ViewModel 를 포함합니다.

 

5-1. ChatMainState

viewModel 의 chatMainLiveData 의 상태들을 정의한 클래스입니다.
이 상태값에 따라 액티비티에서 관찰합니다.

package com.eun.myappkotlin02.realtime.chat.presentation

import com.eun.myappkotlin02.realtime.chat.data.entity.ChatMessageEntity

sealed class ChatMainState {

    object UnInitialized: ChatMainState()

    object Loading: ChatMainState()

    data class Success(
        val chatList: List<ChatMessageEntity>
    ): ChatMainState()

    object Error: ChatMainState()
}

 

 

5-2. ChatMainViewModel

채팅 메세지를 읽어오는 fetchReadData(),
채팅 메세지를 쓰는 fetchWriteData() 를 구현하였습니다.
Repository 를 따로 사용하지 않고 viewModel 에 모두 구현해주었어요.

https://firebase.google.com/docs/database/android/start

 

Android에서 설치 및 설정  |  Firebase Documentation

Check out what’s new from Firebase at Google I/O 2022. Learn more 의견 보내기 Android에서 설치 및 설정 Firebase에 앱 연결 아직 추가하지 않았다면 Android 프로젝트에 Firebase를 추가합니다. 데이터베이스 만들기 F

firebase.google.com

package com.eun.myappkotlin02.realtime.chat.presentation

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.eun.myappkotlin02.LogUtil
import com.eun.myappkotlin02.realtime.chat.data.entity.ChatMessageEntity
import com.eun.myappkotlin02.realtime.chat.util.CommonUtil
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.*
import java.lang.Exception
import java.util.*

class ChatMainViewModel: ViewModel() {

    companion object {
        const val TAG = "ChatMainViewModel"
    }

//    private val chatRepository = ChatRepository(Dispatchers.IO)

    private var _chatMainLiveData = MutableLiveData<ChatMainState>(ChatMainState.UnInitialized)
    val chatMainLiveData: LiveData<ChatMainState> = _chatMainLiveData


    /*
    * 채팅 메세지 읽어오기
    * */
    fun fetchReadData(): Job = viewModelScope.launch {
        try {
            _chatMainLiveData.postValue(ChatMainState.Loading)
            var response: MutableList<ChatMessageEntity> = mutableListOf()
            val database = Firebase.database(CommonUtil.CHAT_DB_URL)
            CommonUtil.CHAT_REF = database.getReference(CommonUtil.CHAT_PATH).child(CommonUtil.CHAT_PATH_CHILD)
            CommonUtil.CHAT_REF.addValueEventListener(object : ValueEventListener {
                override fun onDataChange(dataSnapshot: DataSnapshot) {
                    // This method is called once with the initial value and again
                    // whenever data at this location is updated.
                    for ((index, snapshot) in dataSnapshot.children.withIndex()) {
                        val chatItem = snapshot.value as HashMap<String, String>
                        var msgItem = ChatMessageEntity(chatItem["name"], chatItem["uid"], chatItem["content"], chatItem["timestamp"])
                        response.add(index, msgItem)
                    }
                    LogUtil.d(TAG, "readChatAllMsg() - response :  $response")
                    _chatMainLiveData.postValue(ChatMainState.Success(response))
                }
                override fun onCancelled(error: DatabaseError) {
                    // Failed to read value
                    _chatMainLiveData.postValue(ChatMainState.Error)
                    LogUtil.d(TAG, "readChatAllMsg() - Failed to read value. : ${error.toException()} ")
                }
            })
        } catch (e: Exception) {
            _chatMainLiveData.postValue(ChatMainState.Error)
        } finally {
        }
    }


    /*
    * 채팅 메세지 쓰기
    * */
    fun fetchWriteData(mName: String, mContent: String): Job = viewModelScope.launch {
        // [START write_message]
        // Write a message to the database

        val database = Firebase.database(CommonUtil.CHAT_DB_URL)
        CommonUtil.CHAT_REF = database.getReference(CommonUtil.CHAT_PATH).child(CommonUtil.CHAT_PATH_CHILD)

        val uuid = UUID.randomUUID().toString()
        var chatMessageEntity = ChatMessageEntity(mName, uuid, mContent, CommonUtil.getTime(System.currentTimeMillis()))

        CommonUtil.CHAT_REF.push().setValue(chatMessageEntity)   // 데이터가 계속 쌓이는 방식

        // [END write_message]

        LogUtil.d(TAG, "myRef :: ${CommonUtil.CHAT_REF}")
        fetchReadData()
    }

}

 

 

5-3. ChatMainActivity

메인 액티비티 입니다.

 

초기에는 userName 이 비어있기 때문에 메세지 전송 시 로그인이 필요하다는 토스트가 표시되고, 채팅 리스트도 한가지 색상으로 표시됩니다.

우측 상단 toolbar 메뉴의 login 메뉴 클릭 시 userName 을 입력하여 로그인을 진행하는 Dialog 가 표시되고, 

로그인 시 내가 작성한 메세지는 푸른 계열의 색상으로, 다른 사람이 작성한 메세지는 붉은 계열의 색상으로 표시됩니다.

내가 작성한 메세지는 프로필이 우측에 표시되고, 다른 사람이 작성한 메세지는 프로필이 좌측에 표시되도록 구현하였습니다.

 

initView() 의 viewModel.fetchReadData() 를 통해 데이터베이스의 채팅 메세지를 읽어옵니다.

btnSend 버튼 클릭 시에는 validation 체크 후 viewModel.fetchWriteData() 를 통해

데이터베이스에 채팅 메세지를 작성합니다.

 

Toolbar 의 메뉴와 관련한 메소드들입니다. menu_chat.xml 을 inflate 합니다.
onCreateOptionsMenu() : Toolbar 의 OptionsMenu 가 최초로 생상될 때 호출
onPrepareOptionsMenu() : Toolbar 의 OptionsMenu 가 화면에 보여질 때마다 호출
onOptionsItemSelected() : Toolbar 의 OptionsMenu 아이템이 클릭될 때 호출

 

observeData() 는 viewModel.chatMainLiveData 를 관찰하여 상태가 변경 시에 
ChatMainState 의 UnInitializeLoadingSuccessError 에 따라 처리해줍니다.
ChatMainState.Success 시에는 handleSuccessState() 메소드를 호출시켜
chatListAdapter 에 chatList 를 세팅합니다.
그리고 edittext 의 키보드내리기, 스크롤 맨 아래로, edittext clear 작업을 진행합니다.

 

showLoginDialog() Toolbar 의 login 메뉴를 클릭 시 
userName 을 입력하여 로그인을 하는 Dialog 를 보여주는 메소드입니다.

package com.eun.myappkotlin02.realtime.chat.presentation

import android.app.AlertDialog
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.eun.myappkotlin02.LogUtil
import com.eun.myappkotlin02.R
import com.eun.myappkotlin02.databinding.ActivityChatMainBinding
import com.eun.myappkotlin02.databinding.DialogLoginBinding
import com.eun.myappkotlin02.realtime.chat.util.CommonUtil.userName
import kotlinx.coroutines.Job


class ChatMainActivity: AppCompatActivity() {
    companion object {
        const val TAG = "ChatMainActivity"
    }

    private var viewModel: ChatMainViewModel = ChatMainViewModel()
    lateinit var binding: ActivityChatMainBinding
    private lateinit var fetchJob: Job

    lateinit var chatListAdapter: ChatListAdapter

    var boolLogin: Boolean = false  // 로그인 여부

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

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

        initView()
        observeData()
    }


    private fun initView() {

        // Toolbar 사용 설정
        setSupportActionBar(binding.tbTop)
        binding.tbTop.title = ""

        // Toolbar 의 navigation icon 클릭 이벤트 시 뒤로가기
        binding.tbTop.setNavigationOnClickListener {
            onBackPressed()
        }

        // 로그인 여부 설정
        boolLogin = !userName.isNullOrEmpty()
        if(boolLogin) {
            binding.tvToolbarUser.text = "($userName)"
        }

        // RecyclerView 설정
        chatListAdapter = ChatListAdapter()
        binding.recyclerView.run {
            layoutManager = LinearLayoutManager(this@ChatMainActivity)
            adapter = chatListAdapter
        }
        binding.refreshLayout.isEnabled = false

        // 메세지 보내기
        binding.btnSend.setOnClickListener {
            if(userName.isNullOrEmpty()) {
                Toast.makeText(this, "로그인을 진행하세요.", Toast.LENGTH_LONG).show()
            }
            if(binding.etInput.text.isNullOrEmpty()) {
                Toast.makeText(this, "메세지를 입력하세요.", Toast.LENGTH_LONG).show()
            } else {
                viewModel.fetchWriteData(userName, binding.etInput.text.toString())
            }
        }

        // 데이터 통신하기
        fetchJob = viewModel.fetchReadData()

    }


    /*
    * Toolbar 의 OptionsMenu 가 최초로 생상될 때 호출
    * Toolbar 에 menu_chat.xml 을 inflate 함
    */
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
//        return super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.menu_chat, menu)
        return true
    }

    /*
    * Toolbar 의 OptionsMenu 가 화면에 보여질 때마다 호출
    * */
    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
        // item(0):login, item(1):logout
        if(boolLogin) {  //로그인 O -> 로그인 안보이게, 로그아웃 보이게

            menu?.getItem(0)?.isVisible = false  //login
            menu?.getItem(1)?.isVisible = true  //logout
        } else {  //로그인 X -> 로그인 보이게, 로그아웃 안보이게
            menu?.getItem(0)?.isVisible = true
            menu?.getItem(1)?.isVisible = false
        }
        return super.onPrepareOptionsMenu(menu)
    }

    /*
    * Toolbar 의 OptionsMenu 아이템이 클릭될 때 호출
    * */
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when(item.itemId) {
            R.id.menu_login -> {
                showLoginDialog()
                true
            }
            R.id.menu_logout -> {
                userName = ""
                initView()
                true
            }
            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }


  /*
  * viewModel.chatMainLiveData 관찰하는 메소드
  * */
    private fun observeData() = viewModel.chatMainLiveData.observe(this) {
        viewModel.chatMainLiveData.observe(this) {
            LogUtil.d(TAG, "observeData() - it : $it ")
            when(it) {
                is ChatMainState.UnInitialized -> initView()
                is ChatMainState.Loading -> {
                    binding.progressBar.visibility = View.VISIBLE  // 프로그래스 바 보여주기
                }
                is ChatMainState.Success -> {
                    handleSuccessState(it)
                }
                is ChatMainState.Error -> {
                    handleErrorState()
                }
                else -> {}
            }
        }
    }

    private fun handleSuccessState(state: ChatMainState.Success) {
        LogUtil.d(TAG, "handleSuccessState() - state : $state ")
        binding.progressBar.visibility = View.GONE
        if(state.chatList.isEmpty()) {
            binding.tvEmptyList.visibility = View.VISIBLE
        } else {
            binding.tvEmptyList.visibility = View.GONE
            chatListAdapter.setChatItems(state.chatList)
        }
        // 키보드 내리기
        val inputManager: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
        inputManager.hideSoftInputFromWindow(binding.etInput.windowToken, 0)
        binding.recyclerView.scrollToPosition(chatListAdapter.itemCount -1)  //스크롤 맨 아래로
        binding.etInput.text.clear()
    }
    private fun handleErrorState() {
        binding.tvEmptyList.visibility = View.VISIBLE
        Toast.makeText(this, "에러가 발생했습니다.", Toast.LENGTH_SHORT).show()
    }


    /*
    * 로그인하는 Dialog 띄우기
    * */
    private fun showLoginDialog() {

        var dialogViewBinding = DialogLoginBinding.inflate(layoutInflater)
        var builder = AlertDialog.Builder(this)
        var dialog = builder.setView(dialogViewBinding.root).create()

        dialogViewBinding.btnConfirm.setOnClickListener {
            userName = dialogViewBinding.etName.text.toString()
            initView()
            dialog.dismiss()
        }
        dialogViewBinding.btnCancel.setOnClickListener {
            dialog.dismiss()
        }

        dialog.show()
    }

}

 

 

5-4. ChatMainAdapter

RecycleView.Adapter 를 상속받고 ChatListAdapter.ListViewHolder 를 뷰홀더로 갖는 ChatListAdapter 클래스를 만듭니다.
채팅 메세지의 리스트를 RecycleView 로 표시하기 위한 RecycleViewAdapter 입니다.

ListViewHolder 이너클래스의 bind() 에서 userName 값과 item 의 name 을 비교하여 
나의 메세지와 다른 사람의 메세지를 구별하여 구현하였습니다.
프로필ImageView 는 현재 데이터가 없으므로 하나의 이미지로 통일하였고 둥글게 표현하기 위해
Glide 의 circleCrop() 를 사용하엿습니다.

setChatItems() 는 activity 에서 MainChatState.Success 인 경우 호출되어 데이터변경을 알려주는 메소드입니다.

package com.eun.myappkotlin02.realtime.chat.presentation

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.eun.myappkotlin02.R
import com.eun.myappkotlin02.databinding.ItemChatListBinding
import com.eun.myappkotlin02.realtime.chat.data.entity.ChatMessageEntity
import com.eun.myappkotlin02.realtime.chat.util.CommonUtil.userName

class ChatListAdapter: RecyclerView.Adapter<ChatListAdapter.ListViewHolder>() {
    companion object {
        const val TAG = "ChatListAdapter"
    }

    var chatItems: List<ChatMessageEntity> = listOf()


    inner class ListViewHolder(private val viewBinding: ItemChatListBinding): RecyclerView.ViewHolder(viewBinding.root) {
        fun bind(item: ChatMessageEntity) {

            if(item.name.equals(userName)) {  // 나의 msg
                viewBinding.llChatCard.setBackgroundResource(R.color.teal_100)  // 카드뷰 배경색
                viewBinding.llChatLeft.visibility = View.GONE
                viewBinding.llChatRight.visibility = View.VISIBLE
                viewBinding.clChatLeft.visibility = View.GONE
                viewBinding.clChatRight.visibility = View.VISIBLE
                Glide.with(viewBinding.root).load(R.drawable.ic_launcher_bear).circleCrop().into(viewBinding.ivProfileRight)  // imageView 둥글게
                viewBinding.tvUserRight.text = item.name
                viewBinding.tvMsgRight.text = item.content
                viewBinding.tvTimeRight.text = item.timestamp
            } else {  // 다른사람의 msg
                viewBinding.llChatCard.setBackgroundResource(R.color.deep_orange_100)  // 카드뷰 배경색
                viewBinding.llChatLeft.visibility = View.VISIBLE
                viewBinding.llChatRight.visibility = View.GONE
                viewBinding.clChatLeft.visibility = View.VISIBLE
                viewBinding.clChatRight.visibility = View.GONE
                Glide.with(viewBinding.root).load(R.drawable.ic_launcher_bear).circleCrop().into(viewBinding.ivProfile)  // imageView 둥글게
                viewBinding.tvUser.text = item.name
                viewBinding.tvMsg.text = item.content
                viewBinding.tvTime.text = item.timestamp
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
        return ListViewHolder(ItemChatListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

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

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

    fun setChatItems(list: Any) {
        this.chatItems = list as List<ChatMessageEntity>
        notifyDataSetChanged()
    }

}

 

 

6. resource

res 폴더에 있는 리소스 영역입니다.

 

6-1. res > layout > activity_chat_main.xml

ChatMainActivity 에 대한 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:paddingBottom="20dp" >

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/tb_top"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/amber_200"
        app:navigationIcon="?attr/homeAsUpIndicator"
        android:elevation="10dp"
        app:buttonGravity="center_vertical"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" >
        <TextView
            android:id="@+id/tv_toolbar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="My Chat"
            android:textSize="20sp"
            android:textColor="@color/black"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/tv_toolbar_user"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            tools:text="(userName)"
            android:textSize="20sp"
            android:textColor="@color/black"
            android:textStyle="bold" />
    </androidx.appcompat.widget.Toolbar>

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refresh_layout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingHorizontal="5dp"
        android:paddingVertical="10dp"
        android:background="@color/grey_100"
        app:layout_constraintTop_toBottomOf="@id/tb_top"
        app:layout_constraintBottom_toTopOf="@id/et_input"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" >
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            tools:listitem="@layout/item_chat_list"
            app:layout_constraintTop_toBottomOf="@id/tb_top"
            app:layout_constraintBottom_toTopOf="@id/et_input"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <TextView
        android:id="@+id/tv_empty_list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:text="대화 목록이 없습니다."
        app:layout_constraintTop_toBottomOf="@id/tb_top"
        app:layout_constraintBottom_toTopOf="@id/et_input"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <EditText
        android:id="@+id/et_input"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="20dp"
        app:layout_constraintTop_toBottomOf="@id/refresh_layout"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_send" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginRight="20dp"
        android:background="@drawable/bg_custom_send_button"
        android:text="send"
        android:textColor="@color/white"
        app:layout_constraintTop_toBottomOf="@id/refresh_layout"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/et_input"
        app:layout_constraintEnd_toEndOf="parent" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

6-2. res > menu > menu_chat.xml

ToolBar 는 Actionbar 를 대체하는 View 의 일종으로,

themes.xml 의 style 태그 parent 를 NoActionBar 로 변경 후에 사용해야 합니다.

ToolBar 에 메뉴를 추가하기 위해서는 /res 경로에 menu 라는 directory 를 만들어서

그 안에 xml 파일을 생성하여 사용해야  합니다. menu_chat.xml 는 Toolbar 에 대한 메뉴 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <group>
        <item
            android:id="@+id/menu_login"
            android:icon="@drawable/ic_launcher_foreground"
            android:title="login" />
        <item
            android:id="@+id/menu_logout"
            android:icon="@drawable/ic_launcher_foreground"
            android:title="logout" />
    </group>

</menu>

 

 

6-3. res > layout > dialog_login.xml

 액티비티의 showLoginDialog() 에서 userName 을 입력하여 로그인을 진행하는 Dialog 에 대한 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:orientation="vertical"
            android:weightSum="100" >

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="20"
                android:background="@color/amber_200" >
                <TextView
                    android:id="@+id/dialog_title"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:text="@string/app_name"
                    android:textColor="@color/black"
                    android:textSize="16dp" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="55"
                android:gravity="center" >
                <EditText
                    android:id="@+id/et_name"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:lineSpacingExtra="5dp"
                    android:padding="20dp"
                    android:hint="userName 을 입력하세요." />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="25"
                android:background="@color/white"
                android:gravity="center"
                android:orientation="horizontal"
                android:weightSum="100" >
                <LinearLayout
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginRight="7dp"
                    android:layout_weight="40"
                    android:gravity="top"
                    android:orientation="vertical"
                    android:weightSum="100" >
                    <androidx.appcompat.widget.AppCompatButton
                        android:id="@+id/btn_confirm"
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="55"
                        android:background="@color/green_200"
                        android:gravity="center"
                        android:text="확 인"
                        android:textColor="@color/white"
                        android:textSize="14dp" />
                </LinearLayout>
                <LinearLayout
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="7dp"
                    android:layout_weight="40"
                    android:gravity="top"
                    android:orientation="vertical"
                    android:weightSum="100" >
                    <androidx.appcompat.widget.AppCompatButton
                        android:id="@+id/btn_cancel"
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="55"
                        android:background="@color/blue_grey_200"
                        android:gravity="center"
                        android:text="취 소"
                        android:textColor="@color/white"
                        android:textSize="14dp" />
                </LinearLayout>
            </LinearLayout>

        </LinearLayout>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

6-4. res > layout > item_chat_list.xml

ChatMainAdapter 에 대한 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        app:cardCornerRadius="20dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <LinearLayout
            android:id="@+id/ll_chat_card"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingHorizontal="0dp"
            android:orientation="horizontal"
            android:weightSum="100"
            android:minHeight="60dp"
            android:background="@color/deep_orange_100" >

<!--            다른사람의 msg 인 경우 -->
            <LinearLayout
                android:id="@+id/ll_chat_left"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="15"
                android:paddingStart="7dp"
                android:gravity="center"
                android:layout_gravity="center"
                android:orientation="vertical"
                android:weightSum="100"
                android:visibility="visible">
                <ImageView
                    android:id="@+id/iv_profile"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:layout_weight="50"
                    android:src="@drawable/ic_launcher_bear" />
            </LinearLayout>

            <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/cl_chat_left"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="85"
                android:paddingVertical="7dp"
                android:paddingHorizontal="7dp"
                android:visibility="visible">

                <TextView
                    android:id="@+id/tv_user"
                    android:layout_width="wrap_content"
                    android:layout_height="20dp"
                    android:text="홍길동"
                    android:textSize="15sp"
                    android:textColor="@color/brown_500"
                    app:layout_constraintBottom_toTopOf="@+id/tv_msg"
                    app:layout_constraintStart_toStartOf="parent" />

                <TextView
                    android:id="@+id/tv_msg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:minHeight="30dp"
                    android:text="홍길동의 메세지\n홍길동의 메세지\n홍길동의 메세지\n홍길동의 메세지"
                    android:textSize="18dp"
                    android:textColor="@color/black"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/tv_user" />

                <TextView
                    android:id="@+id/tv_time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="2022-06-23 14:05:30"
                    android:textSize="10sp"
                    android:textColor="@color/grey_500"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/tv_msg"
                    app:layout_constraintVertical_chainStyle="packed"
                    app:layout_constraintHorizontal_chainStyle="packed"
                    app:layout_constraintHorizontal_bias="0" />

            </androidx.constraintlayout.widget.ConstraintLayout>

<!--            나의 msg 인 경우 -->
            <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/cl_chat_right"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="85"
                android:paddingVertical="7dp"
                android:paddingHorizontal="7dp"
                android:visibility="gone">

                <TextView
                    android:id="@+id/tv_user_right"
                    android:layout_width="wrap_content"
                    android:layout_height="20dp"
                    android:text="홍길동"
                    android:textSize="15sp"
                    android:textColor="@color/brown_500"
                    app:layout_constraintBottom_toTopOf="@+id/tv_msg_right"
                    app:layout_constraintEnd_toEndOf="parent" />

                <TextView
                    android:id="@+id/tv_msg_right"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:minHeight="30dp"
                    android:text="홍길동의 메세지\n홍길동의 메세지\n홍길동의 메세지\n홍길동의 메세지"
                    android:textSize="18dp"
                    android:textColor="@color/black"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/tv_user_right" />

                <TextView
                    android:id="@+id/tv_time_right"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="2022-06-23 14:05:30"
                    android:textSize="10sp"
                    android:textColor="@color/grey_500"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/tv_msg_right"
                    app:layout_constraintVertical_chainStyle="packed"
                    app:layout_constraintHorizontal_chainStyle="packed"
                    app:layout_constraintHorizontal_bias="0" />

            </androidx.constraintlayout.widget.ConstraintLayout>

            <LinearLayout
                android:id="@+id/ll_chat_right"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="15"
                android:paddingEnd="7dp"
                android:gravity="center"
                android:layout_gravity="center"
                android:orientation="vertical"
                android:weightSum="100"
                android:visibility="gone">
                <ImageView
                    android:id="@+id/iv_profile_right"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:layout_weight="50"
                    android:src="@drawable/ic_launcher_bear" />
            </LinearLayout>

        </LinearLayout>

    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

결과 화면

 

 

감사합니다 ^-^

 

728x90
반응형