IT/Spring

[안드로이드 앱 서버 만들기] 5. 안드로이드 앱과 서버 통신

김 정 환 2021. 7. 7. 15:27
반응형
게시된 내용은 작성자가 공부한 내용을 정리하여 기록하였습니다. 일부 빠지거나 부족한 부분이 있을 수 있습니다. 최대한 편집 없이 기록하였습니다.

출처 블로그

 

 

사용 도구 : STS 4 (Spring Tools Suite), AWS EC2, MariaDB, Windows, Android Studio


아래와 같은 앱을 만들겠습니다.

이름, 나이, 주소를 넣고 ADD를 누르면 DB에 추가 됩니다.

수정을 눌러서 이름, 나이, 주소를 수정합니다.

삭제를 눌러서 데이터를 삭제합니다.

 

 

 


1. 안드로이드 프로젝트 생성

안드로이드 프로젝트를 생성하겠습니다. Empty Activity를 선택합니다. 

그리고 프로젝트 이름을 정하고, Java를 선택합니다.

 

 

 


2. 설정 추가

Lombok, RecyclerView, Retrofit, Gson 라이브러리를 추가합니다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    // Lombok
    compileOnly 'org.projectlombok:lombok:1.18.8'
    annotationProcessor 'org.projectlombok:lombok:1.18.8'

    // RecyclerView
    implementation "androidx.recyclerview:recyclerview:1.1.0"

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

    // Gson
    implementation 'com.google.code.gson:gson:2.8.5'
}

 

 

 

AndroidManifest.xml에 인터넷 권한을 추가합니다.

<uses-permission android:name="android.permission.INTERNET"/>

 

 

 


3.  레이아웃 만들기

activity_main.xml

<?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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/et_name"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:hint="NAME"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_age"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:hint="AGE"
        app:layout_constraintStart_toEndOf="@+id/et_name"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_address"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:hint="ADDRESS"
        app:layout_constraintStart_toEndOf="@+id/et_age"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:layout_width="69dp"
        android:layout_height="46dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="Add"
        android:id="@+id/btn_add"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="68dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="MemberList"
        android:textColor="#000"
        android:textSize="25sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/member_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

member_info.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="3dp"
    android:background="@drawable/border_shadow"
    android:orientation="vertical">


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:id="@+id/info_layout">

        <TextView
            android:id="@+id/info_id"
            android:layout_width="50dp"
            android:layout_height="40dp"
            android:text="ID"
            android:gravity="center"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/info_name"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:text="NAME"
            android:gravity="center"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/info_id"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/info_age"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:text="AGE"
            android:gravity="center"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/info_name"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/info_address"
            android:layout_width="75dp"
            android:layout_height="40dp"
            android:text="ADDRESS"
            android:gravity="center"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/info_age"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/info_created"
            android:layout_width="80dp"
            android:layout_height="40dp"
            android:text="CREATED"
            android:gravity="center"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/info_address"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/info_update"
            android:layout_width="31dp"
            android:layout_height="18dp"
            android:layout_marginTop="5dp"
            android:layout_marginEnd="4dp"
            android:layout_marginRight="4dp"
            android:background="#f0f0f0"
            android:text="수정"
            android:textSize="8sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/info_delete"
            android:layout_width="31dp"
            android:layout_height="18dp"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="4dp"
            android:layout_marginRight="4dp"
            android:background="#f0f0f0"
            android:text="삭제"
            android:textSize="8sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/info_update" />

    </androidx.constraintlayout.widget.ConstraintLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/update_layout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:visibility="gone"
        >

        <TextView
            android:id="@+id/update_id"
            android:layout_width="50dp"
            android:layout_height="40dp"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="7dp"
            android:gravity="center"
            android:text="ID"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/update_name"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="7dp"
            android:gravity="center"
            android:text="NAME"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/update_id"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/update_age"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="7dp"
            android:gravity="center"
            android:text="AGE"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/update_name"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/update_address"
            android:layout_width="75dp"
            android:layout_height="40dp"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="7dp"
            android:gravity="center"
            android:text="ADDRESS"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/update_age"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/update_created"
            android:layout_width="20dp"
            android:layout_height="40dp"
            android:layout_marginStart="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="7dp"
            android:gravity="center"
            android:text="CREATED"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/update_address"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.416" />

        <Button
            android:id="@+id/update_btn"
            android:layout_width="31dp"
            android:layout_height="35dp"
            android:layout_marginStart="4dp"
            android:layout_marginLeft="4dp"
            android:layout_marginTop="6dp"
            android:layout_marginEnd="4dp"
            android:layout_marginRight="4dp"
            android:layout_marginBottom="8dp"
            android:background="#f0f0f0"
            android:text="수정"
            android:textSize="8sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toEndOf="@+id/update_created"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</LinearLayout>

 

 

