드레:
코딩 뿌시기
드레:
전체 방문자
오늘
어제
  • 분류 전체보기 (268)
    • Python (74)
      • Python 기초 (42)
      • Numpy (8)
      • Pandas (22)
    • Machine Learning (31)
      • Machine Learning (1)
      • Deep Learning (27)
    • AWS (22)
      • RDS (3)
      • EC2 (9)
      • Lambda (8)
      • S3 (2)
    • MySQL (24)
    • Git (8)
    • Streamlit (12)
    • REST API (22)
    • Java (24)
    • Android (36)
    • Debugging (15)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • JWT
  • API
  • 안드로이드 스튜디오
  • pandas
  • GET
  • Ann
  • 서버리스
  • tensorflow
  • fine tuning
  • EC2
  • 깃이그노어
  • volley
  • github
  • Java
  • Streamlit
  • rest api
  • Callback
  • Transfer Learning
  • flask
  • AWS Lambda
  • 딥러닝
  • CNN
  • AWS
  • Retrofit2
  • 액션바
  • Python
  • aws s3
  • serverless
  • Lambda
  • 네이버 API

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
드레:

코딩 뿌시기

Android Studio - Retrofit2를 이용해 API 서버에 요청하기(GET) + RecyclerVeiw 에 표시
Android

Android Studio - Retrofit2를 이용해 API 서버에 요청하기(GET) + RecyclerVeiw 에 표시

2023. 2. 13. 18:16

1. Retrofit2 라이브러리

  • HTTP 통신 라이브러리 중 Volley와 함께 가장 많이 사용되는 대표적인 라이브러리
  • 동일 Squareup사의 OkHttp 라이브러리의 상위 구현체
    - Retrofit은 OkHttp를 네트워크 계층으로 활용하고 그 위에 구축 됨
  • Volley는 response로 받은 JSON 파싱을 직접 해줘야 해서 코딩이 길어지는 반면,
    Retrofit은 자동으로 파싱을 해줘서 더 간단하게 사용할 수 있다.

 

 

2. Retrofit2 사용을 위한 설정

 

인터넷 사용 권한 설정

  • AndroidManifest.xml에 추가해준다.
  • 안드로이드 에뮬레이터에서 인터넷을 사용하기 위한 설정은 이 포스팅을 참고
<uses-permission android:name="android.permission.INTERNET" />

 

dependencies 추가

  • build.gradle(Module)에 추가해준다.
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
}

 

 

Retrofit 객체를 생성하는 함수

  • 복사 붙여넣기로 사용할 템플릿
  • 유지보수를 위해 api 패키지를 만들어 그 안에 작성한다.
import android.content.Context;

import com.reodinas2.memoapp.config.Config;

import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class NetworkClient {

    public static Retrofit retrofit;

    public static Retrofit getRetrofitClient(Context context){
        if (retrofit == null){
            // 통신 로그 확인할 때 필요한 코드
            HttpLoggingInterceptor loggingInterceptor =
                    new HttpLoggingInterceptor();
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); // 커스터마이징 할 부분(로그레벨)

            // 네트워크 연결 관련 코드
            OkHttpClient httpClient = new OkHttpClient.Builder()
                    .connectTimeout(1, TimeUnit.MINUTES)
                    .readTimeout(1, TimeUnit.MINUTES)
                    .writeTimeout(1, TimeUnit.MINUTES)
                    .addInterceptor(loggingInterceptor)
                    .build();

            // 네트워크로 데이터를 보내고 받는
            // 레트로핏 라이브러리 관련 코드
            retrofit = new Retrofit.Builder()
                    .baseUrl(Config.DOMAIN)  // 커스터마이징 할 부분(요청 도메인)
                    .client(httpClient)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

        }
        return retrofit;

    }
}

 

 

 

3. API 명세서를 보고 모델, 인터페이스 만들기

 

아래와 같이 요청하고 응답받는 내 메모 리스트를 가져오는 API 서버에 요청할 것이다.

요청할 때 아래 쿼리스트링과, 추가로 헤더에 Authorization 토큰을 전달한다.

 

 

3-1. Request 할 때 데이터를 담아서 보낼 클래스를 만든다.

  • 메모의 정보를 묶어서 처리하는 Memo 클래스는 아래와 같다.
  • 이번 포스팅 주제는 GET 방식이라 Body에 데이터를 보내지 않지만, Response를 받을 때 사용된다.
