본문 바로가기

Android/Jetpack, Clean Architecture

모던 안드로이드 앱 만들기 (3) - Retrofit, RxJava를 이용한 네트워크 통신

반응형
 
 

Retrofit2, RxJava

RxJava는 asynchronous event based communications (비동기 이벤트 기반 통신)을 위한 라이브러리입니다.

기존 Retrofit 사용 시에는 주로 Call 인터페이스의 enqueue 메서드를 통해서 비동기적으로 API와 통신을 하고 이후에 onFailure와 onResponse로 통신 결과를 전달받습니다.

이번엔 Call 인터페이스 대신 비동기 태스크 처리를 위해 RxJava, 네트워크 통신을 위해 Retrofit을 사용할 것입니다.

 

retrofit2:adapter-rxjava2를 이용하면 기존의 RxJava를 사용하던 타입을 적용해 Retrofit을 통해서 API 호출과 결괏값을 처리할 수 있습니다. RxJava adapter를 추가하여 요청 결과를 RxJava의 Observable로 전달받아 원하는 형태로 쉽게 결과 값을 처리할 수 있습니다.

 

RxJava2에서 사용하는 타입

 

 Single

이 타입에 중에서 저는 Single을 사용할 것입니다.

우리가 할 것은 국가 데이터를 한 번만 가져오는 HTTP 통신을 할 것입니다. 지속적인 스트리밍 이벤트가 아닙니다. 

API 호출은 결과를 두 번 또는 여러 조각으로 반환하지 않습니다.데이터를 한 번 가져 오도록 호출하면 단일 결과만 예상됩니다.

이런 경우에 Single을 사용합니다. 의도를 더 명확하게 하기 위해 Observable 대신 Single을 사용합니다.

공식 문서에서 Single에 대해 자세히 설명해줍니다. 

reactivex.io/documentation/single.html

 

ReactiveX - Single

 

reactivex.io

A Single is something like an Observable, but instead of emitting a series of values — anywhere from none at all to an infinite number — it always either emits one value or an error notification.

 

 

Emit과 Subscribe

RxJava에서 중요한 개념인 Emit과 Subscribe를 살펴보겠습니다.

한글로 해석하면 '발행'과 '구독'입니다.

발행 : 누군가는 데이터를 흘려보내고,

구독 : 누군가는 데이터를 가져갑니다.

 

위 Single을 참고하면 Single은 "emits one value or an error notification"라고 합니다.

우리 앱에 적용을 해보면 Retrofit을 통해 호출한 API 결과 값을 Single을 사용해 내보낸다는 것입니다.

 

그럼 누가 데이터를 가져갈까요??

우리 앱에서는 ViewModel이 가져갑니다.

ViewModel은 Single이 흘려보낸 국가 데이터를 가져옵니다.

-> View는(Fragment or Activity) 관찰자로서 ViewModel에 데이터가(LiveData) 값이 변경되었다는 것을 감지하고, 

국가 데이터를 가지고 리스트에 반영합니다. 

 

 

바로 코드로 넘어가도록 하겠습니다.

기반이 되는 프로젝트와 설명은 다음 링크를 참고해주세요.

github.com/keepseung/Country/tree/01-Add-MVVM

 

keepseung/Country

Country app built using Modern Android Development [MVVM, LiveData] - keepseung/Country

github.com

develop-writing.tistory.com/36

 

모던 안드로이드 앱 만들기 (2) - MVVM 구조를 사용한 리스트 구현

저번 앱 소개에 이어서 이번 포스팅에서 만들 것은 국가 데이터를 보여주는 리스트입니다. MVVM을 적용해 앱을 만들기 전에 MVVM이 무엇인지, 각각의 역할과 사용했을 때의 이점을 살펴보겠습니

develop-writing.tistory.com

Retrofit2, RxJava 사용하기

라이브러리 설정

build.gradle

