
계절의 여왕이라는 오월
길가에 장미가 싱그럽다. 아침 햇쌀을 맞는 장미를 보며, 나의 시간들도 싱그럽기를 기원한다.
빌코리아의 홈페이지 입니다.

이 앱은 말 그대로 여러 나라 사람들이 자기 언어를 이용해서 채팅을 할 수 있는 기능을 지원합니다.
This app literally supports the ability to chat with people from different countries using their own language.
사용방법은 간단합니다.
How to use is simple.
1. 사용자 확인을 위해 구글 로그인이 필요합니다.
1. Google login is required for user verification.
2. 환경 설정에서 내가 사용하는 언어를 설정할 수 있습니다.
2. You can set the language I use in the preferences.
3. 사용자가 선택한 언어가 채팅 창에 입력되면, 내가 선택한 언어로 변환되어 표시됩니다.
3. When the language selected by the user is entered in the chat window, it is converted into the language of your choice and displayed.
4. 채팅 방에 배너 광고가 게시됩니다.
4. A admob banner advertisement is posted in the chat room.
5. 로그인을 위한 이메일 정보 이외에는 저장되는 정보는 없습니다.
5. No information is saved other than e-mail information for login.
6. 지원되는 언어는 다음과 같습니다.
6. The supported languages are:
한국어,
English,
日本語,
中文,
tiếng Việt,
bahasa Indonesia,
اللغة العربية,
বাংলা ভাষা,
Deutsch,
Español,
Français,
हिन्दी,
Italiano,
Malaysia,
Nederlands,
Português,
Русский,
ภาษาไทย,
Türkçe
이 앱에서 번역 기능은 기존 KAKAO Translate API가 사용이 중지 되어 Google ML Translate 을 이용 하고 있습니다. (2024.01.07 ~)
The translation function in this app uses Google ML Translate because the existing KAKAO Translate API has been discontinued. (2024.01.07 ~)
로그인은 그냥 구글 계정을 통한 로그인만 지원합니다. 안드로이드 폰에는 다들 하나씩의 계정은 있으니, 자동으로 선택하는 창이 나옵니다.
Login only supports login through Google account. Since everyone has one account on an Android phone, an automatic selection window appears.

로그인이 되고 나면 메인 화면으로 넘어갑니다. 사용하기 전에 설정을 선택해서 설정을 진행해야 합니다.
After logging in, you will be taken to the main screen. Before use, you need to select Settings to proceed with the settings.

설정해야 하는 것은
what you need to set
1) 사용자 정책에 대한 허락이 필요합니다. 이 앱은 사용자의 스팸성 글 게시, 욕설 등의 게시가 반복될 경우 타인으로 부터 불량 사용자로 신고 될 수 있습니다. 신고가 반복 되는 경우 해당 게시글은 관리자에 의해 삭제 될 수 있습니다. 또한 사용자가 제재를 받을 수 있습니다.
Requires permission from user policy. This app may be reported as a bad user by others if the user's posting of spam or profanity is repeated. If the report is repeated, the post may be deleted by the administrator. Users may also be subject to sanctions.
2) 다국어 번역 처리
Multilingual translation processing
자동으로 번역하기 : 상대가 입력한 언어가 내가 사용하는 언어와 다른 경우 번역한 내용이 채팅창에 추가됩니다.
Translate automatically: If the language entered by the other person is different from the language you are using, the translated content is added to the chat window.
번역한 언어 보지 않기 : 상대가 입력한 내용을 그대로 채팅창에 보입니다.
Do not view translated language: The text entered by the other person is displayed in the chat window.

3) 당신의 사용하는 언어는?
What language do you speak?
한국어,영어,일본어,중국어,베트남어,인도네시아어,아랍어,뱅갈어,독일어,스페인어,프랑스어,힌디어,이탈리아어,말레이시아어,네덜란드어,포르투갈어,러시아어,태국어,터키어
중에서 선택할 수 있습니다.
You can choose from
("한국어","English","日本語","中文","tiếng Việt","bahasa Indonesia","اللغة العربية","বাংলা ভাষা",
"Deutsch","Español","Français","हिन्दी","Italiano","Malaysia","Nederlands","Português","Русский","ภาษาไทย","Türkçe")
4) 알림을 수신할까요?
알림을 수신 허용을 해야만 이 앱에서의 모든 알림을 수신할 수 있습니다.
You must accept notifications to receive all notifications from this app.
5) 자동 로그인 설정
이 앱에 접근하기 위한 사용자의 로그인을 자동으로 할 수 있도록 설정할 수 있습니다.
You can set to automatically log in users to access this app.

