Firebase Realtime Database 를 이용해서 Android Kotlin 으로 간단한 채팅 앱을 만들어보았어요!
먼저 Firebase Realtime Database 는 이전 포스팅을 참고해주세요~
https://eunoia3jy.tistory.com/174
🎨 시나리오
◽ 초기 진입 시 모든 채팅 메세지 목록이 같은 색상으로 표시됩니다.
◽ 채팅 메세지를 보내기 전, 로그인 다이얼로그를 통해 로그인합니다.
◽ 로그인 후 나의 메세지는 프로필/이름/메세지/시간이 우측, 색상이 푸른계열로 나오고,
다른 사람의 메세지는 프로필/이름/메세지/시간이 좌측, 색상이 붉은계열로 분리되어 표시됩니다.
◽ 메세지 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
데이터베이스 URL 과 Path들, DatabaseReference 등 Realtime 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
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 의 UnInitialize, Loading, Success, Error 에 따라 처리해줍니다.
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>
결과 화면
감사합니다 ^-^
'📱 안드로이드 Android ~ Kotlin' 카테고리의 다른 글
[Android/kotlin] 구글 Firebase Remote Config 사용하기 (0) | 2022.09.07 |
---|---|
[Android/kotlin] 구글 Firebase In-App Messaging 사용하기 (0) | 2022.07.28 |
[Android/kotlin] 구글 Firebase Realtime Database 사용하기 (0) | 2022.06.10 |
[Android/kotlin] 구글 Firebase Crashlytics 사용하기 (0) | 2022.06.09 |
[Android/Kotiln] Jetpack Navigation 사용하기 (0) | 2022.05.26 |