본문 바로가기
2023년 이전/ReativeX

RxAndroid - Volley 사용(fromCallable, fromFuture )

by JeongUPark 2020. 2. 18.
반응형

[출처 - RxJava 프로그래밍 : 리액티브 프로그래밍 기초부터 안드로이드 까지 한번에]

본 글은 'RxJava 프로그래밍 : 리액티브 프로그래밍 기초부터 안드로이드 까지 한번에' 를 학습하면서 정리한 글입니다

 

Android 에서 Network통신을 할때 다양한 방법이 있습니다. HttpURLConnectio 클래스를 사용하거나 OkHttp라는 라이브러리를 쓰는 방법이 있습니다. 그리고 이번에 설명 드릴 Volley라이브러리 사용입니다.

 

Volley는 구글IO에서 공개한 안드로이드용 HTTP 클라이언트 라이브러리가 제공하는 기능을 제공하는 라이브러리 입니다. (더 자세한 내용은 여기서 확인하세요)

 

아무튼 이 Volley와 ReativeX를 활용하여 Network 작업을 해보도록 하겠습니다. (모든 code는 kotlin으로 작성하였습니다.)

Volley의 기본 사용법은 다음과 같습니다.

1. RequestQueue 생성

2. Request Object 생성

3. Reqeust Object를 RequestQueue에 추가

4. 설정한 CallBack으로 응답 수신 

 

그럼 Volley 라이브러리를 사용하기 위해 build.gradle에 다음을 추가합니다.

implementation 'com.android.volley:volley:1.1.1'

그리고 ReactiveX와 recyclerview를 사용하기 위해 다음들도 추가합니다.

implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.17'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

마지막으로 인터넷 통신을 위해 AndroidManifest.xml에 다음을 추가합니다.

  <uses-permission android:name="android.permission.INTERNET"/>

 

필요한 기본 설정은 완료 되었습니다.

 

LocalVolley.kt

import android.content.Context
import android.util.Log
import com.android.volley.RequestQueue
import com.android.volley.toolbox.Volley

class LocalVolley {
    companion object {
        private var sRequestQueue: RequestQueue? = null
        private val TAG : String = "LocalVolley"
        fun inite(context: Context) {
            Log.d(TAG,"inite")
            sRequestQueue = Volley.newRequestQueue(context)
        }

        fun getRequestQueue(): RequestQueue? {
            Log.d(TAG,"getRequestQueue")
            return sRequestQueue ?: throw  IllegalStateException("Not inited")
        }
    }
}

LocallVolley 클래스는 inite 매서드 호출 시 context를 이용하여 RequestQueue를 생성하는 클래스입니다. (inite로 한 이유는 kotlin에서는 init라는 명칭이 있기 때문에 따로 혼동하지 않으려고 선택했습니다.)

 

BaseApplication.kt

import android.app.Application
import android.util.Log

class BaseApplication : Application() {
    private val TAG : String = "BaseApplication"
    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate")
        LocalVolley.inite(applicationContext)
    }
}

BaseApplication은 app이 실행 되었을 때 최초로 호출되며 한번만 호출 됩니다. 이때 LocalVolley.inite로 RequestQueue를 생성하고 초기화합니다.( 사용하기 위해서는 AndroidManifest.xml의 application 항목에 android:name=".BaseApplication"를 추가해야 합니다.)

 

i_test_item.xml

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

    <TextView
        android:id="@+id/i_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

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">
    <Button
        android:id="@+id/get_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="GET"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        tools:ignore="MissingConstraints" />
    <Button
        android:id="@+id/get_2_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="GET2"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/get_btn"
        tools:ignore="MissingConstraints" />
    <Button
        android:id="@+id/get_3_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="GET3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/get_2_btn"
        tools:ignore="MissingConstraints" />
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/main_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/get_3_btn"/>


</androidx.constraintlayout.widget.ConstraintLayout>

위의 xml은 activity와 recyclerview에 사용될 item의 layout 들 입니다.

MyViewHolder.kt

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.i_test_item.view.*

class MyViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {

    val nameTView = itemView.i_name

}

MyViewAdapter.kt

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import jeongu.test.restapi_test.R

class MyViewAdapter () : RecyclerView.Adapter<MyViewHolder>(){

    public val mItems : ArrayList<String> = ArrayList<String>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.i_test_item,parent,false))
    }

    override fun getItemCount(): Int {
        return mItems.size
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = mItems[position]
        holder.nameTView.text = item
    }

}

위의 Holder와 Adapter는 RecyclerView를 생성하기 위한 Holder와 Adapter 입니다.

이제 제일 중요한 MainAcitivty를 보겠습니다.

MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.RequestFuture
import org.json.JSONObject
import androidx.recyclerview.widget.LinearLayoutManager
import jeongu.test.rx_async.MyViewAdapter
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.ExecutionException
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.observers.DisposableObserver
import java.util.concurrent.Callable