package com.reodinas2.memoapp.model;

import java.io.Serializable;

public class Memo implements Serializable {

    private int id;
    private String title;
    private String datetime;
    private String content;
    private String createdAt;
    private String updatedAt;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDatetime() {
        return datetime;
    }

    public void setDatetime(String datetime) {
        this.datetime = datetime;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(String createdAt) {
        this.createdAt = createdAt;
    }

    public String getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(String updatedAt) {
        this.updatedAt = updatedAt;
    }
}

 

 

3-2. Response 받을 때 데이터를 담아서 전달 받을 클래스를 만든다.

  • 유지보수를 위해 model 패키지를 만들어 그 안에 작성한다.
import java.io.Serializable;
import java.util.List;

public class MemoList implements Serializable {

    private String result;
    private List<Memo> items;
    private int count;


    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public List<Memo> getItems() {
        return items;
    }

    public void setItems(List<Memo> items) {
        this.items = items;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

 

 

3-3. Retrofit이 처리할 수 있도록 API 인터페이스를 만든다.

  • 유지보수를 위해 api 패키지를 만들어 그 안에 작성한다.
public interface MemoApi {

    // 내 메모 가져오는 API
    @GET("/memo")
    Call<MemoList> getMemoList(@Header("Authorization") String token,
                               @Query("offset") int offset,
                               @Query("limit") int limit);

    // 메모 생성 API
    @POST("/memo")
    Call<Res> addMemo(@Header("Authorization") String token,
                      @Body Memo memo);

    // 메모 수정 API
    @PUT("/memo/{memoId}")
    Call<Res> updateMemo(@Path("memoId") int memoId,
                         @Header("Authorization") String token,
                         @Body Memo memo);

    // 메모 삭제 API
    @DELETE("/memo/{memoId}")
    Call<Res> deleteMemo(@Path("memoId") int memoId,
                         @Header("Authorization") String token);
}

 

 

4. Retrofit을 사용해 API 서버에 요청, 응답

  • 코드 가독성을 위해 메소드로 만들어서 호출
private void getNetworkData() {
        // 네트워크에서 받아오는 동안 프로그레스바 표시
        progressBar.setVisibility(View.VISIBLE);

        Retrofit retrofit = NetworkClient.getRetrofitClient(MainActivity.this);

        MemoApi api = retrofit.create(MemoApi.class);

        // 오프셋 초기화는, API 호출하기 전에!!
        offset = 0;
        count = 0;

        Call<MemoList> call = api.getMemoList("Bearer "+accessToken, offset, limit);

        call.enqueue(new Callback<MemoList>() {
            @Override
            public void onResponse(Call<MemoList> call, Response<MemoList> response) {
                // response를 받았으니 프로그레스바 숨김
                progressBar.setVisibility(View.GONE);

                // getNetworkData 함수는, 항상 처음에 데이터를 가져오는 동작이므로
                // 초기화 코드가 필요.
                memoArrayList.clear();

                if(response.isSuccessful()){

                    // 정상적으로 데이터 받았으니, 리사이클러뷰에 표시
                    MemoList memoList = response.body();

                    memoArrayList.addAll(memoList.getItems());

                    adapter = new MemoAdapter(MainActivity.this, memoArrayList);
                    recyclerView.setAdapter(adapter);

                    // 오프셋 처리하는 코드
                    count = memoList.getCount();
                    offset += count;

                }else{
                    Toast.makeText(MainActivity.this, "서버에 문제가 있습니다.", Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            @Override
            public void onFailure(Call<MemoList> call, Throwable t) {
                // response를 받았으니 프로그레스바 숨김
                progressBar.setVisibility(View.GONE);
            }
        });

    }

 

 

 

5. 전체 코드

 

MainActivity.java

package com.reodinas2.memoapp;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.reodinas2.memoapp.adapter.MemoAdapter;
import com.reodinas2.memoapp.api.MemoApi;
import com.reodinas2.memoapp.api.NetworkClient;
import com.reodinas2.memoapp.api.UserApi;
import com.reodinas2.memoapp.config.Config;
import com.reodinas2.memoapp.model.Memo;
import com.reodinas2.memoapp.model.MemoList;
import com.reodinas2.memoapp.model.Res;
import com.reodinas2.memoapp.model.UserRes;

import java.util.ArrayList;

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


public class MainActivity extends AppCompatActivity {

    Button btnAdd;
    ProgressBar progressBar;

    RecyclerView recyclerView;
    MemoAdapter adapter;
    ArrayList<Memo> memoArrayList = new ArrayList<>();

    String accessToken;
    private ProgressDialog dialog;

    // 페이징 처리를 위한 변수
    int offset = 0;
    int limit = 7;
    int count = 0;


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

        // 억세스토큰이 저장되어 있으면,
        // 로그인한 유저이므로 메인액티비티를 실행하고,

        // 그렇지 않으면,
        // 회원가입 액티비티를 실행하고 메인액티비티는 종료!

        SharedPreferences sp = getSharedPreferences(Config.SP_NAME, MODE_PRIVATE);
        accessToken = sp.getString(Config.ACCESS_TOKEN, "");

        if (accessToken.isEmpty()){

            Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
            startActivity(intent);
            finish();
            return;
        }

        // 회원가입/로그인 유저면, 아래 코드를 실행하도록 둔다.
        btnAdd = findViewById(R.id.btnAdd);
        progressBar = findViewById(R.id.progressBar);

        recyclerView = findViewById(R.id.recyclerView);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
        //리사이클러뷰 페이징 처리
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int lastPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findLastCompletelyVisibleItemPosition();
                int totalCount = recyclerView.getAdapter().getItemCount();
                if(lastPosition+1 == totalCount){
                    // 네트워크 통해서 데이터를 더 불러온다.
                    if(count == limit){
                        addNetworkData();
                    }
                }

            }
        });


        btnAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, AddActivity.class);
                startActivity(intent);
            }
        });

    }


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

        // 네트워크로부터 내 메모를 가져온다.
        getNetworkData();
    }


    private void getNetworkData() {
        progressBar.setVisibility(View.VISIBLE);

        Retrofit retrofit = NetworkClient.getRetrofitClient(MainActivity.this);

        MemoApi api = retrofit.create(MemoApi.class);

        // 오프셋 초기화는, 함수 호출하기 전에!!
        offset = 0;
        count = 0;

        Call<MemoList> call = api.getMemoList("Bearer "+accessToken, offset, limit);

        call.enqueue(new Callback<MemoList>() {
            @Override
            public void onResponse(Call<MemoList> call, Response<MemoList> response) {
                progressBar.setVisibility(View.GONE);

                // getNetworkData 함수는, 항상 처음에 데이터를 가져오는 동작이므로
                // 초기화 코드가 필요.
                memoArrayList.clear();

                if(response.isSuccessful()){

                    // 정상적으로 데이터 받았으니, 리사이클러뷰에 표시
                    MemoList memoList = response.body();

                    memoArrayList.addAll(memoList.getItems());

                    adapter = new MemoAdapter(MainActivity.this, memoArrayList);
                    recyclerView.setAdapter(adapter);

                    // 오프셋 처리하는 코드
                    count = memoList.getCount();
                    offset += count;

                }else{
                    Toast.makeText(MainActivity.this, "서버에 문제가 있습니다.", Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            @Override
            public void onFailure(Call<MemoList> call, Throwable t) {
                progressBar.setVisibility(View.GONE);
            }
        });

    }
    
    private void addNetworkData() {

        progressBar.setVisibility(View.VISIBLE);

        Retrofit retrofit = NetworkClient.getRetrofitClient(MainActivity.this);

        MemoApi api = retrofit.create(MemoApi.class);

        Call<MemoList> call = api.getMemoList("Bearer " + accessToken, offset, limit);

        call.enqueue(new Callback<MemoList>() {
            @Override
            public void onResponse(Call<MemoList> call, Response<MemoList> response) {
                progressBar.setVisibility(View.GONE);

                if (response.isSuccessful()) {

                    // 정상적으로 데이터 받았으니, 리사이클러뷰에 표시
                    MemoList memoList = response.body();

                    memoArrayList.addAll(memoList.getItems());

                    adapter.notifyDataSetChanged();

                    // 오프셋 코드 처리
                    count = memoList.getCount();
                    offset = offset + count;

                } else {
                    Toast.makeText(MainActivity.this, "서버에 문제가 있습니다.", Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            @Override
            public void onFailure(Call<MemoList> call, Throwable t) {
                progressBar.setVisibility(View.GONE);
            }
        });
    }
}

 

 

MemoAdapter.java

package com.reodinas2.memoapp.adapter;

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;

import com.reodinas2.memoapp.EditActivity;
import com.reodinas2.memoapp.MainActivity;
import com.reodinas2.memoapp.R;
import com.reodinas2.memoapp.model.Memo;

import java.util.ArrayList;

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

    Context context;
    ArrayList<Memo> memoArrayList;

    public MemoAdapter(Context context, ArrayList<Memo> memoList) {
        this.context = context;
        this.memoArrayList = memoList;
    }

    @NonNull
    @Override
    public MemoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.memo_row, parent, false);
        return new MemoAdapter.ViewHolder(view) ;
    }

    @Override
    public void onBindViewHolder(@NonNull MemoAdapter.ViewHolder holder, int position) {
        Memo memo = memoArrayList.get(position);

        holder.txtTitle.setText(memo.getTitle());
        // "datetime": "2023-01-19T12:00:00"
        // => "2023-01-19 12:00"
        String date = memo.getDatetime().replace("T", " ").substring(0, 15+1);
        holder.txtDatetime.setText(date);
        holder.txtContent.setText(memo.getContent());

    }

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

    public class ViewHolder extends RecyclerView.ViewHolder{

        TextView txtTitle;
        TextView txtDatetime;
        TextView txtContent;
        ImageView imgDelete;
        CardView cardView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);

            txtTitle = itemView.findViewById(R.id.txtTitle);
            txtDatetime = itemView.findViewById(R.id.txtDatetime);
            txtContent = itemView.findViewById(R.id.editContent);
            imgDelete = itemView.findViewById(R.id.imgDelete);
            cardView = itemView.findViewById(R.id.cardView);


        }
    }
}

 

 

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"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <Button
            android:id="@+id/btnAdd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_margin="10dp"
            android:text="메모 생성"
            android:textSize="26sp" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@id/btnAdd"
            android:layout_margin="10dp" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:visibility="gone" />
    </RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