drawable 폴더에

border_shadow.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#CABBBBBB"/>
            <corners android:radius="2dp" />
        </shape>
    </item>

    <item
        android:left="1dp"
        android:right="1dp"
        android:top="1dp"
        android:bottom="2dp">
        <shape android:shape="rectangle">
            <solid android:color="@android:color/white"/>
            <corners android:radius="2dp" />
        </shape>
    </item>
</layer-list>

 

 

 


4. Object 정의

스프링 프로젝트에서 사용했던 Member 객체를 그대로 사용합니다.

Member.Java

package com.example.retrofixtest;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    private long id;
    private String name;
    private int age;
    private String address;
    private Date createdAt;

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }

    public Date getCreatedAt(){
        return createdAt;
    }

}

 

 

 


5. Interface 정의

아래 BASE_URLEC2의 퍼블릭 IP를 넣어주세요.

 

DataService.java

package com.example.retrofixtest;

import java.util.List;
import java.util.Map;

import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;

public class DataService {
    private String BASE_URL = ""; // TODO REST API 퍼블릭 IP로 변경

    Retrofit retrofitClient =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(new OkHttpClient.Builder().build())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

    SelectAPI select = retrofitClient.create(SelectAPI.class);
    InsertAPI insert = retrofitClient.create(InsertAPI.class);
    UpdateAPI update = retrofitClient.create(UpdateAPI.class);
    DeleteAPI delete = retrofitClient.create(DeleteAPI.class);
}

interface SelectAPI{
    @GET("select/{id}")
    Call<Member> selectOne(@Path("id") long id);

    @GET("select")
    Call<List<Member>> selectAll();
}
interface InsertAPI{
    @POST("insert")
    Call<Member> insertOne(@Body Map<String, String> map);
}

interface UpdateAPI{
    @POST("update/{id}")
    Call<Member> updateOne(@Path("id") long id, @Body Map<String, String> map);
}

interface DeleteAPI{
    @POST("delete/{id}")
    Call<ResponseBody> deleteOne(@Path("id") long id);
}

 

 

 


6. Adapter 정의

콜백을 객체로 받지 않는 Delete 요청 같은 경우에는 콜백 타입이 String이 아닌 ResponseBody 타입으로 받습니다.

 

MemberAdapter.java