추가 되는 기능은 공개적으로 인력을 모집할 수 있도록 기능을 만들어 보고 있습니다. 지금은 작업안내 기능으로 예시을 만들어 보았는데,
공개적인 인력 모집을 위한 창으로 사용할 수 있도록 만들어갈 예정입니다.
추가적인 기능 구현이 필요하시면 메일로 알려 주세요.
The added function is trying to make the function to be able to recruit people publicly. Now, I made an example with the work guide function,
We plan to make it usable as a window for public recruitment.
If you need to implement additional features, please let us know by e-mail.
help@billcorea.com
이 화면도 설정에 따라 자동으로 번역이 지원 됩니다.
This screen is also automatically translated according to the settings.

작업을 등록하여 구인 게시물을 등록할 수 있습니다. 긴략한 내용으로 등록을 지원하며, 이미지는 3개 까지만 등록할 수 있습니다. 등록된 구인게시물을 이용해서 구인시 참고할 수 있습니다.
You can register a job posting by registering a job. Registration is supported with brief content, and up to 3 images can be registered. You can refer to the registered job postings when hiring.

작업참여는 먼저 프로필 에서 개인정보을 공유 허가등록을 해야 합니다. 공유되는 정보는 프로필에서 입력하는 이미지, 특기, 성별, 나이, 로그인에서 받아온 이름이 공유 됩니다. 중요한 개인정보는 없으므로 자료 공유를 해 주어야 합니다.
To participate in work, you must first register for permission to share personal information in your profile. The shared information is shared with the image entered in the profile, skills, gender, age, and the name received from the login. Since there is no important personal information, you must share the data.


내가 작성한 게시물에 참여을 신청한 사람의 목록을 볼 수 있습니다. 또는 다른 사람이 작성한 게시글에 참여 신청을 할 수 있습니다. You can see a list of people who have subscribed to your posts. Alternatively, you can apply to participate in posts written by others.

내가 신청한 게시물이 허가된 경우 확인할 수 있으며, 나의 참여를 등록할 수 있습니다.
You can check if the post you have applied for is approved, and you can register your participation.

참여가 허가되고, 내가 참여을 확인한 경우만 채팅창으로 접근할 수 있습니다.
You can access the chat window only if participation is permitted and I confirm participation.

게시물을 작성한 사용자에게 나의 참여 신청에 대한 알림을 보낼 수 있습니다.
You can send notifications about your subscription to the person who wrote the post.

게시물을 작성한 사용자의 경우만 해당 게시물을 삭제할 수 있습니다.
Only the person who created the post can delete the post.

해당 게시물을 스팸으로 신고할 수 있습니다.
You can report this post as spam

설정 화면에서 나오고 나면 채팅 목록이 나오는 화면이 있습니다.
After exiting the settings screen, there is a screen that shows the chat list.
채팅 목록에 표시는 내용은 첫 번째 줄에는
방제목과 개설자의 사용 언어가 표시됩니다. (en는 영어)
The content displayed in the chat list is in the first line
The title and the language of the creator are displayed. (en is English)
다음 줄에는 방 이름과 개설자의 이름, 참여 중인 사용자의 수
그리고 삭제를 위한 버튼이 표시됩니다.
The next line contains the name of the room, the name of the creator, and the number of participating users.
And a button to delete is displayed.
화면 하단의 + 표시 버튼을 클릭하면 새로운 채팅방을 생성할 수 있습니다.
You can create a new chat room by clicking the + sign button at the bottom of the screen.

