웹사이트 검색

Android RxJava 및 Retrofit


이 튜토리얼에서는 안드로이드 앱에서 RxJava를 구현할 것입니다. Retrofit 및 RxJava를 사용하여 RecyclerView를 채우는 애플리케이션을 만들 것입니다. 우리는 CryptoCurrency API를 사용할 것입니다.

무엇을 배울 것인가?

  • RxJava를 사용하여 Retrofit 호출하기.
  • RxJava를 사용하여 여러 Retrofit 호출 수행
  • RxJava를 사용하여 Retrofit POJO 응답 변환

Android 애플리케이션에서 Java 8을 사용하여 람다 식을 사용할 것입니다.

개요

Retrofit은 응답을 구문 분석하기 위해 OkHttp를 HttpClient 및 JSON 파서로 사용하는 REST 클라이언트입니다. 여기서는 gson을 JSON 파서로 사용합니다. 다음은 Retrofit 인스턴스가 생성되는 방식입니다.

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();

        Gson gson = new GsonBuilder()
                .setLenient()
                .create();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

HttpLoggingInterceptor는 네트워크 호출 중에 데이터를 기록하는 데 사용됩니다. RxJava는 스트림 형태의 비동기식 및 반응형 프로그래밍에 사용되는 라이브러리입니다. 우리는 RxJava에서 다른 스레드를 사용합니다. 네트워크 호출을 위한 백그라운드 스레드와 UI 업데이트를 위한 기본 스레드. RxJava의 스케줄러는 다른 스레드를 사용하여 작업을 수행하는 역할을 합니다. RxAndroid는 RxJava의 확장이며 Android 환경에서 사용할 Android 스레드를 포함합니다. Retrofit 환경에서 RxJava를 사용하려면 두 가지 주요 변경 사항만 수행하면 됩니다.

  • Retrofit Builder에 RxJava를 추가합니다.
  • Call 대신 인터페이스에서 Observable 유형 사용

여러 번 호출하거나 응답을 변환하기 위해 RxJava 연산자를 사용합니다. 아래의 샘플 애플리케이션을 통해 어떻게 수행되는지 살펴보겠습니다.

프로젝트 구조

    implementation 'com.android.support:cardview-v7:27.1.0'
    implementation 'com.android.support:design:27.1.0'
    implementation('com.squareup.retrofit2:retrofit:2.3.0')
            {
                exclude module: 'okhttp'
            }

    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'

암호

레이아웃 activity_main.xml에 대한 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.constraint.ConstraintLayout>

CryptocurrencyService.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.rxjavaretrofit;

import com.journaldev.rxjavaretrofit.pojo.Crypto;

import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface CryptocurrencyService {


    String BASE_URL = "https://api.cryptonator.com/api/full/";

    @GET("{coin}-usd")
    Observable<Crypto> getCoinData(@Path("coin") String coin);
}

@얻다. POJO 클래스 Crypto.java는 다음과 같습니다.

package com.journaldev.rxjavaretrofit.pojo;

import com.google.gson.annotations.SerializedName;
import java.util.List;

public class Crypto {

    @SerializedName("ticker")
    public Ticker ticker;
    @SerializedName("timestamp")
    public Integer timestamp;
    @SerializedName("success")
    public Boolean success;
    @SerializedName("error")
    public String error;


    public class Market {

        @SerializedName("market")
        public String market;
        @SerializedName("price")
        public String price;
        @SerializedName("volume")
        public Float volume;

        public String coinName;

    }

    public class Ticker {

        @SerializedName("base")
        public String base;
        @SerializedName("target")
        public String target;
        @SerializedName("price")
        public String price;
        @SerializedName("volume")
        public String volume;
        @SerializedName("change")
        public String change;
        @SerializedName("markets")
        public List<Market> markets = null;

    }
}

coinName은 우리가 설정한 필드입니다. RxJava의 마법을 사용하여 이 필드에 값을 설정하여 응답을 변환합니다.

RxJava를 사용하여 단일 호출 만들기

CryptocurrencyService cryptocurrencyService = retrofit.create(CryptocurrencyService.class);
        