Retrofit2의 dependecy를 추가해 줍니다. 여기서는 2.3.0 버전을 사용했지만 Retrofit2 배포 페이지를 확인해서 최신 버전을 확인한 뒤 사용하면 됩니다. 여기에 RxJava를 연결해주기 위해 adapter-rxjava를 추가합니다. 추가로 Json형태의 결과를 원하는 형태의 class로 변환해줄 수 있는 converter-gson도 추가할 수 있습니다. 

 

ReactiveX 라이브러리를 사용하기 위해 rxandroid, rxjava를 추가합니다. 여기서 java가 아닌 kotlin으로 구현할 경우 의존성 추가 시 rxkotlin를 추가하시면 됩니다.

그리고 이미지를 로딩하기 위해 Glide 라이브러리를 사용할 것입니다.

def retrofitVersion = '2.3.0'
def glideVersion = '4.9.0'
def rxJavaVersion ='2.1.1'
depenencies{
    implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
    implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"

    implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
    implementation "io.reactivex.rxjava2:rxandroid:$rxJavaVersion"

    implementation "com.github.bumptech.glide:glide:$glideVersion"
}

 

Retrofit을 통해 요청할 인터페이스 생성

get방식으로 api를 요청한 결과는 CountryModel 객체를 리스트 형식으로 반환 받을 수 있게 했습니다.  원래는 Call <T> 형태의 비동기 결과 객체를 반환받는데 adapter-rxjava2를 추가하였기 때문에 ReactiveX에서 제공하는 Observable의 한 종류 변환하여 반환할 수 있습니다. 위에서 말했듯이 Single로 받도록 했습니다.

// CountriesApi.java
public interface CountriesApi {

    @GET("DevTides/countries/master/countriesV2.json")
    Single<List<CountryModel>> getCountries();
    
}

// CountryModel.java
public class CountryModel {
    // 나라 이름을 가짐
    @SerializedName("name")
    String countryName;
    // 나라 수도 이름을 가짐
    @SerializedName("capital")
    String capital;
    // 나라 국기 이미지 url을 가짐
    @SerializedName("flagPNG")
    String flag;

    public CountryModel(String countryName, String capital, String flag) {
        this.countryName = countryName;
        this.capital = capital;
        this.flag = flag;
    }

    public String getCountryName() {
        return countryName;
    }

    public String getCapital() {
        return capital;
    }

    public String getFlag() {
        return flag;
    }
}

 

Retrofit클래스를 이용하여 CountriesService를 생성하기

Retrofit을 생성 시에 CallAdapterFactory와 ConverterFactory를 설정합니다. github api의 결괏값이 json이기 때문에 GsonConverterFactory를 설정해주도록 합니다. 

public class CountriesService {
    private static final String BASE_URL = "https://raw.githubusercontent.com/";

    private static CountriesService instance;

    public CountriesApi api = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create()) 
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
            .create(CountriesApi.class);


    public static CountriesService getInstance(){
        if (instance ==null){
            instance = new CountriesService();
        }
        return instance;
    }

    public Single<List<CountryModel>> getCountries(){
        return api.getCountries();
    }
}

 

API 요청 및 결과 데이터 받기

public class ListViewModel extends ViewModel {

     ...

    public CountriesService countriesService= CountriesService.getInstance();

    // os에 의해 앱의 프로세스가 죽거는 등의 상황에서
    // Single 객체를 가로채기 위함
    private CompositeDisposable disposable = new CompositeDisposable();