채팅창은 내가 입력한 내용은 오른쪽에 나오고, 상대가 입력한 내용은 왼쪽에 표시됩니다.
In the chat window, your input is displayed on the right, and your opponent's input is displayed on the left.
내가 입력한 내용은 그대로 표시가 되지만, 상대가 입력한 내용은 선택에 따라 나의 언어로 번역된 내용이 추가되어 표시됩니다.
(상대방의 언어로 표시는 기본사항)
The content you typed is displayed as is, but the content entered by the other person is displayed with the translated content added to your language according to your selection.
(Basic display in the other person's language)
아래 그림과 같이 나의 설정에 따라 자동으로 변억이 실행 되어 표시 됩니다. 괄호안에 상대방의 언어가 표시 됩니다.
As shown in the picture below, change memory is automatically executed and displayed according to my settings. The other person's language is displayed in parentheses.

그 외의 기능은 현재 진행형이기 때문에 다른 기능은 아직 구현되지 않았습니다. 그리고 playstore에도 현재는 등록을 진행중이기 때문에 관련 링크는 아직 없습니다. playstore 에 등록되고 난 이후에 update을 할 예정입니다.
Other features have not yet been implemented as they are currently in progress. Also, there are no related links yet because registration is in progress on the playstore. We plan to update after being registered on playstore.
아래 링크와 같이 playstore 에 등록이 되어 찾아 볼 수 있습니다.
As shown in the link below, it is registered on the playstore and can be found.
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416
일자리 구하기 - 구인구직 노가다 자동 번역 지원 - Google Play 앱
한국에서 일자리를 구하는 사람들에게 한국어가 익숙하지 않아도 찾을 수 있도록 지원할 예정 입니다.여러 나라 사람들과 채팅을 할 수 있습니다.
play.google.com
2025.02.11 변경된 새버전의 예시 영상 입니다.
https://andresand.medium.com/add-admob-ad-banner-using-android-compose-9ba78c8f1591
Add AdMob Ad banner using Android Compose
Tutorial shows how to display Google AdMob banner ads using Android Compose. Currently there is no official doc about AdMob and Android…
andresand.medium.com
jetpack compose 을 구현하면서 쉽게 광고판 달아보기 예제를 펌 했습니다. 읽어 보시면 도움이 될 것 같아요.
그래도 혹시나 해서 제가 만든 소스 의 일부를 공유해 봅니다.
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.KeyEvent.*
import android.view.View
import android.view.View.inflate
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.core.content.ContextCompat
import com.billcoreatech.multichat416.databean.*
import com.billcoreatech.multichat416.databinding.ActivityChatRoomBinding
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.text.SimpleDateFormat
import java.util.*
class ChatRoomActivity : ComponentActivity() {
var TAG = "ChatRoomActivity"
lateinit var displayName:String
lateinit var auth: FirebaseAuth
lateinit var sp: SharedPreferences
lateinit var sdf:SimpleDateFormat
private val database = Firebase.database
private val chatroomViewModel by viewModels<ListViewModel>()
private val chatMessages = database.getReference("ChatMessage")
lateinit var df:SimpleDateFormat
lateinit var chatId:String
lateinit var startDt:String
lateinit var adapter:ChatAdapter
var chatMesgItems = ArrayList<ChatMessage>()
private set
lateinit var binding:ActivityChatRoomBinding
lateinit var retrofitApi:RetrofitApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retrofitApi = RetrofitApi.create()
auth = Firebase.auth
sp = getSharedPreferences("MultiChat", MODE_PRIVATE)
sdf = SimpleDateFormat("yyyyMMddHHmmss")
df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
chatId = intent.getStringExtra("chatId") as String
startDt = intent.getSerializableExtra("startDt") as String
Log.e(TAG, "${startDt}")
displayName = auth.currentUser?.displayName.toString()
chatMesgItems.clear()
adapter = ChatAdapter(chatMesgItems, displayName)
binding = ActivityChatRoomBinding.inflate(layoutInflater)
setContent {
val isDarkTheme = remember { mutableStateOf(false) }
if(isDarkTheme.value){
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlack)
}else{
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlue)
}
MultiChat416Theme(darkTheme = isDarkTheme.value) {
Scaffold(topBar = {
ThemeAppBar(darkThemeState = isDarkTheme)
}, modifier = Modifier.fillMaxSize()
) { innerPadding ->
mainContent(Modifier.padding(innerPadding))
// 광고를 달아 봅니다.
AdvertView()
}
}
}
}
@Composable
fun ThemeAppBar(darkThemeState: MutableState<Boolean>) {
TopAppBar(title = {
Row {
Text(text = getString(R.string.app_name), modifier = Modifier.weight(8f))
Switch(checked = darkThemeState.value, onCheckedChange = {
darkThemeState.value = it
}, modifier = Modifier.weight(2f))
IconButton(onClick = { doLogOut() }) {
Icon(imageVector = Icons.Default.Logout, contentDescription = "LogOut")
}
}
})
}
@Composable
fun mainContent(padding: Modifier) {
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.scrollable(rememberScrollableState {
// view world deltas should be reflected in compose world
// components that participate in nested scrolling
it
}, Orientation.Vertical)
) {
AndroidViewBinding(ActivityChatRoomBinding::inflate) {
binding = this
chatMessages.child(chatId).orderByChild("crtDtim").startAfter(startDt.toString())
.addChildEventListener(object : ChildEventListener{
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildAdded")
var chatMessageItem = snapshot.getValue(ChatMessage::class.java)
// 왜 2번씩 들어가는지 모르겠지만... 일단은 한번만 들어가게 하기 위해서
if (chatMessageItem != null && chatMesgItems.indexOf(chatMessageItem) < 0) {
if (sp.getBoolean("translateTy", false)
&& !sp.getString("languageCode", "kr").equals(chatMessageItem.locale)
) {
doGetTranslate(chatMessageItem)
}
chatMesgItems.add(chatMessageItem)
}
binding.rv.adapter = adapter
binding.rv.scrollToPosition(chatMesgItems.size - 1)
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildChanged")
}
override fun onChildRemoved(snapshot: DataSnapshot) {
Log.e(TAG, "onChildRemoved")
}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildMoved")
}
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "onCancelled")
}
})
binding.sendIv.setOnClickListener {
doChatWrite()
}
binding.rv.setOnScrollChangeListener { view, i, i2, i3, i4 ->
}
binding.contentEt.setOnKeyListener(object : View.OnKeyListener{
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
if (event?.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
doChatWrite()
}
return true
}
})
}
}
}
// 이 부분은 펌한 코드 입니다.
@Composable
fun AdvertView(modifier: Modifier = Modifier) {
val isInEditMode = LocalInspectionMode.current
if (isInEditMode) {
Text(
modifier = modifier
.fillMaxWidth()
.background(Red)
.padding(horizontal = 2.dp, vertical = 6.dp),
textAlign = TextAlign.Center,
color = White,
text = "Advert Here",
)
} else {
AndroidView(
modifier = modifier.fillMaxWidth(),
factory = { context ->
AdView(context).apply {
adSize = AdSize.BANNER
adUnitId = context.getString(R.string.admob_banner_test_id)
loadAd(AdRequest.Builder().build())
}
}
)
}
}
.....
private fun doLogOut() {
chatMessages.child(chatId).setValue(null)
var rIntent:Intent = intent
rIntent.putExtra("chatId", chatId)
setResult(RESULT_OK, rIntent)
finish()
}
}
이렇게 구현을 해 보면 다음과 같이 광고가 보입니다.

