2026/03/02

오늘의 이야기

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을 가지고 왔다.


 


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 사용 방법도 찾아보아야겠다.


 


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle,羽毛球俱乐部必备应用!
👉 比洞赛 – 记录分数并寻找对手 🎉
适合任何地方,独自一人、与朋友一起或在俱乐部! 🤝
如果你喜欢羽毛球,一定要尝试一下

前往应用程序👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

지난번에는 로그인하는 화면을 만들었으니, 이번에는 로그인해서 나오는 메인을 구현해 볼 요량이다. 


메인화면 예시



 


메인화면이라고는 뭐 아직 버튼 2개와 타이틀 하나를 달았을 뿐이다.


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


이것이 그냥 Layout 을 이용해서 작업을 하고 있다면 조금은 더 쉽고 빠르게 될 것 같기는 하나, 이왕 배우기 시작한 jetpack compose을 활용해서 만들어 보기로 했다.


 



import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
import com.billcoreatech.multichat416.widget.buttonAddChatRoom
import com.billcoreatech.multichat416.widget.buttonLogOut
import com.billcoreatech.multichat416.widget.titleView
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.database.DatabaseReference
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class MainActivity : ComponentActivity() {

var TAG = "MainActivity"
lateinit var database : FirebaseDatabase
lateinit var chatroom : DatabaseReference

lateinit var googleSignInClient: GoogleSignInClient
lateinit var auth: FirebaseAuth

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()

database = Firebase.database
chatroom = database.getReference("ChatRooms")
googleSignInClient = GoogleSignIn.getClient(this, gso)
auth = Firebase.auth

setContent {
MultiChat416Theme {
titleView(getString(R.string.titleChatRoom))
buttonAddChatRoom(applicationContext) {

}

buttonLogOut(applicationContext) {
doLogOut()
}

}
}
}

private fun doLogOut() {
googleSignInClient.signOut().addOnCompleteListener( OnCompleteListener {
task -> if (task.isComplete ) {
Log.e(TAG, "finish ... " )
auth.signOut()
finish()
}
})
}

}

 


MainActivity 에는 이제 꼭 필요한 코드만 들어가고 아무것도 없다. 화면에 구현된 타이틀 (titleView) 과 버튼 2개는 따로 빼서 class을 만들었다. 


 


먼저 제목을 보여주는 titleView 의 코드


 


다른 activity 에서도 활용을 하기 위해서 parameter로 제목 이름을 받고 그것을 전체 화면 상단 오른쪽에 표시하도록 하였다.



import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun titleView(title:String) {

Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start,
){
Card(modifier = Modifier
.width(170.dp)
.height(60.dp)
.padding(5.dp),
shape = RoundedCornerShape(20.dp),
border = BorderStroke(width = 2.dp, color = Color.White),
) {
Text(text = title, color = Color.Blue
, modifier = Modifier.padding(10.dp))
}
}
}

 


다음은 화면 아래쪽에 있는 Floating Button 의 코드


 


버튼에 표시할 테스트를 string.xml 에서 가져오기 위해서 context을 전달했고,  버튼을 클릭했을 때 동작을 위에서 하기 위해 onClick 함수도 만들어 썼다.



import android.content.Context
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.billcoreatech.multichat416.R

@Composable
fun buttonAddChatRoom(
context: Context,
onClick: () -> Unit
) {
var context = LocalContext.current
Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.End
){
ExtendedFloatingActionButton(
backgroundColor = Color.White,
contentColor = Color.Blue,
icon = { Icon(Icons.Filled.Add,"") },
text = { Text(context.getString(R.string.addChatRoom)) },
onClick = { onClick() },
elevation = FloatingActionButtonDefaults.elevation(8.dp)
)
}
}

 


다음은 화면 상단에 있는 Logout 버튼


 


이 버튼 소스에서 context 을 전달해서 string.xml의 글자를 보여주도록 하였고, 버튼 click 처리를 하기 위해서 onClick을 전달했다. (사실은 받아와서 처리할 수 있도록 구현은 MainActivity에서...)


import android.content.Context
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FloatingActionButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Logout
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.billcoreatech.multichat416.R

