-
[Android/ kotlin] STOMP 를 이용한 실시간 양방향 채팅 기능 구현Android/kotlin 2023. 2. 16. 02:25
Spring boot에서
STOMP, Redis, Websocket을 이용해 채팅서버를 만들게 된다.
그렇다면 안드로이드에서는 stomp를 활용한 채팅기능을 구현하려면 어떻게 코드를 구성해야 할까?
1. 퍼미션 추가
//chat implementation fileTree(dir: "libs", include: ["*.jar"]) // stomp 라이브러리 추가 api "com.github.NaikSoftware:StompProtocolAndroid:1.6.4" //implementation 'com.github.NaikSoftware:StompProtocolAndroid:1.6.5' implementation 'com.github.bishoybasily:stomp:2.0.5' implementation 'org.java-websocket:Java-WebSocket:1.3.0' implementation 'com.squareup.okhttp3:okhttp:3.12.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.5' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0' implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.beust:klaxon:5.0.1'
2. Constant 파일을 만들기
object Constant{ val MESSAGE_TYPE_ENTER: String = "ENTER" val MESSAGE_TYPE_TALK: String = "TALK" var SENDER: String = "DEFAULT" val URL: String = "ws://172.20.14.160:8080/chat/chatting/websocket" //ws://[도메인]/[엔드포인트]/websocket var CHATROOM_ID: String = "0" fun set(sender: String, chatRoomId: String){ SENDER = sender CHATROOM_ID = chatRoomId } }
통신 url은 다음과 같이 작성
ws://[도메인]/[엔드포인트]/websocket
2.ChatAdapter 만들기 : 채팅 메시지를 나타낸다
class ChatAdapter(val context: Context) : RecyclerView.Adapter<ChatAdapter.Holder>() { val MY_VIEW = 1 val RECEIVE_VIEW = 2 val ENTER_VIEW = 3 val constant = Constant var receiverImage="" var chatDatas = ArrayList<Chat>() fun addItem(item: Chat,image:String){ chatDatas.add(item) receiverImage=image notifyDataSetChanged() } inner class Holder(itemView: View): RecyclerView.ViewHolder(itemView){ val mid = itemView.findViewById<TextView>(R.id.mid) val chatItem = itemView.findViewById<TextView>(R.id.messageTextView) val senderImage = itemView.findViewById<ImageView>(R.id.sender_img) fun bind(chatData: Chat, context: Context){ val viewType = itemViewType when(viewType){ ENTER_VIEW -> chatItem.text = chatData.message MY_VIEW -> { //mid.text = chatData.sender chatItem.text = chatData.message} else -> { //senderImage val defaultImage = R.drawable.profile_none Glide.with(itemView) .load(receiverImage) // 불러올 이미지 url .placeholder(defaultImage) // 이미지 로딩 시작하기 전 표시할 이미지 .error(defaultImage) // 로딩 에러 발생 시 표시할 이미지 .fallback(defaultImage) // 로드할 url 이 비어있을(null 등) 경우 표시할 이미지 .circleCrop() // 동그랗게 자르기 .into(senderImage) mid.text = chatData.sender chatItem.text = chatData.message } } } } override fun getItemViewType(position: Int) : Int{ val currentItem: Chat = chatDatas[position] val msgType = currentItem.type val sender = currentItem.sender when(msgType){ constant.MESSAGE_TYPE_ENTER -> { return ENTER_VIEW } constant.MESSAGE_TYPE_TALK -> { when(sender){ constant.SENDER -> return MY_VIEW else -> return RECEIVE_VIEW } } } return super.getItemViewType(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { val view = when(viewType){ MY_VIEW -> LayoutInflater.from(context).inflate(R.layout.item_my_msg, parent, false) RECEIVE_VIEW -> LayoutInflater.from(context).inflate(R.layout.item_receive_msg, parent, false) else -> LayoutInflater.from(context).inflate(R.layout.item_enter_msg, parent, false) } return Holder(view) } override fun getItemCount(): Int { return chatDatas.size } override fun onBindViewHolder(holder: Holder, position: Int) { holder.bind(chatDatas[position], context) } fun RecyclerView.smoothSnapToPosition(position: Int, snapMode: Int = LinearSmoothScroller.SNAP_TO_START) { val smoothScroller = object: LinearSmoothScroller(this.context) { override fun getVerticalSnapPreference(): Int { return snapMode } override fun getHorizontalSnapPreference(): Int { return snapMode } } smoothScroller.targetPosition = position layoutManager?.startSmoothScroll(smoothScroller) } }
수신자와 발신자 타입에 따라서 뷰를 다르게 보여준다.
3. ChatFramgent : 채팅 코드 구현
class ChatFragment: Fragment() { lateinit var cAdapter: ChatAdapter var jsonObject = JSONObject() val constant: Constant = Constant lateinit var stompConnection: Disposable lateinit var topic: Disposable var receiverImage="" var receiver="" var selectEmoticonUrl="" var url1="" var url2="" var url3="" var url4="" @RequiresApi(Build.VERSION_CODES.O) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { var root = inflater.inflate(R.layout.fragment_chat, container, false) val bundle = arguments if(bundle != null) { constant.set(App.prefs.userId.toString(), bundle.getString("roomId")!!) root.chat_sender.text=bundle.getString("receiver") receiver= bundle.getString("receiver")!! receiverImage = bundle.getString("receiverImage")!! //Log.e("chatIntent",bundle.getString("roomId")!!) } cAdapter = ChatAdapter(requireContext()) root.recycler_chat.adapter = cAdapter root.recycler_chat.layoutManager = LinearLayoutManager(requireContext()) root.recycler_chat.setHasFixedSize(true) //원래 채팅기록 가져오기 init_chat() //채팅 //1. STOMP init // url: ws://[도메인]/[엔드포인트]/websocket val url = constant.URL val intervalMillis = 5000L val client = OkHttpClient.Builder() .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS) .build() val stomp = StompClient(client, intervalMillis).apply { this@apply.url = url } // 2. connect stompConnection = stomp.connect().subscribe { when (it.type) { Event.Type.OPENED -> { // subscribe 채널구독 // 메세지 받아오기 Log.e("web","open") topic = stomp.join("/chat/sub/room/" + constant.CHATROOM_ID).subscribe{ stompMessage -> val result = Klaxon() .parse<Chat>(stompMessage) requireActivity().runOnUiThread { if (result != null) { //imgae cAdapter.addItem(result,receiverImage) root.recycler_chat.smoothScrollToPosition(cAdapter.itemCount) } } } // 처음 입장 try { jsonObject.put("type", "ENTER") jsonObject.put("roomId", constant.CHATROOM_ID) jsonObject.put("sender", constant.SENDER) } catch (e: JSONException) { e.printStackTrace() } stomp.send("/chat/pub/message", jsonObject.toString()).subscribe() root.send.setOnClickListener { root.chat_emoticon_group.visibility=View.GONE try { jsonObject.put("type", "TALK") jsonObject.put("roomId", constant.CHATROOM_ID) jsonObject.put("sender", constant.SENDER) jsonObject.put("message", root.message.text.toString()) } catch (e: JSONException) { e.printStackTrace() } // send stomp.send("/chat/pub/message", jsonObject.toString()).subscribe() root.message.text = null } // unsubscribe //topic.dispose() } Event.Type.CLOSED -> { } Event.Type.ERROR -> { Log.e("web","err") } } } return root } fun init_chat(){ val okHttpClient = OkHttpClient.Builder().addInterceptor(AuthInterceptor()).build() var retrofit = Retrofit.Builder() .client(okHttpClient) .baseUrl(ChatService.API_URL) .addConverterFactory(GsonConverterFactory.create()).build() var apiService = retrofit.create(ChatService::class.java) var tests = apiService.get_chatMessage(constant.CHATROOM_ID) tests.enqueue(object : Callback<chatMessageGetBody> { override fun onResponse(call: Call<chatMessageGetBody>, response: Response<chatMessageGetBody>) { if (response.isSuccessful) { var mList = response.body()!! for( i: Int in 0..mList.list.size-1) { if (mList.list[i].type != "ENTER") { cAdapter.chatDatas.add(mList.list[i]) } } } } override fun onFailure(call: Call<chatMessageGetBody>, t: Throwable) { Log.e("teamDialog", "OnFailuer+${t.message}") } }) } }
init_chat() 함수는 기존에 데이터베이스에 저장한 채팅메시지들을 불러오는 함수입니다. 기존 내용을 불러오지 않아도 되면 굳이 작성하지 않아도 됩니다.
1) stomp를 초기화하는 과정
//1. STOMP init // url: ws://[도메인]/[엔드포인트]/websocket val url = constant.URL val intervalMillis = 5000L val client = OkHttpClient.Builder() .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS) .build() val stomp = StompClient(client, intervalMillis).apply { this@apply.url = url }
2. 연결 과정
stompConnection = stomp.connect().subscribe { when (it.type) { Event.Type.OPENED -> { // subscribe 채널구독 // 메세지 받아오기 Log.e("web","open") topic = stomp.join("/chat/sub/room/" + constant.CHATROOM_ID).subscribe{ stompMessage -> val result = Klaxon() .parse<Chat>(stompMessage) requireActivity().runOnUiThread { if (result != null) { //imgae cAdapter.addItem(result,receiverImage) root.recycler_chat.smoothScrollToPosition(cAdapter.itemCount) } } } // 처음 입장 try { jsonObject.put("type", "ENTER") jsonObject.put("roomId", constant.CHATROOM_ID) jsonObject.put("sender", constant.SENDER) } catch (e: JSONException) { e.printStackTrace() } stomp.send("/chat/pub/message", jsonObject.toString()).subscribe() root.send.setOnClickListener { root.chat_emoticon_group.visibility=View.GONE try { jsonObject.put("type", "TALK") jsonObject.put("roomId", constant.CHATROOM_ID) jsonObject.put("sender", constant.SENDER) jsonObject.put("message", root.message.text.toString()) } catch (e: JSONException) { e.printStackTrace() } // send stomp.send("/chat/pub/message", jsonObject.toString()).subscribe() root.message.text = null } // unsubscribe //topic.dispose() } Event.Type.CLOSED -> { } Event.Type.ERROR -> { Log.e("web","err") } } }
Type을 open, closed, error를 구별합니다.
방을 가입하고 입장시 "ENTER"타입으로 메시지를 보내게 됩니다.
메시지를 누르면 서버가 지정한 /chat/pub/message 주소를 통해 실시간으로 통신하게 됩니다.
*roomid와 sender정보는 chatfragment 전에 chatListFramgmet를 만들어 채팅방 리스트를 가져옵니다.
리스트 중에 채팅방을 하나 클릭하면 ChatFramgent로 넘어오면서 채팅방정보와 사용자정보를 넘겨주면 됩니다.
*결과물
'Android > kotlin' 카테고리의 다른 글
[android/kotlin] recyclerview item clicklistener (0) 2022.08.28 [Android/kotlin] Bundle 사용법, Fragment와 Fragment 사이 데이터 전달, Fragment 전환 (0) 2022.08.24 [Android / kotlin] Retrofit으로 LocalDateTime형식 데이터 주고받기 (0) 2022.07.17 [Android/kotiln] jwt 토큰 임시 저장, shared preference, OkHttp3 Interceptor 사용법 (0) 2022.06.21