package com.example.retrofixtest;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MemberAdapter extends RecyclerView.Adapter<MemberAdapter.ViewHolder> {

    private List<Member> data;
    private Context context;
    private DataService dataService;

    MemberAdapter(List<Member> data, Context context, DataService dataService) {
        this.data = data;
        this.context = context;
        this.dataService = dataService;
    }

    @Override
    public MemberAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        return new MemberAdapter.ViewHolder(inflater.inflate(R.layout.member_info, parent, false));
    }

    @Override
    public int getItemCount() {
        return data.size() ;
    }

    @Override
    public void onBindViewHolder(final MemberAdapter.ViewHolder holder, final int position) {
        holder.info_id.setText(String.valueOf(data.get(position).getId()));
        holder.info_name.setText(data.get(position).getName());
        holder.info_age.setText(String.valueOf(data.get(position).getAge()));
        holder.info_address.setText(data.get(position).getAddress());
        holder.info_created.setText(dateParser(data.get(position).getCreatedAt()));

        holder.info_update.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                holder.update_id.setText(String.valueOf(data.get(position).getId()));
                holder.update_name.setText(data.get(position).getName());
                holder.update_age.setText(String.valueOf(data.get(position).getAge()));
                holder.update_address.setText(data.get(position).getAddress());
                holder.update_created.setText(data.get(position).getCreatedAt().toString());

                holder.info_layout.setVisibility(View.GONE);
                holder.update_layout.setVisibility(View.VISIBLE);
            }
        });

        holder.info_delete.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                dataService.delete.deleteOne(data.get(position).getId()).enqueue(new Callback<ResponseBody>() {
                    @Override
                    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                        data.remove(position);
                        notifyItemRemoved(position);
                        Toast.makeText(context, "아이템 삭제 완료", Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onFailure(Call<ResponseBody> call, Throwable t) {
                        t.printStackTrace();
                    }
                });

            }
        });

        holder.update_btn.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                Map<String, String> map = new HashMap();
                map.put("name", holder.update_name.getText().toString());
                map.put("age", holder.update_age.getText().toString());
                map.put("address", holder.update_address.getText().toString());
                dataService.update.updateOne(data.get(position).getId(), map).enqueue(new Callback<Member>() {
                        @Override
                    public void onResponse(Call<Member> call, Response<Member> response) {
                        data.set(position, response.body());
                        notifyDataSetChanged();
                        Toast.makeText(context, "아이템 수정 완료", Toast.LENGTH_SHORT).show();
                        holder.info_layout.setVisibility(View.VISIBLE);
                        holder.update_layout.setVisibility(View.GONE);
                    }
                    @Override
                    public void onFailure(Call<Member> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
            }
        });
    }

    public String dateParser(Date date){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM/dd hh:mm");
        return simpleDateFormat.format(date);
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        ConstraintLayout info_layout, update_layout;
        TextView info_id, info_name, info_age, info_address, info_created, update_id, update_created;
        Button info_update, info_delete, update_btn;
        EditText update_name, update_age, update_address;

        ViewHolder(View itemView) {
            super(itemView);
            // 뷰 영역
            info_layout = itemView.findViewById(R.id.info_layout);
            info_id = itemView.findViewById(R.id.info_id);
            info_name = itemView.findViewById(R.id.info_name);
            info_age = itemView.findViewById(R.id.info_age);
            info_address = itemView.findViewById(R.id.info_address);
            info_created = itemView.findViewById(R.id.info_created);
            info_update = itemView.findViewById(R.id.info_update);
            info_delete = itemView.findViewById(R.id.info_delete);

            // 수정 영역
            update_layout = itemView.findViewById(R.id.update_layout);
            update_id = itemView.findViewById(R.id.update_id);
            update_name = itemView.findViewById(R.id.update_name);
            update_age = itemView.findViewById(R.id.update_age);
            update_address = itemView.findViewById(R.id.update_address);
            update_created = itemView.findViewById(R.id.update_created);
            update_btn = itemView.findViewById(R.id.update_btn);
        }
    }
}

 

 

 


7. MainActivity 정의

 

MainActivity.java

package com.example.retrofixtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {

    DataService dataService = new DataService();
    List<Member> members;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final RecyclerView member_list = findViewById(R.id.member_list);
        final EditText et_name = findViewById(R.id.et_name);
        final EditText et_age = findViewById(R.id.et_age);
        final EditText et_address = findViewById(R.id.et_address);
        member_list.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));

        dataService.select.selectAll().enqueue(new Callback<List<Member>>() {
                    @Override
                    public void onResponse(Call<List<Member>> call, Response<List<Member>> response) {
                        Log.d("test", response.body().toString());
                        members = response.body();
                        setAdapter(member_list);
            }
            @Override
            public void onFailure(Call<List<Member>> call, Throwable t) {
                t.printStackTrace();
            }
        });

        Button btn_add = findViewById(R.id.btn_add);
        btn_add.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                Map<String, String> map = new HashMap();
                map.put("name", et_name.getText().toString());
                map.put("age", et_age.getText().toString());
                map.put("address", et_address.getText().toString());

                dataService.insert.insertOne(map).enqueue(new Callback<Member>() {
                    @Override
                    public void onResponse(Call<Member> call, Response<Member> response) {
                        members.add(response.body());
                        setAdapter(member_list);
                        Toast.makeText(MainActivity.this, "유저 등록 완료", Toast.LENGTH_SHORT).show();
                        et_name.setText("");
                        et_age.setText("");
                        et_address.setText("");
                    }

                    @Override
                    public void onFailure(Call<Member> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
            }
        });
    }

    void setAdapter(RecyclerView member_list){
        member_list.setAdapter(new MemberAdapter(members, this, dataService));
    }
}

 

 

 


이렇게 안드로이드 앱에서 데이터를 추가/수정/삭제/조회할 수 있는 과정을 모두 알아봤습니다.

이렇게 배운 내용을 통해서 재활 운동 앱 서비스를 만들었습니다. 환자의 재활 상태를 DB에 저장하여 관리하고, 축적된 데이터를 활용하여 환자들이 효율적인 재활이 가능한 앱 서비스입니다.

반응형