@Composable
fun buttonLogOut(
context: Context,
onClick: () -> Unit
) {
Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.End,
){
ExtendedFloatingActionButton(
backgroundColor = Color.White,
contentColor = Color.Blue,
icon = { Icon(Icons.Filled.Logout,"") },
text = { Text(context.getString(R.string.doLogout)) },
onClick = { onClick() },
elevation = FloatingActionButtonDefaults.elevation(8.dp)
)
}
}

 


이런 분리를 하니, 재활용성이나 Copy&Paste 해서 다른 함수로 옮겨 가는 것도 도움이 될 것 같다.  아무튼 오늘도 이 거 하나 만들어 내는데, 3시간이 훌쩍~   이래서야 언제 다 끝낼 수 있을지???


 





오늘의 이야기

점심을 먹고 나선 길에서 만난 여름...


가로숲



 


이렇게 보면 마치 어느 시골에 있는 가로수길 같은 느낌이 들기는 하다.  정작 이 곳은 길도 아니고 사무실 근처 정부청사앞 숲 공원의 나무들 사이다.


 


사진만 보면 여유로운 시간을 가지고 멀리 나들이 나간 것 같기는 하지만...   늘상의 시간이 지나가고 있고, 그 와중에 이 봄은 중간쯤 와 있는 것 같기는 하지만, 기온등의 날씨로 봐서는 선뜻 여름이 다가와 섰다고 느껴진다.


 


그렇게 또 한 계절이 스치고 지나간다.  나른한 봄 기운이 좋은 시절이기는 한데, 나의 마음에 아직도 어두운 밤 공기만 가득한 지 ?  긍정의 에너지만 있어야 할 것 같은 시기이기도 하지만, 정작은 ...


 


그래도 오늘 하루를 마무리 하며... 좋은 일만 함께 하길 바란다. 이 시간도 어느새 지나갈 테이니...


 


 


 





오늘의 이야기


#스하리1000명프로젝트,
韓国で迷子になりましたか?韓国語が話せなくても、このアプリを使えば簡単に移動できます。
あなたの言語で話すだけで、翻訳、検索が行われ、結果があなたの言語で表示されます。
旅行者に最適!英語、日本語、中国語、ベトナム語などを含む 10 以上の言語をサポートします。
今すぐ試してみましょう!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/01

오늘의 이야기

이 나라에는 여러 나라의 사람 들어와 살고 있다.  코리안 드림을 꿈꾸며... 한 때는 이 나라의 사람들이 아메리칸드림을 꿈꾸며, 미국으로 떠났던 것처럼... 그래서 이번에는 다국적 언어를 이용한 채팅앱을 하나 만들어 볼까 한다.


 


잘 될지는 모르겠지만, 그러면서 Jetpack Compose 을 이용한 화면 구성 등에 대한 공부를 해 볼 요량이다. 


오늘은 그 처음으로 로그인 페이지를 하나 만들어 보고자 한다. 


 


로그인 페이지



이 화면을 layout 으로 그리라고 했다면


아마도 금새 그리지 않았을까 하는 생각이 든다.


 


화면에 Card box 와 Text가 들어간 button 기능 3개


제목 이름 표시 하나


 


그런데, 난 이 화면 하나를 그리기 위해서 며칠을 고민했고, 예제를 하나 찾았다. 그리고 그 예제를 참고해 가면서 화면을 그렸다.


 


그러자니, 3시간쯤... 지났다.


 


예전 project 파일에서 필요한 기능 몇개를 가지고 왔고, 그리고는 이제 완성이 되었다.


 


그냥 layout 으로 그리는 것이 나은가?  쉽게 그리는 데는 그것이 낫을 것 같기도 하다.


 


 


 


 


 


 


 


 


 


 


 


화면을 그린 source 을 살펴보자.


 



import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.preference.PreferenceManager.getDefaultSharedPreferences
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
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.common.api.ApiException
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.android.play.core.review.model.ReviewErrorCode
import com.google.android.play.core.tasks.RuntimeExecutionException
import com.google.android.play.core.tasks.Task
import com.google.firebase.appcheck.FirebaseAppCheck
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import java.text.SimpleDateFormat
import java.util.*

