Retrofit
REST API 통신을 위해 구현된 Squareup 사의 OkHttp 라이브러리의 상위 구현체로, OkHttp 를 네트워크 계층으로 활용하고 구축된 라이브러리.
AsyncTask 없이 Background Thread 를 실행하고 Callback 을 통해 Main Thread 에서 UI 업데이트가 가능하다.
🚨 장점
빠른 성능 - AsyncTask 를 사용하는 OkHttp 의 3배이상 차이가 난다고 한다.
가독성 - Annotation 사용으로 코드의 가독성이 뛰어남. 직관적인 설계가 가능.
간단한 구현 - HttpUrlConnection 의 Connection / Input&Output Stream / URL Encoding 등의 작업 또는 OkHttp 의 request / response 등의 작업을 라이브러리로 넘겨서 작업함.
동기/비동기 쉬운 구현 - response 를 받는 옵션으로 동기(execute()) / 비동기(enqueue(callback)) 가 있다. Background Thread 에서 request 를 수행한 후 callback 은 메인 스레드에서 처리.
🚨 구성요소
DTO - Model 클래스 사용 (JSON 타입변환에 사용)
Interface - 사용할 CRUD 동작 들을 정의해놓은 인터페이스
Retrofit.builder - Interface 를 사용할 인스턴스
🚨 Retrofit 예제 코드
안드로이드에서 서버와 통신하기 위한 retrofit 을 이용하는 예제 코드를 openweathermap 의 api 를 통해 날씨 정보를 표시해주는 앱으로 만들어 보았습니다. 🌤
https://openweathermap.org/api
작성한 파일 목록 입니다.
1. AndroidManifest.xml
2. build.gradle(:app)
3. strings.xml
4. WeatherInfoModel.java / WeatherWeatherModel.java / WeatherMainModel.java /
WeatherWindModel.java / WeatherSysModel.java / WeatherCloudsModel.java
5. APIService.java
6. APIClient.java
7. MainActivity.java
8. activity_main.xml
1. AndroidManifest.xml
인터넷 권한 을 추가해야 합니다.
<uses-permission android:name="android.permission.INTERNET"/>
를 추가해주세요!
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.eun.myapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
2. build.gradle(:app)
하단 dependencies {} 안에 gson, glide, retrofit 에 대한 라이브러리를 사용하기 위해 추가해 줍니다.
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.eun.myapp"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.android.support:design:28.0.0'
//gson
implementation 'com.google.code.gson:gson:2.8.0'
//glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
}
3. strings.xml
openweathermap.org 의 주소와 해당 사이트에서 발급받은 key값을 여기저기서 가져다 사용하기 위해 res/values/strings.xml 의 resources 에 지정해주었습니다.
<resources>
<string name="app_name">MyApp</string>
<string name="weather_url">http://api.openweathermap.org/</string>
<string name="weather_app_id">key값</string>
</resources>
4. WeatherInfoModel.java
REST API 통신 시 받아올 데이터를 사용하기 쉽도록 응답받는 데이터에 맞추어 VO 를 구현한 Weather 관련 model class 입니당
JSON 을 사용할 경우 : @SerializedName("속성명") 으로 속성명 일치시켜주면 변수명 다르게도 가능
XML 을 사용할 경우 : @Element(name="속성명") XML은 @Element 어노테이션 사용
WeatherInfoModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class WeatherInfoModel {
@SerializedName("name")
String name = ""; //도시이름
@SerializedName("weather")
List<WeatherWeatherModel> weather;
@SerializedName("main")
WeatherMainModel main;
@SerializedName("wind")
WeatherWindModel wind;
@SerializedName("sys")
WeatherSysModel sys;
@SerializedName("clouds")
WeatherCloudsModel clouds;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<WeatherWeatherModel> getWeather() {
return weather;
}
public void setWeather(List<WeatherWeatherModel> weather) {
this.weather = weather;
}
public WeatherMainModel getMain() {
return main;
}
public void setMain(WeatherMainModel main) {
this.main = main;
}
public WeatherWindModel getWind() {
return wind;
}
public void setWind(WeatherWindModel wind) {
this.wind = wind;
}
public WeatherSysModel getSys() {
return sys;
}
public void setSys(WeatherSysModel sys) {
this.sys = sys;
}
public WeatherCloudsModel getClouds() {
return clouds;
}
public void setClouds(WeatherCloudsModel clouds) {
this.clouds = clouds;
}
}
WeatherWeatherModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
public class WeatherWeatherModel {
@SerializedName("main")
String main = ""; //날씨
@SerializedName("description")
String description = ""; //상세 날씨 설명
@SerializedName("icon")
String icon = ""; //날씨 이미지
public String getMain() {
return main;
}
public void setMain(String main) {
this.main = main;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
}
WeatherMainModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
public class WeatherMainModel {
@SerializedName("temp")
double temp = 0.0; //현재온도
@SerializedName("humidity")
double humidity = 0.0; //현재습도
public double getTemp() {
return temp;
}
public void setTemp(double temp) {
this.temp = temp;
}
public double getHumidity() {
return humidity;
}
public void setHumidity(double humidity) {
this.humidity = humidity;
}
}
WeatherWindModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
public class WeatherWindModel {
@SerializedName("speed")
double speed = 0.0;
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}
WeatherSysModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
public class WeatherSysModel {
@SerializedName("country")
String country = ""; //나라
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
WeatherCloudsModel.java
package com.eun.myapp.data.retrofit.data;
import com.google.gson.annotations.SerializedName;
public class WeatherCloudsModel {
@SerializedName("all")
double all = 0.0; //구름
public double getAll() {
return all;
}
public void setAll(double all) {
this.all = all;
}
}
5. APIService.java
Http Method(GET/POST/PUT/DELETE/HEAD) 와 자원의 정보를 정의할 인터페이스를 구현합니다.
필요에 따라 변수를 포함하여 정의합니다.
@GET(EndPoint URL/{path}) ...
ex) http://eunoia3jy.tistory.com/main/123 에서 main/123 이 EndPoint URL 이 됩니다.
package com.eun.myapp.data.retrofit;
import com.eun.myapp.data.retrofit.data.WeatherInfoModel;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface APIService {
@GET("data/2.5/{path}")
Call<WeatherInfoModel> doGetJsonData(
@Path("path") String path,
@Query("q") String q,
@Query("appid") String appid
);
}
6. APIClient.java
retrofit 객체를 초기화합니다.
baseUrl() 안의 url 은 꼭 / 로 끝나야 합니다. 아니면 예외 발생이 됩니다.
package com.eun.myapp.data.retrofit;
import android.util.Log;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class APIClient {
private static final String TAG = APIClient.class.getSimpleName();
public static Retrofit getClient(String url) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
Log.d(TAG, "APIClient >> url : "+url);
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
return retrofit;
}
}
7. MainActivity.java
화면을 구성하고 네트워크 통신하는 과정을 나타냅니다.
retrofit 객체와 인터페이스를 연결하고 네트워크 통신 요청 및 응답 콜백을 구현합니다.
response 를 받는 옵션으로 동기(execute()) / 비동기(enqueue(callback)) 가 있습니다.
Background Thread 에서 request 를 수행한 후 callback 은 메인 스레드에서 처리합니다.
onResponse() 에서는 실패코드, 성공코드 모두 호출 할 수 있기 때문에 isSuccessful() 로 확인이 필요합니다.
package com.eun.myapp;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.eun.myapp.data.retrofit.APIClient;
import com.eun.myapp.data.retrofit.APIService;
import com.eun.myapp.data.retrofit.data.WeatherInfoModel;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
public static String TAG = "["+MainActivity.class.getSimpleName() +"] ";
Context context = MainActivity.this;
TextView tv_name, tv_country;
ImageView iv_weather;
TextView tv_temp, tv_main, tv_description;
TextView tv_wind, tv_cloud, tv_humidity;
APIService apiInterface = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
requestNetwork();
}
/* view 를 설정하는 메소드 */
private void initView() {
tv_name = (TextView) findViewById(R.id.tv_name);
tv_country = (TextView) findViewById(R.id.tv_country);
iv_weather = (ImageView) findViewById(R.id.iv_weather);
tv_temp = (TextView) findViewById(R.id.tv_temp);
tv_main = (TextView) findViewById(R.id.tv_main);
tv_description = (TextView) findViewById(R.id.tv_description);
tv_wind = (TextView) findViewById(R.id.tv_wind);
tv_cloud = (TextView) findViewById(R.id.tv_cloud);
tv_humidity = (TextView) findViewById(R.id.tv_humidity);
}
/* retrofit 을 통해 통신을 요청하기 위한 메소드 */
private void requestNetwork() {
//retrofit 객체와 인터페이스 연결
apiInterface = APIClient.getClient(getString(R.string.weather_url)).create(APIService.class);
//통신 요청
Call<WeatherInfoModel> call = apiInterface.doGetJsonData("weather", "seoul", getString(R.string.weather_app_id));
//응답 콜백 구현
call.enqueue(new Callback<WeatherInfoModel>() {
@Override
public void onResponse(Call<WeatherInfoModel> call, Response<WeatherInfoModel> response) {
WeatherInfoModel resource = response.body();
if(response.isSuccessful()) {
setWeatherData(resource); //UI 업데이트
} else {
showFailPop();
}
}
@Override
public void onFailure(Call<WeatherInfoModel> call, Throwable t) {
call.cancel();
showFailPop();
}
});
}
/* 통신하여 받아온 날씨 데이터를 통해 UI 업데이트 메소드 */
private void setWeatherData(WeatherInfoModel model) {
tv_name.setText(model.getName());
tv_country.setText(model.getSys().getCountry());
Glide.with(context).load(getString(R.string.weather_url)+"img/w/"+model.getWeather().get(0).getIcon()+".png") //Glide 라이브러리를 이용하여 ImageView 에 url 로 이미지 지정
.placeholder(R.drawable.icon_image)
.error(R.drawable.icon_image)
.into(iv_weather);
tv_temp.setText(doubleToStrFormat(2, model.getMain().getTemp()-273.15) + " 'C"); //소수점 2번째 자리까지 반올림하기
tv_main.setText(model.getWeather().get(0).getMain());
tv_description.setText(model.getWeather().get(0).getDescription());
tv_wind.setText(doubleToStrFormat(2, model.getWind().getSpeed()) + " m/s");
tv_cloud.setText(doubleToStrFormat(2, model.getClouds().getAll()) + " %");
tv_humidity.setText(doubleToStrFormat(2, model.getMain().getHumidity()) + " %");
}
/* 통신 실패시 AlertDialog 표시하는 메소드 */
private void showFailPop() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Title").setMessage("통신실패");
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
Toast.makeText(getApplicationContext(), "OK Click", Toast.LENGTH_SHORT).show();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
Toast.makeText(getApplicationContext(), "Cancel Click", Toast.LENGTH_SHORT).show();
}
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
/* 소수점 n번째 자리까지 반올림하기 */
private String doubleToStrFormat(int n, double value) {
return String.format("%."+n+"f", value);
}
}
8. activity_main.xml
MainActivity 에 대한 레이아웃입니다.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:background="@color/white"
tools:context=".ConnectActivity" >
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/amber_200"
android:elevation="10dp"
android:gravity="center"
android:text="현재 날씨"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/ll_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll_name"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="50dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="20dp"
android:weightSum="100"
android:orientation="horizontal"
android:background="@drawable/bg_custom_title_name"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title" >
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:layout_marginRight="2dp"
android:gravity="right|center_vertical"
android:text="Seoul"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20dp" />
<TextView
android:id="@+id/tv_country"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:layout_marginLeft="2dp"
android:gravity="left|center_vertical"
android:text="KR"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:weightSum="100"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_name" >
<ImageView
android:id="@+id/iv_weather"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="30"
android:src="@drawable/icon_image"
android:background="@drawable/bg_solid" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="70"
android:gravity="center_vertical"
android:orientation="vertical"
android:weightSum="100"
app:layout_constraintLeft_toRightOf="@+id/iv_weather"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:id="@+id/tv_temp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="35"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="12 C"
android:textColor="@color/blue_light"
android:textSize="25sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_main"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="35"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="Clear Sky"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_description"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="30"
android:gravity="center_vertical"
android:paddingLeft="25dp"
android:text="broken clouds"
android:textColor="@color/grey_500"
android:textSize="17sp" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/view_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:background="@color/grey_200"
app:layout_constraintBottom_toTopOf="@+id/ll_detail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_main" />
<LinearLayout
android:id="@+id/ll_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:weightSum="100"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view_divider" >
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:paddingTop="5dp"
android:src="@drawable/icon_wind" />
<TextView
android:id="@+id/nm_wind"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="바람 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_wind"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="4.6m/s"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:paddingTop="5dp"
android:src="@drawable/icon_cloud" />
<TextView
android:id="@+id/nm_cloud"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="구름 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_cloud"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="75%"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="33"
android:orientation="vertical"
android:weightSum="100" >
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="45"
android:gravity="center"
android:paddingTop="5dp"
android:src="@drawable/icon_humidity" />
<TextView
android:id="@+id/nm_humidity"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="15"
android:gravity="center"
android:text="습도 "
android:textColor="@color/grey_500"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_humidity"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:gravity="center_horizontal"
android:text="59%"
android:textColor="@color/black"
android:textSize="17sp" />
</LinearLayout>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
결과 화면
감사합니당!
'🤖 안드로이드 Android' 카테고리의 다른 글
[안드로이드/Android] javax.xml.bind.UnmarshalException 에러 (0) | 2022.01.09 |
---|---|
[안드로이드/Android] Context (Application Context vs. Activity Context) (0) | 2021.10.27 |
[안드로이드/Android] AsyncTask 로 HttpURLConnection 를 이용한 날씨 앱 만들기 (0) | 2021.08.29 |
[안드로이드/Android] AsyncTask 를 이용한 프로그레스바(ProgressBar) 표시하기 (0) | 2021.08.23 |
[안드로이드/Android] 안드로이드 스튜디오(Android Studio) 에서 벡터(Vector) 이미지/아이콘 추가 (1) | 2021.08.13 |