class MainActivity : AppCompatActivity() {
    private val URL = "http://time.jsontest.com/"
    private val mAdapter = MyViewAdapter()
    private val mCompositeDisposable = CompositeDisposable()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        main_recyclerview.layoutManager = LinearLayoutManager(this)
        main_recyclerview.adapter = mAdapter

        get_btn.setOnClickListener {
            post(getObservable())
        }

        get_2_btn.setOnClickListener {
            post(getObservableFromCallable())
        }
        get_3_btn.setOnClickListener {
            post(getObservableFromFuture())
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        mCompositeDisposable.clear()
    }
    private fun post(observable: Observable<JSONObject>) {
        val observer = getObserver()

        mCompositeDisposable.add(
            observable.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(observer)
        )
    }
    private fun getObservable(): Observable<JSONObject> {
        return Observable.defer{
            try {
                return@defer Observable . just < org . json . JSONObject >(getData())
            } catch (e: InterruptedException) {
                return@defer Observable . error < JSONObject >(e)
            } catch (e: ExecutionException) {
                return@defer Observable . error < JSONObject >(e.cause)
            }
        }
    }
    private fun getObservableFromCallable(): Observable<JSONObject> {
        return Observable.fromCallable(Callable<JSONObject> { this.getData() })
    }
    private fun getObservableFromFuture(): Observable<JSONObject> {
        return Observable.fromFuture(getFuture())
    }
    private fun getObserver(): DisposableObserver<JSONObject> {
        return object : DisposableObserver<JSONObject>() {
            override fun onNext(jsonObject: JSONObject) {
                addItme(" >> $jsonObject")
            }

            override fun onError(t: Throwable) {
                addItme(t.toString())
            }

            override fun onComplete() {
                addItme("complete")
            }
        }
    }
    private fun addItme(str : String){
        mAdapter.mItems.add(str)
        mAdapter.notifyDataSetChanged()
    }

    fun getFuture():RequestFuture<JSONObject>{
        val future = RequestFuture.newFuture<JSONObject>()
        val req = JsonObjectRequest(URL,null, future,future)
        LocalVolley.getRequestQueue()?.add(req)
        return future
    }
    @Throws(ExecutionException::class, InterruptedException::class)
    private fun getData(): JSONObject {
        return getFuture().get()
    }
}

 

위에서 getFuture는 생성한 JsonObjectRequest 객체를 RequestQueue에 추가합니다. 그리고 JsonObjectRequest의 원 code를 보면 

    public JsonObjectRequest(
            String url,
            @Nullable JSONObject jsonRequest,
            Listener<JSONObject> listener,
            @Nullable ErrorListener errorListener) 

이렇게 되는데, 제가 작성한 code는 결과와 에러 모두를 RequestFuture로 받고 있는 것을 알 수 있습니다. (code에서는 future 입니다.)

그리고 위의 버튼들은 각각 defer, fromCallable, fromFuture 함수를 이용하여 Volley의 RequestFuture 객체를 처리합니다.

defer를 사용하여 subscribe가 발생하면 데이터를 사용합니다. 그래서 getData를 통하여 Volley를 사용하여 데이터를 요청하고 그 결과 값을 받게 됩니다. (defer의 자세한 내용은 여기서 확인). 그리고 just로 새로운 Observable을 생성한 이유는 내부적으로 예외 처리를 하지 못해서 try-catch로 처리하기 위해서 입니다.
fromCallable 함수는 Callable<? extends T> supplier를 매개변수로 합니다. 즉, defer와 다르게 어떤 데이터 타입도 사용할 수 있습니다. fromCallable의 마블다이어 그램을 보면

 

출처 : http://reactivex.io/RxJava/javadoc/io/reactivex/Flowable.html#fromCallable-java.util.concurrent.Callable-

이렇게 Callable에 데이터가 들어가고 그에 따른 결과값을 받습니다. 그래서 RequestFuture.get()을 전달합니다.

 

마지막으로 fromFuture 함수는 Future<? extends T> future를 매개변수로 사용합니다. Observable 내부에서 get 메서드를 요청하고 결과를 전달받아 Future 객체 자체를 내부에서 바로 처리합니다. 

마블다이어그램을 보면

출처: http://reactivex.io/RxJava/javadoc/io/reactivex/Flowable.html#fromFuture-java.util.concurrent.Future-

위의 설명과 같은 형상을 취하고 있습니다. 그래서 요청시 ReqeustFuture를 전달합니다.

 

위의 방법들을 통하여 Volley를 사용해보면 아마 error값이 나타나는 것을 확인 할 수 있습니다.

그 이유는 위의 code에 있는 사이트에 접속해보면 503 error로 다음과 같은 결과를 볼 수 있습니다.

(503 error는 서비스를 사용할 수 없는 상태로, 서버가 오버로드되었거나 유지관리를 위해 다운되었기 때문에 현재 서버를 사용할 수 없다. 이는 대개 일시적인 상태입니다.)

이렇게 나오는데, 사용량을 초과하였기 때문입니다. 결과를 보려면 다음에 다시 도전해 봐야 할 것 같습니다. 그래도 Volley를 사용한 NetWork 통신을 하는 것은 확인을 할 수 있었습니다.

반응형