class GoogleLogin : ComponentActivity() {

var appUpdateManager: AppUpdateManager? = null
var appUpdateInfoTask: Task<AppUpdateInfo>? = null
lateinit var sp: SharedPreferences
lateinit var editor: SharedPreferences.Editor
lateinit var sdf: SimpleDateFormat
lateinit var today: Calendar
lateinit var googleSignInClient: GoogleSignInClient
lateinit var auth: FirebaseAuth
val MY_REQUEST_CODE = 1000
var RC_SIGN_IN:Int = 1001
var TAG = "GoogleLogin"

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

setContent {
MultiChat416Theme {

fun signIn() {
val signInIntent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}

fun doLogOut() {
googleSignInClient.signOut().addOnCompleteListener( OnCompleteListener {
task -> if (task.isComplete ) {
Log.e(TAG, "finish ... " )
auth.signOut()
finish()
}
})
}

Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("M")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("ulti")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" L")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("anguage")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" C")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("hatting")
}
}, fontSize = 30.sp)
Spacer(Modifier.size(32.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
signIn()
},content={
Text(text = getString(R.string.title_activity_google_login), color = Color.Blue)
})
}
Spacer(Modifier.size(8.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
Toast.makeText(applicationContext,
getString(R.string.mesgRegistorOnlyGoogle),
Toast.LENGTH_SHORT
).show()
}) {
Text(text = getString(R.string.doRegister), color = Color.Red)
}
}
Spacer(Modifier.size(8.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
doLogOut()
}) {
Text(text = getString(R.string.doLogout), color = Color.Gray)
}
}

}
}
}

val firebaseAppCheck = FirebaseAppCheck.getInstance()
firebaseAppCheck.installAppCheckProviderFactory(
if (BuildConfig.DEBUG_MODE) {
Log.e(TAG, "DebugAppCheckProviderFactory ...")
DebugAppCheckProviderFactory.getInstance() // AVD 에서 적용시
//SafetyNetAppCheckProviderFactory.getInstance() // 폰에서 적용시
} else {
Log.e(TAG, "SafetyNetAppCheckProviderFactory ...")
SafetyNetAppCheckProviderFactory.getInstance() // 폰에서 적용시
}
)

appUpdateManager = AppUpdateManagerFactory.create(this@GoogleLogin)
// Returns an intent object that you use to check for an update.
appUpdateInfoTask = appUpdateManager!!.appUpdateInfo
doUpdateCheck()

sp = getSharedPreferences("MultiChat", MODE_PRIVATE)
editor = sp.edit()

sdf = SimpleDateFormat("yyyyMMdd")
today = Calendar.getInstance()
if (!sp.getBoolean("REVIEW", false)) {
editor.putString("REVIEW_DATE", sdf.format(today.timeInMillis))
editor.commit()
}

// 사용하고 10일 지나가면 ...
if ((today.timeInMillis - sdf.parse(sp.getString("REVIEW_DATE",sdf.format(today.timeInMillis))).time) / (1000 * 60 * 60 * 24) > 10) {
var manager = ReviewManagerFactory.create(this@GoogleLogin)
var request = manager.requestReviewFlow()
editor.putBoolean("REVIEW", true)
editor.commit()
request.addOnCompleteListener { task ->
if (task.isSuccessful) {
// We got the ReviewInfo object
val reviewInfo = task.result
if (sp.getBoolean("REVIEW", false)) {
doReviseInfo(manager, this@GoogleLogin, reviewInfo)
}
} else {
// There was some problem, log or handle the error code.
@ReviewErrorCode val reviewErrorCode = (task.getException() as RuntimeExecutionException).errorCode
}
}
}
}

private fun doUpdateCheck() {
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask!!.addOnSuccessListener { appUpdateInfo: AppUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// This example applies an immediate update. To apply a flexible update
// instead, pass in AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// Request the update.
try {
doRequestUpdate(appUpdateInfo)
} catch (e: IntentSender.SendIntentException) {
e.printStackTrace()
}
}
}
}

@Throws(IntentSender.SendIntentException::class)
private fun doRequestUpdate(appUpdateInfo: AppUpdateInfo) {
Log.e(TAG, "do Update Start")
appUpdateManager!!.startUpdateFlowForResult( // Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo, // Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.IMMEDIATE, // The current activity making the update request.
this, // Include a request code to later monitor this update request.
MY_REQUEST_CODE
)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == MY_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
Log.e(TAG, "Update flow failed! Result code: $resultCode")
// If the update is cancelled or fails,
// you can request to start the update again.
}
}