Observable<Crypto> cryptoObservable = cryptocurrencyService.getCoinData("btc");
cryptoObservable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(result -> result.ticker)
.subscribe(this::handleResults, this::handleError);

subscribeOn()은 네트워크 호출을 수행하는 스케줄러 스레드를 생성합니다. 다음 스케줄러 중 하나를 전달할 수 있습니다.

  • trampoline(): 현재 스레드에서 작업을 실행합니다. 따라서 스레드의 현재 작업이 완료된 후 코드를 실행합니다. 대기열 작업에 유용합니다.
  • newThread(): 각 작업 단위에 대해 새 스레드를 생성하는 스케줄러를 생성하고 반환합니다. 매번 별도의 스레드를 생성하기 때문에 비용이 많이 듭니다.
  • computation(): 계산 작업을 위한 스케줄러를 생성하고 반환합니다. 스레드 풀이 바인딩되어 있으므로 병렬 작업에 사용해야 합니다. 여기서는 I/O 작업을 수행하면 안 됩니다.
  • io(): IO 바운드 작업을 위한 스케줄러를 생성하고 반환합니다. 다시 계산처럼 제한됩니다. 일반적으로 네트워크 호출에 사용됩니다.

subscribeOn() 대 observeOn()

  • subscribeOn은 다운스트림 및 업스트림에서 작동합니다. 위와 아래의 모든 작업은 동일한 스레드를 사용합니다.
  • observeOn은 다운스트림에서만 작동합니다.
  • 연속적인 subscribeOn 메소드는 스레드를 변경하지 않습니다. 첫 번째 subscribeOn 스레드만 사용됩니다.
  • 연속적인 observeOn 메서드는 스레드를 변경합니다.
  • observerOn() 다음에 subscribeOn()을 넣어도 스레드가 변경되지 않습니다. 따라서 observeOn은 일반적으로 subscribeOn 다음에 와야 합니다.

AndroidSchedulers.mainThread()는 RxAndroid의 일부이며 메인 스레드에서만 데이터를 관찰하는 데 사용됩니다. subscribe 메서드는 개조 호출을 트리거하고 곧 보게 될 handleResults 메서드에서 데이터를 가져옵니다.

다중 통화

우리는 RxJava 연산자 merge를 사용하여 두 번의 개조 호출을 차례로 수행합니다.

Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc");

Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth");

Observable.merge(btcObservable, ethObservable)
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::handleResults, this::handleError);

응답 변환

POJO 응답을 변환하기 위해 다음을 수행할 수 있습니다.

Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc")
                .map(result -> Observable.fromIterable(result.ticker.markets))
                .flatMap(x -> x).filter(y -> {
                    y.coinName = "btc";
                    return true;
                }).toList().toObservable();

        Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth")
                .map(result -> Observable.fromIterable(result.ticker.markets))
                .flatMap(x -> x).filter(y -> {
                    y.coinName = "eth";
                    return true;
                }).toList().toObservable();

        Observable.merge(btcObservable, ethObservable)
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::handleResults, this::handleError);

Observable.fromIterable을 사용하여 맵 결과를 Observable 스트림으로 변환합니다. flatMap은 요소에서 하나씩 작동합니다. 따라서 ArrayList를 단일 단일 요소로 변환합니다. filter 메서드에서 응답을 변경합니다. toList()는 flatMap의 결과를 List로 다시 변환하는 데 사용됩니다. toObservable()은 이를 Observable 스트림으로 래핑합니다.

MainActivity.java

MainActivity.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.rxjavaretrofit;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.widget.Toast;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.journaldev.rxjavaretrofit.pojo.Crypto;

import java.util.List;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