    private void fetchCountries(){
        // 서버로부터 데이터를 받아오는 동안에 로딩 스피너를 보여주기 위함
        loading.setValue(true);
        disposable.add(countriesService.getCountries() // Single<List<CountryModel>>를 반환한다.
                        .subscribeOn(Schedulers.newThread()) // 새로운 스레드에서 통신한다.
                        .observeOn(AndroidSchedulers.mainThread()) // 응답 값을 가지고 ui update를 하기 위해 필요함, 메인스레드와 소통하기 위
                        .subscribeWith(new DisposableSingleObserver<List<CountryModel>>() {
                            @Override
                            public void onSuccess(@io.reactivex.annotations.NonNull List<CountryModel> countryModels) {
                                countries.setValue(countryModels);
                                countryLoadError.setValue(false);
                                loading.setValue(false);

                            }

                            @Override
                            public void onError(@NonNull Throwable e) {
                                countryLoadError.setValue(true);
                                loading.setValue(false);
                                e.printStackTrace();
                            }
                        })
        );
    }

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

        // 앱이 통신 중에 프로세스가 종료될 경우(앱이 destory됨)
        // 메모리 손실을 최소화 하기 위해 백그라운드 스레드에서 통신 작업을 중단한다.
        disposable.clear();
    }
}

1. countriesService.getCountries()

서버 API 호출을 통해 Single<List<CountryModel>>를 반환합니다.

 

2. subscribeOn()

 Android에서는 네트워크 통신시 UI Thread를 이용할 경우 에러를 발생시키기 때문에 새로운 스레드에서 서버 API 호출을 합니다.

 

3. observeOn()

데이터를 줍는 스레드(스케줄러)를 지정합니다

즉, 데이터를 구독하는 스케줄러를 명시하는 것입니다.

응답 값을 가지고 ui update를 하기 위해 AndroidSchedulers.mainThread()로 선택합니다.

 

4. subscribe

이제, 네트워킹을 통해서 가져온 데이터를 뿌렸으니 주워야 합니다.

성공했을 때, 실패했을 때에 맞는 처리를 하면 됩니다.

 

5. CompositeDisposable

앱이 통신 중에 프로세스가 종료될 경우(앱이 destory 됨) 메모리 손실을 최소화하기 위해 백그라운드 스레드에서 통신 작업을 중단하기 위해 사용합니다. 

 

 

 

이미지 가져오기

Retrofit, RxJava와 별개로 리스트에서 이미지를 보여주기 위해 Glide 라이브러리를 사용할 것입니다.

이미지 로딩 시 보여줄 원형 프로그래스 바를 Glide의 RequestOptions을 통해 설정합니다.

public class Util {

    public static void loadImage(ImageView view, String url, CircularProgressDrawable progressDrawable){
        // 이미지 로드할 때 옵션 설정
        RequestOptions options = new RequestOptions()
                .placeholder(progressDrawable) // 이미지 로딩하는 동안 보여줄 원형 프로그레스
                .error(R.mipmap.ic_launcher_round); // url 로드할 때 error 발생시 보여줄 이미지
        Glide.with(view.getContext())
                .setDefaultRequestOptions(options)
                .load(url)
                .into(view);
    }

    // 이미지 로딩 중에 보여줄 원형 프로그레스 만들기
    public static CircularProgressDrawable getProgressDrawable(Context context){
        CircularProgressDrawable progressDrawable = new CircularProgressDrawable(context);
        progressDrawable.setStrokeWidth(10f);
        progressDrawable.setCenterRadius(50f);
        progressDrawable.start();
        return progressDrawable;
    }
}

 

adapter클래스에서 bind 할 때 다음 코드를 추가해 이미지를 가져올 수 있게 합니다.

 void bind(CountryModel country){
            countryName.setText(country.getCountryName());
            countryCapital.setText(country.getCapital());
            Util.loadImage(countryImage, country.getFlag(), Util.getProgressDrawable(countryImage.getContext()));
 }

 

결과 확인

 앱 소개에서 나온 결과 이미지와 같게 나옵니다. 

 

전체 소스 확인하기

github.com/keepseung/Country/tree/02-Add-Retrofit-Rxjava

 

keepseung/Country

Country app built using Modern Android Development [MVVM, LiveData] - keepseung/Country

github.com

 

 

 
반응형