How to authenticate to Firebase using Google One Tap in Jetpack Compose?
A simple solution for implementing Firebase Authentication with Google, using Jetpack Compose on Android.
medium.com
오늘은 Google One tab login에 관한 참고 자료 링크를 하나 공유합니다. 좋은 공부가 되시길 바라며...
Compose을 활용한 앱을 구현하는 동안에 이전에 만들었던 layout 을 활용하고자 하는 경우가 생긴다면... Androind ViewBinding을 활용하는 방법이 있었다.
https://developer.android.com/jetpack/compose/interop/interop-apis?hl=ko
상호 운용성 API | Jetpack Compose | Android Developers
상호 운용성 API 앱에 Compose를 채택하는 동안 Compose와 뷰 기반 UI를 결합할 수 있습니다. 다음에는 Compose로의 전환을 보다 쉽게 할 수 있는 API, 권장사항 및 팁이 나와 있습니다. Android 뷰의 Compose
developer.android.com
구현을 시작해 보면, 먼저 gradle 파일에 implementation 을 추가해야 한다.
implementation "androidx.compose.ui:ui-viewbinding:$compose_version"다음은 채팅방 구현을 위해서 예전에 만들었던 코드에서 Recycleview 을 활용했던 layout을 가지고 왔다.

<?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="match_parent"
tools:context=".ChatRoomActivity">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="vertical"
android:weightSum="5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="5"
android:orientation="horizontal"
android:weightSum="10">
<TextView
android:id="@+id/textView11"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:gravity="center_horizontal|center_vertical"
android:text="현재시간" />
<TextClock
android:id="@+id/textView10"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="7"
android:format12Hour="hh:mm"
android:format24Hour="HH:mm"
android:gravity="center_horizontal|center_vertical" />
</LinearLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:name="com.roopre.simpleboard.Fragment.ChatMsgFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout2"
tools:context=".Fragment.ChatMsgFragment"
tools:listitem="@layout/custom_chat_msg" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/content_et"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_content_et"
android:hint="메시지를 입력하세요."
android:lines="1"
android:maxLines="1"
android:padding="8dp" />
<ImageView
android:id="@+id/send_iv"
android:layout_width="40dp"
android:padding="2dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/ic_send" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
다음은 recycleview 에 들어갈 item layout은 다음과 같이 구현하였다.
<?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:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/other_cl"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/my_cl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/userid_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="userid_tv"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/date_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="date_tv"
android:textSize="10sp"
app:layout_constraintStart_toStartOf="@+id/userid_tv"
app:layout_constraintTop_toBottomOf="@+id/userid_tv" />
<TextView
android:id="@+id/content_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/bg_content_et"
android:padding="8dp"
android:text="content_tv"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/date_tv"
app:layout_constraintTop_toBottomOf="@+id/date_tv" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/my_cl"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/other_cl">
<TextView
android:id="@+id/userid_tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:text="userid_tv"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/date_tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:text="date_tv2"
android:textSize="10sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/userid_tv2" />
<TextView
android:id="@+id/content_tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_content_et"
android:padding="8dp"
android:text="content_tv2"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/date_tv2"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
그리고 Recycleview 에 데이터를 넣고 구현하기 위해서 adapter을 구현하였다.
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.compose.runtime.snapshots.SnapshotStateList;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView;
import com.billcoreatech.multichat416.R;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ViewHolder> {
private static final String TAG = "ChatAdapter";
private final ArrayList<ChatMessage> chatMsgModels;
String displayName ;
public ChatAdapter( ArrayList<ChatMessage> items, String displayName) {
this.chatMsgModels = items;
this.displayName = displayName;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.custom_chat_msg, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
ChatMessage vo = chatMsgModels.get(position);
try {
Log.e(TAG, " userId=" + vo.getDisplayName()+ ": displayName=" + displayName) ;
} catch (Exception e) {
}
if (vo.getDisplayName().equals(displayName)) {
holder.other_cl.setVisibility(View.GONE);
holder.my_cl.setVisibility(View.VISIBLE);
holder.userid_tv2.setText(vo.getDisplayName());
holder.date_tv2.setText(vo.getCrtDtim());
holder.content_tv2.setText(vo.getContent());
}else
{
holder.other_cl.setVisibility(View.VISIBLE);
holder.my_cl.setVisibility(View.GONE);
holder.userid_tv.setText(vo.getDisplayName()); // userId 대신 nickName 으로 대체
holder.date_tv.setText(vo.getCrtDtim());
holder.content_tv.setText(vo.getContent());
}
}
@Override
public int getItemCount() {
return chatMsgModels.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public ConstraintLayout my_cl, other_cl;
public TextView userid_tv, date_tv, content_tv, userid_tv2, date_tv2, content_tv2;
public ViewHolder(View view) {
super(view);
my_cl = view.findViewById(R.id.my_cl);
other_cl = view.findViewById(R.id.other_cl);
userid_tv = view.findViewById(R.id.userid_tv);
date_tv = view.findViewById(R.id.date_tv);
content_tv = view.findViewById(R.id.content_tv);
userid_tv2 = view.findViewById(R.id.userid_tv2);
date_tv2 = view.findViewById(R.id.date_tv2);
content_tv2 = view.findViewById(R.id.content_tv2);
// 2021.11.01 item 클릭 처리를 위해서 추가
itemView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
int pos = getAdapterPosition() ;
if (pos != RecyclerView.NO_POSITION) {
// 리스너 객체의 메서드 호출.
if (mListener != null) {
mListener.onItemClick(v, pos) ;
}
}
}
});
}
}
// 2021.11.01 리스너 객체 참조를 저장하는 변수
private OnItemClickListener mListener = null ;
// OnItemClickListener 리스너 객체 참조를 어댑터에 전달하는 메서드
public void setOnItemClickListener(OnItemClickListener listener) {
this.mListener = listener ;
}
public interface OnItemClickListener {
void onItemClick(View v, int position) ;
}
}
다음은 데이터를 넣기 위한 구조체는 다음 처럼 구현을 하였다.
data class ChatMessage(
var displayName:String = "",
var crtDtim:String = "",
var content:String = ""
)kotlin 으로 구현을 하면서 좋은 것은 source code 가 간소화된다는 것이다. java로 구현했다면 getter / setter을 다 넣어 주었어야 하겠지만, kotlin 으로 구현하다 보니 그럼 군더더기는 필요가 없게 되었다.
이번에는 채팅방 운영을 위한 activity code을 구현해 보았다.
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.KeyEvent.KEYCODE_ENTER
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.ManageAccounts
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.core.content.ContextCompat
import com.billcoreatech.multichat416.databean.ChatAdapter
import com.billcoreatech.multichat416.databean.ChatMessage
import com.billcoreatech.multichat416.databean.ChatMessageViewModel
import com.billcoreatech.multichat416.databinding.ActivityChatRoomBinding
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class ChatRoomActivity : ComponentActivity() {
var TAG = "ChatRoomActivity"
lateinit var displayName:String
lateinit var auth: FirebaseAuth
lateinit var sp: SharedPreferences
lateinit var sdf:SimpleDateFormat
private val database = Firebase.database
private val chatMessages = database.getReference("ChatMessage")
lateinit var df:SimpleDateFormat
lateinit var chatId:String
lateinit var startDt:String
lateinit var adapter:ChatAdapter
var chatMesgItems = ArrayList<ChatMessage>()
private set
lateinit var binding:ActivityChatRoomBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
auth = Firebase.auth
sp = getSharedPreferences("MultiChat", MODE_PRIVATE)
sdf = SimpleDateFormat("yyyyMMddHHmmss")
df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
chatId = intent.getStringExtra("chatId") as String
startDt = intent.getSerializableExtra("startDt") as String
Log.e(TAG, "${startDt}")
displayName = auth.currentUser?.displayName.toString()
chatMesgItems.clear()
adapter = ChatAdapter(chatMesgItems, displayName)
binding = ActivityChatRoomBinding.inflate(layoutInflater)
setContent {
val isDarkTheme = remember { mutableStateOf(false) }
if(isDarkTheme.value){
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlack)
}else{
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlue)
}
MultiChat416Theme(darkTheme = isDarkTheme.value) {
Scaffold(topBar = {
ThemeAppBar(darkThemeState = isDarkTheme)
}, modifier = Modifier.fillMaxSize()
) { innerPadding ->
mainContent(Modifier.padding(innerPadding))
}
}
}
}
@Composable
fun ThemeAppBar(darkThemeState: MutableState<Boolean>) {
TopAppBar(title = {
Row {
Text(text = getString(R.string.app_name), modifier = Modifier.weight(8f))
Switch(checked = darkThemeState.value, onCheckedChange = {
darkThemeState.value = it
}, modifier = Modifier.weight(2f))
IconButton(onClick = { }) {
Icon(imageVector = Icons.Default.Face, contentDescription = "ChatRoom")
}
IconButton(onClick = { doProfile() }) {
Icon(imageVector = Icons.Default.ManageAccounts, contentDescription = "Profile")
}
IconButton(onClick = { doLogOut() }) {
Icon(imageVector = Icons.Default.Logout, contentDescription = "LogOut")
}
}
})
}
private fun doProfile() {
var intent = Intent(this@ChatRoomActivity, SettingActivity::class.java)
startActivity(intent)
}
@Composable
fun mainContent(padding: Modifier) {
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.scrollable(rememberScrollableState {
// view world deltas should be reflected in compose world
// components that participate in nested scrolling
it
}, Orientation.Vertical)
) {
// compose 에서 layout 을 binding 해서 사용하는 코드 구현...
AndroidViewBinding(ActivityChatRoomBinding::inflate) {
var binding = this
chatMessages.child(chatId).orderByChild("crtDtim").startAfter(startDt.toString())
.addChildEventListener(object : ChildEventListener{
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildAdded")
var chatMessageItem = snapshot.getValue(ChatMessage::class.java)
// 왜 2번씩 들어가는지 모르겠지만... 일단은 한번만 들어가게 하기 위해서
if (chatMessageItem != null && chatMesgItems.indexOf(chatMessageItem) < 0) {
chatMesgItems.add(chatMessageItem)
}
binding.rv.adapter = adapter
binding.rv.scrollToPosition(chatMesgItems.size - 1)
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildChanged")
}
override fun onChildRemoved(snapshot: DataSnapshot) {
Log.e(TAG, "onChildRemoved")
}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
Log.e(TAG, "onChildMoved")
}
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "onCancelled")
}
})
this.sendIv.setOnClickListener {
if (this.contentEt.text.length > 0) {
var chatMessage = ChatMessage(displayName, df.format(GregorianCalendar.getInstance(TimeZone.getDefault()).timeInMillis),this.contentEt.text.toString() )
chatMessages.child(chatId).push().setValue(chatMessage).addOnSuccessListener {
Log.e(TAG, "push Success...")
}.addOnFailureListener {
Log.e(TAG, "push Failure...")
}
}
}
}
}
}
private fun doLogOut() {
chatMessages.child(chatId).setValue(null)
finish()
}
}
이렇게 구현을 해서 처리가 되기는 했지만, 아직 해소가 되지 않은 것은 내용을 입력 하면 realtime database에 기록이 되고, addChiledEventListener을 통해서 기록된 내용을 가져와서 recycleview에 표시를 하기 위해서 arryalist에 넣어 주는 구현을 하였는데, 입력은 1번인데 실제 표시는 2번씩 나오는 현상이 발생하였다. 아직 그 원인을 알지 못해 꼼수를 넣었다. arraylist에 이미 들어 있는 거면 넣지 않도록 하여 해소를 하였다.
구현된 화면 예시와 동작은 다음과 같이 처리가 되었다.

