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 |