memo_row.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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <androidx.cardview.widget.CardView
        android:id="@+id/cardView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="7dp"
        android:layout_marginRight="15dp"
        android:layout_marginBottom="7dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="10dp"
            android:orientation="horizontal">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="4"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/txtTitle"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="제목"
                    android:textSize="24sp" />

                <TextView
                    android:id="@+id/txtDatetime"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="15dp"
                    android:text="날짜"
                    android:textSize="24sp" />

                <TextView
                    android:id="@+id/editContent"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="15dp"
                    android:text="내용"
                    android:textSize="24sp" />

            </LinearLayout>

            <ImageView
                android:id="@+id/imgDelete"
                android:layout_width="40dp"
                android:layout_height="40dp"
                app:srcCompat="@drawable/baseline_clear_24" />
        </LinearLayout>

    </androidx.cardview.widget.CardView>
</LinearLayout>

 

 

실행결과

'Android' 카테고리의 다른 글

Android Studio - 카메라 or 갤러리에서 사진 가져오기 Cheet Sheet  (0) 2023.02.17
Android Studio - DatePickerDialog/TimePickerDialog 사용법  (0) 2023.02.17
Android Studio - Retrofit2를 이용해 API 서버에 요청하기(POST)  (1) 2023.02.09
Android Studio - Volley 라이브러리 Body와 Header에 데이터 담아서 Request 하는 법  (0) 2023.02.08
Android Studio - RecyclerView 페이징 처리 (마지막까지 스크롤 됐을 때, 이벤트 처리)  (0) 2023.02.08
    'Android' 카테고리의 다른 글
    • Android Studio - 카메라 or 갤러리에서 사진 가져오기 Cheet Sheet
    • Android Studio - DatePickerDialog/TimePickerDialog 사용법
    • Android Studio - Retrofit2를 이용해 API 서버에 요청하기(POST)
    • Android Studio - Volley 라이브러리 Body와 Header에 데이터 담아서 Request 하는 법
    드레:
    드레:

    티스토리툴바