이렇게 까지 구현을 하면 compose 로 화면을 구현하고 예전에 만들었던 layout 을 가져와서 활용하는 것도 구현을 해 보았다.
오늘은 firebase의 real time database에 채팅방 개설을 하는 기능을 구현해 보아야겠다. 먼저 저장할 채팅방의 데이터 구조체를 구현해 보자.
import java.util.*
data class ChatRooms(
var chatRooms:String = "",
var chatTitle:String = "",
var roomOwner:String = "",
var chatNo:String = ""
)들어가는 항목은 방 이름, 방제목, 방 개설자, 방 번호(이건 key로 사용할 것)
firebase의 realtime database 연동을 위한 준비는 이전에 posting 했던 글이나, 구글에서 찾아보면 많이 나오고 있으므로 생략...
다음은 데이터를 가져오고 저장하는 것을 구현하기 위해서 viewmodel을 하나 만들었다.
jetpack compose 을 이용해서 setContent로 만든 화면은 앱이 시작될 때 구성이 마무리되기 때문에 화면 구성이 된 이후에 데이터를 화면에 보여주는 작업을 하기 위해서 onCreate에서 데이터를 읽어오고 하는 것을 해 보기는 했는 데, 데이터가 online으로 받아서 저장을 하는 것이다 보니, 화면이 구성된 이후에는 변화가 생기지 않았다.
그래서 구글 검색을 통해서 확인한 방법은 viewmodel을 이용하여 구현하는 방법이 있는 것을 알게 되었다.
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
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 kotlin.coroutines.suspendCoroutine
class ListViewModel: ViewModel() {
private val database = Firebase.database
private val chatroom = database.getReference("ChatRooms")
var charItems = mutableStateListOf<ChatRooms>()
private set
fun addItem(chatRooms: ChatRooms) {
chatroom.child(chatRooms.chatNo).setValue(chatRooms)
.addOnSuccessListener {
Log.e("setValue", "success")
viewAllItem()
}
.addOnFailureListener { Log.e("setValue", "error") }
}
fun removeItem(chatRooms: ChatRooms) {
chatroom.child(chatRooms.chatNo).setValue(null)
.addOnSuccessListener {
Log.e("setValue", "delete success")
viewAllItem()
}
.addOnFailureListener { Log.e("setValue", "error") }
}
fun viewAllItem() {
chatroom.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
charItems.clear()
dataSnapshot.children.map { snapshot ->
charItems.add(snapshot.getValue(ChatRooms::class.java)!!)
}
}
override fun onCancelled(error: DatabaseError) {}
})
}
}viewmodel에 정의 함수는 addItem, removeItem, viewAllItem 딱 3개의 함수만 있다. 이것으로 데이터를 저장하고, 삭제하고, 전체 조회를 구현하고 하는 기능은 다 구현이 된다. update 기능의 경우는 realtime database의 경우는 key만 동일하다면 setValue을 통해서 처리가 가능하기 때문에 따로 구현하지 않았다.
import android.content.DialogInterface
import android.content.DialogInterface.BUTTON_NEGATIVE
import android.content.DialogInterface.BUTTON_POSITIVE
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.navigation.compose.rememberNavController
import com.billcoreatech.multichat416.databean.ChatRooms
import com.billcoreatech.multichat416.databean.ListViewModel
import com.billcoreatech.multichat416.databinding.MakeChatroomLayoutBinding
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
import com.billcoreatech.multichat416.widget.*
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import java.text.SimpleDateFormat
import java.util.*
class MainActivity : ComponentActivity() {
var TAG = "MainActivity"
lateinit var googleSignInClient: GoogleSignInClient
lateinit var auth: FirebaseAuth
lateinit var sp: SharedPreferences
private val chatroomViewModel by viewModels<ListViewModel>()
lateinit var today:Calendar
lateinit var sdf:SimpleDateFormat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val gso = GoogleSignInOptions
.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)
auth = Firebase.auth
sp = getSharedPreferences("MultiChat", MODE_PRIVATE)
sdf = SimpleDateFormat("yyyyMMddHHmmss")
if (auth.currentUser == null) {
var intent = Intent(this@MainActivity, GoogleSignIn::class.java)
startActivity(intent)
finish()
}
chatroomViewModel.viewAllItem()
setContent {
val isDarkTheme = remember { mutableStateOf(false) }
val navController = rememberNavController()
if(isDarkTheme.value){
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlack)
}else{
this.window.statusBarColor = ContextCompat.getColor(this, R.color.softBlue)
}
MultiChat416Theme(darkTheme = isDarkTheme.value) {
Scaffold(topBar = {
ThemeAppBar(darkThemeState = isDarkTheme)
},floatingActionButton = {
FloatingActionButton(onClick = {
doMakeChatRoom()
}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "add ChatRoom"
)
}
}
) { innerPadding ->
mainContent(Modifier.padding(innerPadding))
}
}
}
}
@Composable
fun mainContent(padding: Modifier) {
LazyColumnDemo(chatroomViewModel.charItems,
onClick = {
Log.e(TAG, "${it.chatNo}")
}, onDelete = {
if (it.roomOwner.contentEquals(auth.currentUser?.displayName.toString())) {
Log.e(TAG, "delete ${it.chatNo}")
chatroomViewModel.removeItem(it)
}
})
}
@Composable
fun ThemeAppBar(darkThemeState: MutableState<Boolean>) {
TopAppBar(title = {
Row {
Text(text = getString(R.string.app_name), modifier = Modifier.weight(8f))
Switch(checked = darkThemeState.value, onCheckedChange = {
darkThemeState.value = it
}, modifier = Modifier.weight(2f))
IconButton(onClick = { doLogOut() }) {
Icon(imageVector = Icons.Default.Home, contentDescription = "Home")
}
IconButton(onClick = { doLogOut() }) {
Icon(imageVector = Icons.Default.Face, contentDescription = "Facebook")
}
IconButton(onClick = { doLogOut() }) {
Icon(imageVector = Icons.Default.Logout, contentDescription = "LogOut")
}
}
})
}
private fun doMakeChatRoom() {
var binding = MakeChatroomLayoutBinding.inflate(layoutInflater)
var alertDialogBuilder = AlertDialog.Builder(this, R.style.ThemeDialog)
.setTitle(getString(R.string.titleMakeChatRoom))
.setView(binding.root)
.setPositiveButton(getString(R.string.OK), DialogInterface.OnClickListener{dialogInterface, i ->
today = Calendar.getInstance(Locale.KOREA)
var chatRoom = ChatRooms(binding.editRoomNm.text.toString(),
binding.editTitle.text.toString(),
auth.currentUser?.displayName.toString(),
sdf.format(today.timeInMillis))
chatroomViewModel.addItem(chatRoom)
Log.e(TAG, ".....")
})
.setNegativeButton(getString(R.string.CANCEL), DialogInterface.OnClickListener{ dialogInterface: DialogInterface, i: Int ->
})
var alertDialog = alertDialogBuilder.create()
alertDialog.show()
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
alertDialog.getButton(BUTTON_POSITIVE).setTextColor(getColor(R.color.softBlue))
alertDialog.getButton(BUTTON_NEGATIVE).setTextColor(getColor(R.color.softRed))
}
private fun doLogOut() {
googleSignInClient.signOut().addOnCompleteListener( OnCompleteListener {
task -> if (task.isComplete ) {
Log.e(TAG, "finish ... " )
auth.signOut()
finish()
}
})
}
}MainActivity 가 이전 posting 과 달라지는 것은
chatroomViewModel by viewModels<ListViewModel>()viewmodel을 생성하고 데이터를 읽어오기 위해서
chatroomViewModel.viewAllItem()데이터를 조회한 부분과
@Composable
fun mainContent(padding: Modifier) {
LazyColumnDemo(chatroomViewModel.charItems,
onClick = {
Log.e(TAG, "${it.chatNo}")
}, onDelete = {
if (it.roomOwner.contentEquals(auth.currentUser?.displayName.toString())) {
Log.e(TAG, "delete ${it.chatNo}")
chatroomViewModel.removeItem(it)
}
})
}mainContent 함수를 이용해서 LazyColumn에 데이터를 넣어주는 부분이 달라진다. 그리고 이번에는 LazyColumn 울 이용해서 화면에 데이터를 보여줄 view을 구현하는 부분인데, 아래와 같이 새로 하나 만들어 구현하였다.
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.billcoreatech.multichat416.databean.ChatRooms
@Composable
fun LazyColumnDemo(charItems: SnapshotStateList<ChatRooms>,
onClick:(ChatRooms) -> Unit,
onDelete:(ChatRooms) -> Unit
) {
Log.e("LazyColumnDemo", "${charItems.size}")
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 2.dp)
) {
items(
items = charItems,
itemContent = {
UserListItem(chatRooms = it, onClick = {onClick(it)}, onDelete = {onDelete(it)})
})
}
}
@Composable
fun UserListItem(chatRooms: ChatRooms,
onClick:() -> Unit,
onDelete:() -> Unit
) {
Card(
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth(),
elevation = 4.dp
) {
Column(modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
TextButton(
onClick = { onClick() },
content = {
Text(
text = chatRooms.chatTitle,
style = TextStyle(
color = Color.Blue,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
)
}
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = chatRooms.chatRooms,
style = TextStyle(
color = Color.Black,
fontSize = 16.sp
)
)
Spacer(modifier = Modifier.padding(16.dp))
Text(
text = chatRooms.roomOwner,
style = TextStyle(
color = Color.Blue,
fontSize = 16.sp
)
)
IconButton(onClick = { onDelete() } ) {
Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete" )
}
}
}
}
}
이제 구현된 화면의 예시를 보면 다음과 같다.

기능을 구현하는 과정에서 저번 posting 에서 달아 두었던 타이틀과 버튼들은 제거하고 topappbar을 달고 아이콘 버튼을 달았다. 다음에는 이 아이콘 버튼에 기능을 하나씩 달아보아야겠다.
이렇게 해서 채팅방을 생성하는 화면을 구현해 보았다. 이제 다음번에는 채팅창을 구현해 보아야겠다. 그리고 원래 목적인 다국어 채팅을 위한 번역 API 사용 방법도 찾아보아야겠다.