2023.03.16 - 안드로이드 앱개발자 과정

CHA·2023년 3월 18일
0

Android



Retrofit 실전 적용

Retrofit 을 활용하여, 사용자의 입력을 받아 서버에 보내는 작업과 DB 에 저장하는 작업 그리고 서버의 DB 로 부터 데이터를 파싱해오는 작업을 해봅시다.


activity_edit.xml 화면 설계

우선 사용자의 입력을 받기 위해서 EditText 를 만들어 주겠습니다. 작성자의 이름, 글의 제목, 내용, 가격 까지 사용자가 입력해야 하며, 버튼과 이미지 뷰를 하나 만들어 버튼을 누르면 사진을 선택하여 선택한 사진이 이미지뷰에 뜨도록 하겠습니다.

<LinearLayout ... 중략 >

    <EditText
        android:id="@+id/et_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="이름"
        android:ems="10"
        android:inputType="text"
        />
    <EditText
        android:id="@+id/et_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="제목"
        android:ems="20"
        android:inputType="text"
        />
    <EditText
        android:id="@+id/et_msg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="내용"
        android:inputType="textMultiLine"
        android:layout_weight="1"
        android:gravity="top"
        />
    <EditText
        android:id="@+id/et_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="가격"
        android:ems="10"
        android:inputType="number"
        />
    <Button
        android:id="@+id/btn_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="이미지 업로드"
        android:layout_gravity="right"/>

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/black"
        android:padding="4dp"/>

    <Button
        android:id="@+id/btn_complete"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:backgroundTint="@color/teal_700"
        android:text="작성 완료"
        />
</LinearLayout>


EditActivity.java

전체 코드

public class EditActivity extends AppCompatActivity {

    ActivityEditBinding binding;