if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
// Google Sign In was successful, authenticate with Firebase
val account = task.getResult(ApiException::class.java)!!
Log.e(TAG, "firebaseAuthWithGoogle: ${account.id}")
firebaseAuthWithGoogle(account.idToken!!)
} catch (e: ApiException) {
// Google Sign In failed, update UI appropriately
Log.e(TAG, "Google sign in failed ${e.message}")
}
}
}

private fun firebaseAuthWithGoogle(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
auth.signInWithCredential(credential)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.e(TAG, "signInWithCredential:success")
val user = auth.currentUser
var intent = Intent(this@GoogleLogin, MainActivity::class.java)
startActivity(intent)
finish()
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.exception)

}
}
}

private fun doReviseInfo(manager: ReviewManager, activity: Activity, reviewInfo: ReviewInfo) {
val flow = manager.launchReviewFlow(activity, reviewInfo)
flow.addOnCompleteListener { _ ->
editor.putBoolean("REVIEW", false)
editor.putString("REVIEW_DATE", sdf.format(today.timeInMillis))
editor.commit()
}
}
}

다른 건 볼 꺼 없고, setContent 함수에 들어 있는 것만 보면 될 것 같다.  전체 흐름을 알아야 하기 때문에,  


 


setContent {
// 테마이름은 개별적이므로 참고만...
MultiChat416Theme {

.....

// 먼저 아래로 배열을 하기 위해서 선언 linearlayout 처럼 사용...
Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){

// 제목을 표시 한다. 첫글짜만, 대문자로 표시, 폰트색상도 빨간색...
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("M")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("ulti")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" L")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("anguage")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" C")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("hatting")
}
}, fontSize = 30.sp)
// 여백 두기
Spacer(Modifier.size(32.dp))

// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
.....
},content={
Text(text = getString(R.string.title_activity_google_login), color = Color.Blue)
})
}

// 여백 두기
Spacer(Modifier.size(8.dp))
// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
Toast.makeText(applicationContext,
getString(R.string.mesgRegistorOnlyGoogle),
Toast.LENGTH_SHORT
).show()
}) {
Text(text = getString(R.string.doRegister), color = Color.Red)
}
}

// 여백 두기
Spacer(Modifier.size(8.dp))
// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
.....
}) {
Text(text = getString(R.string.doLogout), color = Color.Gray)
}
}

}
}
}

대략적으로 이런 정도의 코드로 위에 나오는 화면을 구성을 하게 된다.  이걸 Layout으로 만들어 보면 얼마나 걸릴까?


 


아무튼 오늘은 1일 차 coding 끝.


 


 


 


다음은 채팅목록을 구현해 보겠다.  Database 은 Firebase 의 Realtime database 을 활용할 생각이다.


 





오늘의 이야기

https://rrtutors.com/tutorials/implement-room-database-in-jetpack-compose



 


Flutter Mobile app,Flutter widgets,Jetpack Compose Tutorial,Create Android App,Java,python,Ruby


Create Flutter applications,Jetpack Compose Tutorial,Create android application,Python,Java,RxJava,dart,GoLang examples


rrtutors.com




 


사실은 room database 가 어떤 것으로 구성해야 하는 가에 대한 고민을 하는 과정에서 찾아보게 된 site인데, 


내용을 보던 중에 여러가지 예제를 볼 수 있었다.  샘플 코드를 보고 이것저것 해 보면 compose을 어떻게 구현해야 하는 가에 대한 이해를 할 수 있을 것 같아서 기록해 두기로 한다. 


 


어젯밤 이후에 확인된 부분만 간단하게 기록을 해 보면 다음과 같다. 


activity 연동 예제 1



 


activity 연동 예제 2




 




 


회원가입 페이지 예제



 


데이터를 이용한 list 보여주기



 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


이것들을 하나씩 찾아서 수정된 부분으로 실행을 해 보아야 하겠지만, 그런 정도의 수고(?)는 해야 배우는 맛이 나지 않겠는가?


 


코드 작업을 할 때 참고할 부분이 많을 것 같은 예제를 찾았다.  잘 뜯어 보면서 나의 코드를 만들어 보아야 겠다.


다음엔 나의 코드 이야기를 적어 볼 수 있기를 바라며...


 





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

소셜 로그인 firebase에서 지원하고 있는 소셜 로그인(?)은 Google, Facebook, Apple, Microsoft, Twitter 등 대부분 외국계(?)입니다. firebase 의 소셜 로그인 지원 우리나라에서 대다수가 사용하는 nave...