import static com.journaldev.rxjavaretrofit.CryptocurrencyService.BASE_URL;

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    Retrofit retrofit;
    RecyclerViewAdapter recyclerViewAdapter;

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

        recyclerView = findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerViewAdapter = new RecyclerViewAdapter();
        recyclerView.setAdapter(recyclerViewAdapter);


        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();

        Gson gson = new GsonBuilder()
                .setLenient()
                .create();

        retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();


        callEndpoints();
    }

    private void callEndpoints() {

        CryptocurrencyService cryptocurrencyService = retrofit.create(CryptocurrencyService.class);

        //Single call
        /*Observable<Crypto> cryptoObservable = cryptocurrencyService.getCoinData("btc");
        cryptoObservable.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).map(result -> result.ticker).subscribe(this::handleResults, this::handleError);*/

        Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc")
                .map(result -> Observable.fromIterable(result.ticker.markets))
                .flatMap(x -> x).filter(y -> {
                    y.coinName = "btc";
                    return true;
                }).toList().toObservable();

        Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth")
                .map(result -> Observable.fromIterable(result.ticker.markets))
                .flatMap(x -> x).filter(y -> {
                    y.coinName = "eth";
                    return true;
                }).toList().toObservable();

        Observable.merge(btcObservable, ethObservable)
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::handleResults, this::handleError);



    }


    private void handleResults(List<Crypto.Market> marketList) {
        if (marketList != null && marketList.size() != 0) {
            recyclerViewAdapter.setData(marketList);


        } else {
            Toast.makeText(this, "NO RESULTS FOUND",
                    Toast.LENGTH_LONG).show();
        }
    }

    private void handleError(Throwable t) {

        Toast.makeText(this, "ERROR IN FETCHING API RESPONSE. Try again",
                Toast.LENGTH_LONG).show();
    }

}

handleResultshandleError는 Java 8 호출 ::을 사용하여 호출됩니다. handlResults에서 ReyclerViewAdapter에 변환된 응답을 설정합니다. 응답에 오류가 있으면 handleError()가 호출됩니다. recyclerview_item_layout 레이아웃의 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:id="@+id/cardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="16dp">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp">

            <TextView
                android:id="@+id/txtCoin"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="8dp"
                android:layout_marginRight="8dp"
                android:layout_marginTop="8dp"
                android:textAllCaps="true"
                android:textColor="@android:color/black"
                app:layout_constraintHorizontal_bias="0.023"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/txtMarket"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="8dp"
                android:layout_marginRight="8dp"
                android:layout_marginTop="8dp"
                app:layout_constraintHorizontal_bias="0.025"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/txtCoin" />

            <TextView
                android:id="@+id/txtPrice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="8dp"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                app:layout_constraintHorizontal_bias="0.025"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/txtMarket" />


        </android.support.constraint.ConstraintLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>

RecyclerViewAdapter.java 클래스의 코드는 다음과 같습니다.

package com.journaldev.rxjavaretrofit;

import android.graphics.Color;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.journaldev.rxjavaretrofit.pojo.Crypto;

import java.util.ArrayList;
import java.util.List;

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

    private List<Crypto.Market> marketList;


    public RecyclerViewAdapter() {
        marketList = new ArrayList<>();
    }

    @Override
    public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                             int viewType) {

        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recyclerview_item_layout, parent, false);

        RecyclerViewAdapter.ViewHolder viewHolder = new RecyclerViewAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, int position) {
        Crypto.Market market = marketList.get(position);
        holder.txtCoin.setText(market.coinName);
        holder.txtMarket.setText(market.market);
        holder.txtPrice.setText("$" + String.format("%.2f", Double.parseDouble(market.price)));
        if (market.coinName.equalsIgnoreCase("eth")) {
            holder.cardView.setCardBackgroundColor(Color.GRAY);
        } else {
            holder.cardView.setCardBackgroundColor(Color.GREEN);
        }
    }

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

    public void setData(List<Crypto.Market> data) {
        this.marketList.addAll(data);
        notifyDataSetChanged();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        public TextView txtCoin;
        public TextView txtMarket;
        public TextView txtPrice;
        public CardView cardView;

        public ViewHolder(View view) {
            super(view);

            txtCoin = view.findViewById(R.id.txtCoin);
            txtMarket = view.findViewById(R.id.txtMarket);
            txtPrice = view.findViewById(R.id.txtPrice);
            cardView = view.findViewById(R.id.cardView);
        }
    }
}

RxJavaRetrofit