[Android] 채팅 어플 만들기
소켓 통신을 이용한 채팅 어플을 만들어보자.
<소스코드>
<MainActivity java>
import android.database.sqlite.SQLiteDatabase;
import android.icu.util.Output;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static kr.ac.cnu.computer.week12.MessageType.LEFT_CONTENTS;
public class MainActivity extends AppCompatActivity {
private DBHelper dbHelper;
private Handler mHandler;
int portNumber = 8888;
Socket sock;
EditText editText;
String message = "" ; // 입력한 메세지를 담을 변수 message
List<Message> list;
RecyclerView recyclerview;
MessageAdapter adapter;
String read;
char[] msgfromserver = new char[9999];
/*
문제에 맞게 onCreate 메소드를 정의하시오.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new Handler();
dbHelper = new DBHelper(this, 1);
list = dbHelper.selectAll(); // 싹다 가져오자. 실행할 때는 싹다 가져와서 보여줘야 함
recyclerview = findViewById(R.id.recyclerView);
adapter = new MessageAdapter(list);
recyclerview.setAdapter(adapter);
// 소켓연결스레드
class socketRunnable implements Runnable{
@Override
public void run(){
try{
sock = new Socket("chopper.kim",portNumber);
if(sock.isConnected()){
Log.d("연결","됐음");
}
InputStream input = sock.getInputStream();
Log.d("연결1","됐음1");
BufferedReader bfr = new BufferedReader(new InputStreamReader(sock.getInputStream()));
while(true){
read = bfr.readLine();
long id = dbHelper.insert(read,LEFT_CONTENTS);
list.add(dbHelper.selectOne(id));
////////
mHandler.post(new updating());
}
}catch(Exception e){
e.printStackTrace();
}
}
}
socketRunnable sr = new socketRunnable();
Thread sr1 = new Thread(sr);
sr1.start();
//소켓연결스레드끝
// 여기부터는 버튼 누르는 부분임
Button send = findViewById(R.id.sendButton);
send.setEnabled(false);
editText = findViewById(R.id.contentsEdit);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
message = editText.getText().toString();
}
@Override
public void afterTextChanged(Editable editable) {
if (editable.toString().length() > 0) {
send.setEnabled(true);
} else {
send.setEnabled(false);
}
}
});
// 여기까지 버튼 누르는 부분임
recyclerview.smoothScrollToPosition(list.size());
} // onCreate의 끝
class updating implements Runnable{
public void run(){
adapter.notifyDataSetChanged();
recyclerview.smoothScrollToPosition(list.size());
}
}
/*
전송 버튼을 누를 때 동작 되는 메소드
이 메소드 내용 작성
*/
public void sendAction(View view) {
MessageType type = MessageType.of(2);
String data = editText.getText().toString();
long id = dbHelper.insert(data,type); // 어차피 보내는 타입밖에 없음 db에 메세지 등록
// db에 저장한 id값을 담을 변수인 id
list.add(dbHelper.selectOne(id)); // db에서 꺼내오고 리스트에 담아주자.
adapter.notifyDataSetChanged();
recyclerview.smoothScrollToPosition(list.size());
//소켓스레드시작
class sockRunnable implements Runnable{
@Override
public void run(){
try{
sock = new Socket("chopper.kim",portNumber);
if(sock.isConnected()){
Log.d("연결1","됐음1");
}
BufferedWriter bfw = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream()));
bfw.write(data);
bfw.flush();
}catch(Exception e){
e.printStackTrace();
}
}
}
sockRunnable sr = new sockRunnable();
Thread sr1 = new Thread(sr);
sr1.start();
//소켓스레드끝
editText.setText(null); // 메세지 지우기
}
}
<dbHelper java>
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class DBHelper extends SQLiteOpenHelper {
public DBHelper(@Nullable Context context, int version) {
super(context, "chatting.db", null, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS MESSAGE (ID INTEGER PRIMARY KEY AUTOINCREMENT, CONTENTS TEXT, REGISTER_DATE TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), MESSAGE_TYPE INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS MESSAGE");
onCreate(db);
}
public Message selectOne(long insertedId) {
Message message = null;
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query("message", new String[]{"id", "contents", "register_date", "message_type"}, "id = ?", new String[]{String.valueOf(insertedId)}, null, null, null);
// 하나의 메시지만 가져오기 때문에 while이 아니라 if문을 사용하여 가져올 데이터 있으면 가져와서 message를 리턴하고, 없으면 null을 리턴한다.
if (cursor.moveToNext()) {
int id = cursor.getInt(0);
String contents = cursor.getString(1);
String registerDate = cursor.getString(2);
int code = cursor.getInt(3);
message = new Message(id, contents, registerDate, MessageType.of(code));
}
return message;
}
/*
selectOne 메소드를 참고하여 아래 메소드를 완성하시오.
이거 밑에 작성하셈
*/
public List<Message> selectAll() { // 아이디값 없이 전부를 가져옴.. 처음 시작할 때 사용하게 된다.
List<Message> list = new ArrayList<>(); // 전부 가져와야 하니까 배열 선언
SQLiteDatabase db = getReadableDatabase();// 위에 참조해서..
Cursor cursor = db.query("message",new String[]{"id","contents","register_date","message_type"},null,null,null,null,"register_date"); // 위에 참조
while(cursor.moveToNext()){
int id = cursor.getInt(0);
String contents = cursor.getString(1);
String registerDate = cursor.getString(2);
int code = cursor.getInt(3);
list.add(new Message(id,contents,registerDate,MessageType.of(code)));
}
return list;
}
/**
* 메시지를 등록한다.
* @param contents 등록할 메시지
*/
public long insert(String contents, MessageType type) { // 메세지를 가져오는 메서드라고 생각하기. 이걸로 접근
SQLiteDatabase db = getWritableDatabase();
// db.execSQL("INSERT INTO MESSAGE (CONTENTS) VALUES ('" + contents + "')");
ContentValues values = new ContentValues();
values.put("contents", contents);
values.put("message_type", type.getCode());
// 57번 라인과 62번 라인은 같은 동작을 한다.
return db.insert("message", null, values);
}
/**
* 메시지 아이디에 해당하는 데이터를 삭제한다.
* @param id 삭제할 아이디
*/
public void delete(int id) {
SQLiteDatabase db = getWritableDatabase();
// db.execSQL("DELETE FROM MESSAGE WHERE ID = " + id);
// 71번 라인과 73번 라인은 가능 동작을 한다.
db.delete("message", "id = ?", new String[]{String.valueOf(id)});
}
}
<Message java>
// 메세지 데이터를 저장하는 객체(Value Object)
public class Message {
private int id;
private String message;
private String registerDate;
private MessageType messageType;
public Message(String message) {
this.message = message;
}
public Message(int id, String message, String registerDate, MessageType messageType) {
this.id = id;
this.message = message;
this.registerDate = registerDate;
this.messageType = messageType;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getRegisterDate() {
return registerDate;
}
public void setRegisterDate(String registerDate) {
this.registerDate = registerDate;
}
public MessageType getMessageType() {
return messageType;
}
public void setMessageType(MessageType messageType) {
this.messageType = messageType;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", message='" + message + '\'' +
", registerDate='" + registerDate + '\'' +
", messageType=" + messageType +
'}';
}
}
<MessageAdapter java>
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
// 여기는 메세지 어댑터임
private List<Message> list;
/*
위의 list 변수에는 null이 초기화되어 있으므로 list에 값을 넣어줄 수 있는 생성자나 메소드 정의해야 함
안에 넣을 수 있어야 할텐데...
*/
// 생성자로 메세지리스트를 가져오자.
MessageAdapter(List<Message> messagelist) {
list = messagelist;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 뷰홀더의 온크리에이트 뷰홀더임
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (Objects.requireNonNull(MessageType.of(viewType))) {
case LEFT_CONTENTS:
return new LeftViewHolder(inflater.inflate(R.layout.message_left_item, parent, false));
case RIGHT_CONTENTS:
return new RightViewHolder(inflater.inflate(R.layout.message_right_item, parent, false));
default:
return new CenterViewHolder(inflater.inflate(R.layout.message_center_item, parent, false));
}
}
/*
이 메소드를 구현하시오.
*/
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
// 여기는 바인드뷰홀더임 어차피 right만 쓰니까 right만 가져오자
if(holder instanceof RightViewHolder) {
((RightViewHolder) holder).messageText.setText((list.get(position).getMessage()));
((RightViewHolder) holder).datetimeText.setText(list.get(position).getRegisterDate());
}else if(holder instanceof LeftViewHolder){
((LeftViewHolder) holder).messageText.setText((list.get(position).getMessage()));
((LeftViewHolder) holder).datetimeText.setText(list.get(position).getRegisterDate());
}
}
/*
이 메소드를 구현하시오.
*/
@Override
public int getItemCount() {
return list.size();
}
// 이 메소드를 재정의 하면 onCreateViewHolder 메소드의 두번째 파라미터 viewType 변수에 이 메소드의 리턴값이 들어간다.
@Override
public int getItemViewType(int position) {
// 해당 메시지의 message_type을 리턴
return list.get(position).getMessageType().getCode();
}
private class RightViewHolder extends RecyclerView.ViewHolder {
//뷰홀더 ㅇㅇ... right만 쓰니까 right 만 ㅇㅇ..
private final TextView messageText;
private final TextView datetimeText;
public RightViewHolder(@NonNull View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.messageText);
datetimeText = itemView.findViewById(R.id.datetimeText);
}
}
private class LeftViewHolder extends RecyclerView.ViewHolder{
private final TextView messageText;
private final TextView datetimeText;
public LeftViewHolder(@NonNull View itemView){
super(itemView);
messageText = itemView.findViewById(R.id.messageText);
datetimeText = itemView.findViewById(R.id.datetimeText);
}
}
private class CenterViewHolder extends RecyclerView.ViewHolder{
private final TextView datetimeText;
public CenterViewHolder(@NonNull View itemView){
super(itemView);
datetimeText = itemView.findViewById(R.id.datetimeText);
}
}
public void update(List li){
list = li;
notifyDataSetChanged();
}
}
<MessageType java>
public enum MessageType {
NONE(-1), LEFT_CONTENTS(0), CENTER_CONTENTS(1), RIGHT_CONTENTS(2);
private int code;
MessageType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public static MessageType of(int code) {
MessageType[] types = MessageType.values();
for (MessageType type : types) {
if (type.getCode() == code) {
return type;
}
}
return NONE;
}
}
<activity_main xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/recyclerView"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/contentsEdit" app:layout_constraintHorizontal_bias="0.0"/>
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/contentsEdit"
app:layout_constraintTop_toBottomOf="@id/recyclerView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/sendButton"/>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/sendButton"
android:text="전송"
android:enabled="false"
android:onClick="sendAction"
app:layout_constraintTop_toBottomOf="@id/recyclerView"
app:layout_constraintStart_toEndOf="@id/contentsEdit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<message_center_item xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/datetimeText"
android:textStyle="bold"
android:padding="4dp"
android:textSize="16sp"
android:text="2021.11.29"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<message_left_item xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/cardView"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="#FFF59D"
android:layout_marginRight="128dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/datetimeText">
<TextView android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/messageText"
android:text="안녕하세요."
android:padding="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.cardview.widget.CardView>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/datetimeText"
android:textSize="12sp"
android:layout_marginRight="128dp"
android:text="2021.11.29 16:32:12"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/cardView"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<message_right_item xml>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/cardView"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="#90CAF9"
android:layout_marginLeft="128dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/datetimeText">
<TextView android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/messageText"
android:text="안녕하세요."
android:padding="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.cardview.widget.CardView>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/datetimeText"
android:textSize="12sp"
android:text="2021.11.29 16:32:12"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/cardView"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
SelectOne메서드는 하나의 메시지만 가져와서 데이터베이스에 저장하고, SelectAll메서드는 데이터베이스에 있는 모든 메시지들을 가져와서 저장한다.
while문으로 가져올 데이터가 없을 때 까지 반복해서 데이터를 가져오도록 했다.
상대방이 보낸 메시지는 왼쪽, 날짜 정보는 가운데, 내가 보낸 메시지는 오른쪽으로 지정하고 각각의 메시지 타입에 숫자를 붙여서 메시지를 구분했다.
onbindviewholder로 화면에 담을 수 없는 채팅을 스크롤해서 확인할 수 있도록 했다.
리스트를 채우기 위해 생성자로 리스트를 입력받으면 그 리스트의 값으로 리스트를 채우도록 했다.
메시지를 입력했을 때만 전송 버튼을 누를 수 있도록 textchanged메서드를 구현했다.
처음 실행됐을 때는 데이터베이스에 있는 정보들을 화면에 로드해야 한다. 데이터베이스의 selectall 메서드를 사용해 리사이클러뷰에 저장하고, 리사이클러뷰와 어댑터를 통해 화면에 정보를 나타낼 수 있도록 했다.
전송 버튼을 눌렀을 때는 메시지에 담긴 정보를 db에 저장하고, 메시지를 화면에 보여줘야 한다. 데이터베이스에서 저장한 값을 아이디를 통해 리스트에 저장하고 어댑터를 불러온다.
리사이클러뷰와 어댑터를 활용해 내가 보낸 메시지가 지금까지 보낸 메시지의 밑에 보여줄 수 있도록 코드를 작성했다.
smoothscrolltoposition 메서드를 통해서 메시지가 많이 쌓여있을 때도 맨 밑으로 바로 갈 수 있도록 했다.
확실히 코딩 공부는 서비스를 직접 만들어 보는 공부가 큰 도움이 된다.
"채팅 어플이라는 서비스를 만들자"라는 목표를 설정하고 이루기 위해 소켓통신, 데이터베이스, 레이아웃에 대한 개념을 능동적으로 학습할 수 있었다.
'Mobile > Android' 카테고리의 다른 글
[Android] 네트워킹 2 (0) | 2021.12.13 |
---|---|
[Android] 네트워킹 1 (0) | 2021.12.05 |
[Android] 내용 제공자 (Content Provider) (0) | 2021.11.29 |
[Android] 모바일 데이터베이스 (0) | 2021.11.21 |
[Android] 음악 재생 플레이어 만들기 (0) | 2021.11.21 |
댓글
이 글 공유하기
다른 글
-
[Android] 네트워킹 2
[Android] 네트워킹 2
2021.12.13 -
[Android] 네트워킹 1
[Android] 네트워킹 1
2021.12.05 -
[Android] 내용 제공자 (Content Provider)
[Android] 내용 제공자 (Content Provider)
2021.11.29 -
[Android] 모바일 데이터베이스
[Android] 모바일 데이터베이스
2021.11.21