    String imgPath = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityEditBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        getSupportActionBar().setTitle("글작성");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        binding.btnSelect.setOnClickListener(view -> clickSelect());
        binding.btnComplete.setOnClickListener(view -> clickComplete());
    }

    @Override
    public boolean onSupportNavigateUp() {
        finish();
        return super.onSupportNavigateUp();
    }

    void clickSelect(){
        Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
        launcher.launch(intent);
    }
    ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
        if(result.getResultCode() == RESULT_CANCELED) return;

        Uri uri = result.getData().getData();
        Glide.with(this).load(uri).into(binding.iv);

        imgPath = getFilePathFromUri(uri);
    });

    String getFilePathFromUri(Uri uri){
        String[] proj= {MediaStore.Images.Media.DATA};
        CursorLoader loader= new CursorLoader(this, uri, proj, null, null, null);
        Cursor cursor= loader.loadInBackground();
        int column_index= cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        String result= cursor.getString(column_index);
        cursor.close();
        return  result;
    }

    void clickComplete(){
        String name = binding.etName.getText().toString();
        String title = binding.etTitle.getText().toString();
        String message = binding.etMsg.getText().toString();
        String price = binding.etPrice.getText().toString();

        Map<String,String> dataPart = new HashMap<>();
        dataPart.put("name",name);
        dataPart.put("title",title);
        dataPart.put("msg",message);
        dataPart.put("price",price);

        MultipartBody.Part filePart = null;
        if(imgPath != null){
            File file = new File(imgPath);
            RequestBody body = RequestBody.create(MediaType.parse("image/*"),file);
            filePart = MultipartBody.Part.createFormData("img",file.getName(),body);
        }

        Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
        RetrofitService retrofitService = retrofit.create(RetrofitService.class);
        Call<String> call = retrofitService.postDataToServer(dataPart,filePart);
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {
                String s = response.body();
                Toast.makeText(EditActivity.this, s, Toast.LENGTH_SHORT).show();
                finish();
            }

            @Override
            public void onFailure(Call<String> call, Throwable t) {
                Toast.makeText(EditActivity.this, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

액션바 커스텀

getSupportActionBar().setTitle("글작성");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

public boolean onSupportNavigateUp() {
    finish();
    return super.onSupportNavigateUp();
}

사진 선택

void clickSelect(){
    Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
    launcher.launch(intent);
}

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
    if(result.getResultCode() == RESULT_CANCELED) return;

    Uri uri = result.getData().getData();
    Glide.with(this).load(uri).into(binding.iv);

    imgPath = getFilePathFromUri(uri);
});

String getFilePathFromUri(Uri uri){
    String[] proj= {MediaStore.Images.Media.DATA};
    CursorLoader loader= new CursorLoader(this, uri, proj, null, null, null);
    Cursor cursor= loader.loadInBackground();
    int column_index= cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
    cursor.moveToFirst();
    String result= cursor.getString(column_index);
    cursor.close();
    return result;
}

데이터 준비 작업

String name = binding.etName.getText().toString();
String title = binding.etTitle.getText().toString();
String message = binding.etMsg.getText().toString();
String price = binding.etPrice.getText().toString();

Map<String,String> dataPart = new HashMap<>();
dataPart.put("name",name);
dataPart.put("title",title);
dataPart.put("msg",message);
dataPart.put("price",price);

데이터를 준비할 때, Map 으로 데이터를 만든 이유는 궁극적으로 사진 파일과 텍스트를 같이 서버로 전송해주어야 하기 때문입니다.

일단 RetrofitService.java 를 봅시다.

public interface RetrofitService {

    @Multipart
    @POST("RetrofitMarket/insertDB.php")
    Call<String> postDataToServer(@PartMap Map<String,String> dataPart, @Part MultipartBody.Part filePart);

}

파일을 POST 방식으로 서버에 보내줄 때에는 @Part 어노테이션이 필요합니다. 또한 추가적으로 @Multipart 어노테이션 또한 필요합니다. 그런데 우리가 Map 방식이 아닌 단일 데이터의 전송 방식으로 보내게 되면@Query 어노테이션이 필요하죠. 또한 @FormUrlEncoded 어노테이션 또한 필요하게 됩니다. 그렇게 된다면 @FormUrlEncoded 와 @Multipart 을 중복으로 사용해주어야 하는데, 이게 불가능합니다. 그래서 Map 방식을 활용하여 데이터를 보내게 된다면, @Multipart 어노테이션 하나만 사용해도 되기 때문에 Map 방식으로 데이터를 보내주어야 합니다.


이미지 파일 감싸주기


MultipartBody.Part filePart = null;
if(imgPath != null){
    File file = new File(imgPath);
    RequestBody body = RequestBody.create(MediaType.parse("image/*"),file);
    filePart = MultipartBody.Part.createFormData("img",file.getName(),body);
}

이미지 파일을 서버에 전송하기 위해서는 그냥 바로 보낼 수 없습니다. File 객체를 이용하여 이미지 파일을 한번 감싸준 후, RequestBody 객체를 이용해 한번더 감싸줍니다. 마지막으로 MultiPartBody.Part 클래스의 createFormData 를 이용하여 감싸주면 이미지 파일을 전송할 준비가 끝납니다.


Retrofit 을 활용한 서버 전송

Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
RetrofitService retrofitService = retrofit.create(RetrofitService.class);
Call<String> call = retrofitService.postDataToServer(dataPart,filePart);

call.enqueue(new Callback<String>() {
    @Override
    public void onResponse(Call<String> call, Response<String> response) {
        String s = response.body();
        Toast.makeText(EditActivity.this, s, Toast.LENGTH_SHORT).show();
        finish();
    }

    @Override
    public void onFailure(Call<String> call, Throwable t) {
        Toast.makeText(EditActivity.this, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
    }
});

insertDB.php

<?php
    header('Content-Type:text/plain; charset=utf-8');

    $name=      $_POST['name'];
    $title=     $_POST['title'];
    $message=   $_POST['msg'];
    $price=     $_POST['price'];

    $file=      $_FILES['img'];
    $srcName=   $file['name'];   
    $tmpName=   $file['tmp_name'];

    $dstName =  "./image/".date('YmdHis').$srcName;
    move_uploaded_file($tmpName,$dstName);

    $message = addslashes($message);
    $title = addslashes($title);

    $now = date('Y-m-d H:i:s');

    $db = mysqli_connect('localhost','tjdrjs0803','dkssud109!','tjdrjs0803');
    mysqli_query($db,"set names utf8");

    $sql = "INSERT INTO market(name,title,msg,price,image,date) VALUES ( '$name' , '$title' , '$message' , '$price' , '$dstName' , '$now' )";
    $result = mysqli_query($db,$sql);

    if($result) echo "게시글이 업로드 되었습니다.";
    else echo "게시글 업로드에 실패하였습니다. 다시 시도해 주세요.";

    mysqli_close($db);
?>
  • $name= $_POST['name'];
    $_POST['name'] 의 name 은 Map 방식으로 만들어진 데이터의 키값을 넣어줘야 합니다.

  • $file= $_FILES['img'];
    @Part 로 전달된 이미지 파일의 경우 $_POST 대신 $_FILES 로 받아주어야 합니다. 그리고 $file 변수에서는 여러가지의 파일 정보를 받아올 수 있습니다. 이번에는 srcNametmpName 을 받아왔습니다.

  • move_uploaded_file($tmpName,$dstName);
    이미지 파일을 영구적으로 저장하기 위해 임시저장소에서 이동을 시켜주었습니다.

  • $message = addslashes($message);
    입력받은 데이터들 중 특수문자가 포함되어 있을 가능성이 있습니다. 다만, 특수문자의 경우 쿼리문이 SQL 에서 제대로 동작하지 않을 가능성이 있기 때문에 특수문자 앞에 슬래시를 붙여주는 작업이 필요합니다. addslashes() 를 이용하면 슬래시가 필요한 부분에 알아서 슬래시를 포함시켜줍니다. 단, 원본은 변하지 않기 때문에 원본 데이터에 다시 넣어주는 작업도 필요합니다.

Retrofit 을 이용한 데이터의 서버전송 작업은 이렇게 끝났습니다. 이제 다른 사용자가 올려진 글을 볼 수 있도록 하는 작업이 필요합니다. 그래서 일단 MainActivity 의 화면을 설계한 후, DB 에 저장된 작성글을 앱으로 보내는 서버 작업을 먼저 해둡시다.


activity_main.xml 및 리사이클러 뷰 화면 설계

Retrofit 을 적용해보는 앱이므로, 간단하게 만들어 봅시다. 메인 화면에서는 글을 보여주겠습니다. 그래서 리사이클러뷰 하나와 글쓰기 화면으로 넘어갈 FloatingActionButton 하나만 만들어 둡시다.

<RelativeLayout 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=".MainActivity">

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:orientation="vertical"/>

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_edit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:backgroundTint="#8E73CC"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:layout_margin="15dp"
        android:src="@drawable/baseline_edit_24"
        app:tint="@null"/>

</RelativeLayout>


우리는 새로운 게시글 등을 확인하고 싶을 때, 보통 위에서 아래로 스와이프 하는 동작을 하게됩니다. 그러면 화면이 새로고침 된다는 사실을 알고있으니 말이죠. 그래서 우리는 refresh 용 레이아웃을 따로 설계해주어야 합니다. 이는 별도 라이브러리 이며, 추가하면 사용할 수 있습니다. 새로고침을 하고 싶은 뷰를 감싸게끔 레이아웃을 설계해주면 됩니다. 기능 구현은 자바 코드로 추가하면 되구요.

우리는 리사이클러뷰를 통해 작성된 글들을 보여줄 예정이므로, 리사이클러뷰의 화면도 설계해봅시다.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="120dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="16dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp">

    <androidx.cardview.widget.CardView
        android:id="@+id/cv"
        android:layout_width="120dp"
        android:layout_height="match_parent"
        app:cardCornerRadius="8dp">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/penguins"
            android:scaleType="centerCrop"/>

    </androidx.cardview.widget.CardView>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/cv"
        android:text="Android Development"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:layout_marginLeft="10dp"
        android:textSize="18sp"
        android:maxLines="2"
        android:ellipsize="end"/>
  
    <TextView
        android:id="@+id/tv_msg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/cv"
        android:layout_below="@id/tv_title"
        android:text="안드로이드 앱을 만들어 드립니다."
        android:textColor="@color/black"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="2dp"
        android:textSize="12sp"
        android:maxLines="1"
        android:ellipsize="end"/>
    <TextView
        android:id="@+id/tv_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignLeft="@id/tv_msg"
        android:text="50000원"
        android:textColor="@color/black"
        android:maxLines="2"
        android:ellipsize="end"/>
    <ToggleButton
        android:id="@+id/tb_fav"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:textOn=" "
        android:textOff=" "
        android:background="@drawable/bg_fav"/>
</RelativeLayout>

추가적으로, TextView 의 ellipsize 속성은 텍스트의 길이가 maxLines 로 설정된 길이보다 길거나, 뷰의 사이즈보다 길어질 때, ... 으로 대체할 수 있는 속성입니다. 속성값으로 end 를 설정해주어 ... 의 위치를 마지막으로 설정해두었습니다.


MainActivity.java

전체 코드

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;
    MarketAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.fabEdit.setOnClickListener(view -> {
            Intent intent = new Intent(this, EditActivity.class);
            startActivity(intent);
        });

        binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                loadData();
                binding.refreshLayout.setRefreshing(false);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        loadData();
    }

    void loadData(){
        Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
        RetrofitService retrofitService = retrofit.create(RetrofitService.class);
        Call<ArrayList<MarketItem>> call = retrofitService.getDataFromServer();
        call.enqueue(new Callback<ArrayList<MarketItem>>() {
            @Override
            public void onResponse(Call<ArrayList<MarketItem>> call, Response<ArrayList<MarketItem>> response) {
                ArrayList<MarketItem> items = response.body();
                // Toast.makeText(MainActivity.this, items.size()+"", Toast.LENGTH_SHORT).show();
                adapter = new MarketAdapter(MainActivity.this,items);
                binding.recycler.setAdapter(adapter);
            }

            @Override
            public void onFailure(Call<ArrayList<MarketItem>> call, Throwable t) {
                Toast.makeText(MainActivity.this, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

글쓰기 화면으로 넘어가기

binding.fabEdit.setOnClickListener(view -> {
    Intent intent = new Intent(this, EditActivity.class);
    startActivity(intent);
});

FloatingActionButton 을 누르면 글쓰기 화면으로 넘어가는 기능을 구현했습니다.


화면 새로고침

binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        loadData();
        binding.refreshLayout.setRefreshing(false);
    }
});

화면 새로고침 기능입니다. 사용자가 아래로 스와이프를 하게 되면 onRefresh() 콜백 메서드가 호출됩니다. 그래서 메서드 내부에 DB 에서 데이터를 불러오는 기능인 loadData() 를 호출해주었습니다. 또한 setRefreshing() 을 호출하여 새로고침 아이콘이 사라지도록 설정해주었습니다. 단, loadData() 에서 내부적으로 비동기적인 방식으로 DB 에서 데이터를 읽어오므로, 데이터가 다 읽혀있지도 않은 상황에서 새로고침 아이콘이 사라질 가능성도 있긴 합니다. 그 상황을 잘 생각해서 속성을 설정해줍시다.


loadData() 구현하기

@Override
protected void onResume() {
    super.onResume();
    loadData();
}

우리는 DB 에서 데이터를 읽어와 화면에 뿌려주는 작업을 Activity 가 화면에 표시되고 난 뒤 해주겠습니다. 그래야 액티비티가 다른 화면으로 넘어갔다가 다시 돌아올 때 다시 호출이 가능하기 때문입니다.

이제 DB 에서 데이터를 가져오는 기능인 loadData() 를 구현해봅시다. 우리는 DB 에서 데이터를 읽어오기 위해서, 먼저 php 파일을 준비해야 합니다. loadDB.php 를 먼저 구현해놓읍시다.

<?php
    header('Content-Type:application/json; charset=utf-8');

    $db = mysqli_connect('localhost','tjdrjs0803','dkssud109!','tjdrjs0803');
    mysqli_query($db,"set names utf8");

    $sql = "SELECT * FROM market";
    $result = mysqli_query($db,$sql);

    $rowNum = mysqli_num_rows($result);

    $rows = array();
    for($i =0;$i<$rowNum;$i++){
        $row = mysqli_fetch_array($result,MYSQLI_ASSOC); 
        $rows[$i] = $row;
    }

    echo json_encode($rows);
    mysqli_close($db);
?>

자, 우리는 서버에서 받아올 데이터가 한개가 아닙니다. 여러개의 데이터가 DB 에 저장되어 있으며, 한 record 씩 받아올겁니다. 그래서 여러개의 데이터를 저장할 MarketItem 클래스를 만들어둡시다.

public class MarketItem {
    String no;
    String name;
    String title;
    String msg;
    String price;
    String image;
    String date;

    public MarketItem() {
    }

    public MarketItem(String no, String name, String title, String msg, String price, String image, String date) {
        this.no = no;
        this.name = name;
        this.title = title;
        this.msg = msg;
        this.price = price;
        this.image = image;
        this.date = date;
    }
}

이제 Retrofit 객체가 해야할 명세를 준비해줍시다.

-------------- RetrofitService.java


public interface RetrofitService {
... 중략
    @GET("RetrofitMarket/loadDB.php")
    Call<ArrayList<MarketItem>> getDataFromServer();
}

우리는 데이터를 여러개 받아와야 해서 MarketItem 의 객체에게 저장시켜 줍니다. 단, 우리가 받아와서 리사이클러뷰에 뿌려줄 데이터의 갯수는 한개가 아니므로, MarketItem 을 제네릭 타입으로 가지는 ArrayList 를 Retrofit 으로 부터 돌려받아야 하므로, Call 리턴타입의 제네릭으로 ArrayList<MarketItem> 을 지정해주어야 합니다.

그리고 이 ArrayList 를 뿌려줄 어댑터도 필요하겠네요. 만들어둡시다.

------------- MarketAdapter.java
public class MarketAdapter extends RecyclerView.Adapter<MarketAdapter.VH> {

    Context context;
    ArrayList<MarketItem> items;

    public MarketAdapter(Context context, ArrayList<MarketItem> items) {
        this.context = context;
        this.items = items;
    }

    @NonNull
    @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new VH(LayoutInflater.from(context).inflate(R.layout.recycler_itemview,parent,false));
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        MarketItem item = items.get(position);

        holder.binding.tvTitle.setText(item.title);
        holder.binding.tvMsg.setText(item.msg);
        holder.binding.tvPrice.setText(item.price + "원");

        String baseAddr = "";
        if(item.image != null) baseAddr = "http://tjdrjs0803.dothome.co.kr/RetrofitMarket/" + item.image;
        Glide.with(context).load(baseAddr).error(R.drawable.penguins).into(holder.binding.iv);

    }

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

    class VH extends RecyclerView.ViewHolder{
        RecyclerItemviewBinding binding;
        public VH(@NonNull View itemView) {
            super(itemView);
            binding = RecyclerItemviewBinding.bind(itemView);
        }
    }
}
  • Glide.with(context).load(baseAddr).error(R.drawable.penguins).into(holder.binding.iv);

현재 DB 에 저장되어 있는 이미지 파일의 경로는 "./image/... 으로 되어있습니다. 그런데 이 경로로는 Glide 가 이미지를 제대로 불러올 수 없습니다. 그래서 item 이 가지고 있는 image 에 이미지 파일의 앞 부분 경로를 추가시켜줘야 합니다. 또한, 이미지를 선택하지 않을 경우, 기본 이미지를 error 메서드로 지정해줄수도 있습니다.

이렇게 어댑터까지 만들었다면 이제 loadData() 메서드를 구현해봅시다.

void loadData(){
    Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
    RetrofitService retrofitService = retrofit.create(RetrofitService.class);
    Call<ArrayList<MarketItem>> call = retrofitService.getDataFromServer();
    call.enqueue(new Callback<ArrayList<MarketItem>>() {
        @Override
        public void onResponse(Call<ArrayList<MarketItem>> call, Response<ArrayList<MarketItem>> response) {
            ArrayList<MarketItem> items = response.body();
            adapter = new MarketAdapter(MainActivity.this,items);
            binding.recycler.setAdapter(adapter);
        }

        @Override
        public void onFailure(Call<ArrayList<MarketItem>> call, Throwable t) {
            Toast.makeText(MainActivity.this, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
        }
    });
}

데이터 삭제하기

리사이클러뷰의 아이템을 롱클릭했을 때, 아이템이 삭제되는 테스트를 해봅시다. 단, 그냥 삭제되는게 아니라, DB 에서 삭제를 시켜봅시다.

그러려면 일단 리사이클러뷰의 아이템을 건드려야 하기 때문에 어댑터의 뷰홀더 클래스에서 이벤트를 처리해주면 될것같습니다. 또한, DB 데이터를 삭제해야 하므로, RetrofitService 에서 Retrofit 에게 실행시킬 명세도 작성해주고, php 파일도 만들어줘야겠습니다. 그럼 일단 php 파일부터 만들어둡시다.

<?php
    header('Content-Type:text/plain; charset=utf-8');

    $no = $_GET['no'];
    $db = mysqli_connect('localhost','tjdrjs0803','dkssud109!','tjdrjs0803');
    mysqli_query($db,"set names utf8");
    $sql = "SELECT image FROM market WHERE no=$no";
    $result = mysqli_query($db,$sql);
    if($result){
        $row = mysqli_fetch_array($result,MYSQLI_ASSOC);
        $image = $row['image'];
    }

    $sql = "DELETE FROM market WHERE no = $no";
    $result = mysqli_query($db,$sql);

    if($result) {
        echo "아이템이 삭제되었습니다.";
        unlink($image);
    }
    else echo "삭제과정에서 오류발생";

    mysqli_close($db);
?>

삭제의 기준은 클릭한 아이템이므로, 서버에게 보낼 데이터는 no 값입니다. sql 쿼리문을 작성할 때, DB 의 no 값이 서버로 보내진 no 값과 동일한지 체크해서 삭제해주면 됩니다. 또한 DELETE 쿼리문으로 데이터를 삭제한 후, unlink 를 이용해 이미지 파일또한 삭제해주는 코드도 짜둡시다.

php 파일은 만들어졌으니, 명세를 만들어봅시다.

--------------- RetrofitService.java
public interface RetrofitService {
... 중략
    
    @GET("RetrofitMarket/deleteItem.php")
    Call<String> deleteItem(@Query("no") String no);
}

보내는건 @GET 방식으로 deleteItem.php 로 보내며, 보내줄 데이터는 no 값으로 단일데이터 이니, @Query 어노테이션을 이용해줍시다. 또한 받아올 데이터는 삭제의 성공여부 정도이므로 String 으로 받아옵시다.

명세까지 작성되었으니, 이제 뷰홀더 클래스 내부에 이벤트 처리를 해둡시다.

class VH extends RecyclerView.ViewHolder{
    RecyclerItemviewBinding binding;
    public VH(@NonNull View itemView) {
        super(itemView);
        binding = RecyclerItemviewBinding.bind(itemView);

        itemView.setOnLongClickListener(view -> {
            MarketItem item = items.get(getLayoutPosition());

            Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
            RetrofitService retrofitService = retrofit.create(RetrofitService.class);
            Call<String> call = retrofitService.deleteItem(item.no);
            call.enqueue(new Callback<String>() {
                @Override
                public void onResponse(Call<String> call, Response<String> response) {
                    String s = response.body();
                    Toast.makeText(context, s, Toast.LENGTH_SHORT).show();

                    items.remove(item);
                    notifyDataSetChanged();
                }

                @Override
                public void onFailure(Call<String> call, Throwable t) {
                    Toast.makeText(context, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
                }
            });
            return true;
        });
    }
}
profile
Developer

0개의 댓글