삽질도사

[안드로이드] 빗썸 api 가져오기 (Retrofit2 + Rxjava) using Kotlin 본문

안드로이드

[안드로이드] 빗썸 api 가져오기 (Retrofit2 + Rxjava) using Kotlin

전성진블로그 2022. 1. 5. 00:05

빗썸에서 가져온 정보를 코인어플처럼 뿌려줄 것입니다. 결과부터 보시죠.

 

퍼렇게 멍든 코인들..

지속적으로 코인의 api를 가져와서 갱신해주고 검색을 하였을 때에 해당 코인의 정보를 다시 지속적으로 갱신해주는 방식입니다.

 

 

살펴보기 전에 api docs를 봅시다.

해당 사이트에 가면 위처럼 정보가 상세히 나와있습니다.

 

api docs에 나온 규칙대로 요청을 하면됩니다.

그럼 기능구현에 필요한 클래스들을 한번 살펴보도록 합시다.

흰색표시 된 부분만 다룰 것입니다. + MainActivity

 

MainActivity (View의 역할)

class MainActivity : AppCompatActivity() {

    private lateinit var binding : ActivityMainBinding
    private lateinit var ET_Observable_Disposable: Disposable
    private lateinit var liveData_tickerMap:MutableLiveData_TickerMap
    private lateinit var adapter: Coin_Adapter
    private var thread_all:NetworkThread? = null
    private var thread_search:NetworkThread? = null

    override fun onRestart() { //다시 돌아왔을 경우 마지막에 사용하던 Thread를 다시 시작
        super.onRestart()
        Log.e("onRestart","onRestart : "+ binding.searchET.text.toString())
        Set_threads(binding.searchET.text.toString())
    }

    override fun onStop() { //화면 밖으로 나갈 경우 모든 Thread 종료
        super.onStop()
        Log.e("onStop","onStop")
        Interrupt_threads()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_main)

        adapter = Coin_Adapter(this,ArrayList<Ticker>())

        val utility = Utility(this,binding.CoinRecyclerView,adapter) //리사이클러뷰 적용하는 것
        utility.RecyclerInit("VERTICAL")

        liveData_tickerMap = ViewModelProvider(this).get(MutableLiveData_TickerMap::class.java) //VM의 LiveData를 set하면서 Adapter를 Notify할 것입니다.
        liveData_tickerMap.coins.observe(this, Observer {
            adapter.CoinDiffUtil(Sort(it))
        })

        ET_Observable_Disposable = //RxAndroidUtil에서 검색창의 정보가 바뀔 때마다 자동으로 api를 가져올 것입니다.
            RxAndroidUtils.getEditTextObservable(binding.searchET)
                .debounce(700,TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(Consumer {
                    Set_threads(it)
                })
    }

    inner class NetworkThread( //Thread에 sleep을 주어서 딜레이를 주고 그 외엔 계속 돌립니다.
        private val search_ET: String
    ):Thread() {

        var isRunning = true

        override fun run() {
            while (isRunning) {
                try {
                    Log.d("NetworkThread", "running")
                    liveData_tickerMap.Get_API(search_ET)
                    sleep(3000)
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
            }
        }

    }

    private fun Sort(map: Map<String,Ticker>):ArrayList<Ticker>{ // 가져온 data를 거래금액 순으로 정렬해줍니다.

        val list:ArrayList<Ticker> = ArrayList(map.values)
        list.sortByDescending { it.acc_trade_value_24H!!.toDouble() }

        return list
    }

    private fun Interrupt_threads(){ //Thread 중단

        thread_all = thread_all?.run{
            this.isRunning = false
            if(!this.isInterrupted)
                this.interrupt()

            null
        }

        thread_search = thread_search?.run{
            this.isRunning = false
            if(!this.isInterrupted)
                this.interrupt()

            null
        }
        Log.d("search","thread_all thread is null? : " + thread_all?.run { "false" })
        Log.d("search","thread_search thread is null? : " + thread_search?.run { "false" })
    }

    private fun Set_threads(Search:String){ //Thread 시작
        if(Search.length >= 2){
            Log.d("search","search something")
            Interrupt_threads()

            liveData_tickerMap.disposable?.run {
                this.dispose()
            }

            if(thread_all == null){
                thread_search = NetworkThread(Search).apply {
                    Log.d("search", "thread_search thread is starting: $Search")
                    this.start()
                }
            }

        }
        else if(Search.isEmpty()){
            Log.d("search","no search")
            Interrupt_threads()

            liveData_tickerMap.disposable?.run {
                this.dispose()
            }

            if(thread_search == null){
                thread_all = NetworkThread(Search).apply {
                    Log.d("search","thread_all thread is starting")
                    this.start()
                }
            }
        }
        else{
            Toast.makeText(this,"\"2글자 이상 입력해주세요.\"",Toast.LENGTH_SHORT).show()
        }
    }

}

 추후에 정리 좀 해야겠네요 좀 불필요한 것도 보입니다 ;ㅁ;

 

Repository,RetrofitFactory,Service (Model의 역할) 를 알아봅시다. 

 

Service 

interface Service {

    @GET("public/transaction_history/{path}_KRW")
    fun TRANSACTION_LIST_OBS(
        @Path("path") path: String,
        @Query("count") count: Int
    ): Observable<Transaction_List_Response> // ?count={count}를 나타냄 -> 원하는 리스트 갯수요청

    @GET("public/ticker/{path}_KRW")
    fun TICKER_SINGLE(
        @Path("path") path: String
    ): Single<Ticker_Response>

    @GET("public/candlestick/{path}_KRW/24h")
    fun CANDLE_LIST_SINGLE(
        @Path("path") path: String
    ): Single<Candle_List>
   
}

어노테이션 @GET(베이스URL을 제외한 나머지)으로 api를 요청할 것이고, @Path("path") path: String 인수를 받아서 원하는 값을 어노테이션에 넣을 수 있습니다.

 

@GET("public/ticker/{path}_KRW") 이고, path 인수에 BTC를 넣는다면,

@GET("public/ticker/BTC_KRW")가 되겠지요.

 

추가로 위에 있는 

@GET("public/transaction/{path}_KRW") 에서 @Query("count")는 인수를 만약에 10을 주면,

@GET("public/transaction/{path}_KRW"/?count=10) 이 됩니다.  요청할 때 사용하면 됩니다.

 

RetrofitFactory

class RetrofitFactory {

    companion object {
        fun createRetrofit(baseUrl: String): Retrofit {
            val httpClient = OkHttpClient.Builder()
                .callTimeout(1, TimeUnit.MINUTES)
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)

            return Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(httpClient.build())
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
                .build()
        }
    }
}

return만 보시면 됩니다. Retrofit.Builder를 이용해서 retrofit객체를 돌려줍니다.

return위의 코드는 통신할 때 timeout에 대한 정의를 내린 것입니다.

 

Repository

object Repository {

    fun get_CandleList_Single(path:String) : Single<Candle_List>{
        return RetrofitFactory
            .createRetrofit("https://api.bithumb.com/")
            .create(Service::class.java)
            .CANDLE_LIST_SINGLE(path)
    }

    fun get_Ticker(path: String): Single<Ticker_Response>{
        return RetrofitFactory
            .createRetrofit("https://api.bithumb.com/")
            .create(Service::class.java)
            .TICKER_SINGLE(path)
    }

}

드디어 레파지토리입니다. 싱글톤으로 사용하기 위해 object로 생성되어있습니다.

결론적으로 레파지토리를 사용하기 위해 Service와 RetrofitFactory를 만든 것 입니다.

return값으로 Factory에서 만든 Retrofit을 돌려받고 거기에 Service클래스를 받아서 그 안에 있는

TIKER_SINGLE(path)를 실행합니다. -> 그러면 결국 아까 봤던대로 single<Ticker_Response>이 반환됩니다.

 

Ticker

data class Ticker(
    @SerializedName("prev_closing_price")
    val prev_closing_price //전일 종가
    : String?,

    @SerializedName("acc_trade_value_24H")
    val acc_trade_value_24H //최근 24시간 거래금액
    : String?,

    @SerializedName("fluctate_rate_24H")
    val fluctate_rate_24H //최근 24시간 변동률
    : String?,

    var name:String?,
    var sub_name:String?
)

우리가 가져올 Ticker는 data class로 만들었고, 어노테이션을 통해 api통신을 하게 하였습니다.

 

Ticker_Response

data class Ticker_Response(
    @SerializedName("data") @Expose
    val data: Map<String?, Any?>,

    @SerializedName("status")
    val status: String?
)

위에 있는 Ticker를 Map형태로 여러개 받아오기 위해 하나 더 만들었습니다.

결론적으로 처음에 요청하면 이 녀석이 들어옵니다.

Map의 Value를 Any?로 받았는 데, 이는 빗썸의 api를 사용할 때 각각의 코인이 아니라 ALL을 사용해서 전부 받아오면,

마지막에 Ticker의 형태가 아니라 날짜값인 date가 들어있기 때문에 이후에 처리해주기 위해 넣었습니다.

 

MutableLiveData_TockerMap (ViewModel 역할)

class MutableLiveData_TickerMap:ViewModel(){

    var disposable: Disposable? = null

    val coins: MutableLiveData<Map<String, Ticker>> by lazy {
        MutableLiveData<Map<String, Ticker>>()
    }
    val observable: Single<Ticker_Response> = Repository.get_Ticker("ALL")

    fun Get_API(search_str:String){

        disposable = observable
            .retryWhen{ e:Flowable<Throwable> -> //에러시에 1초단위로 100번까지 재시도
                val counter = AtomicInteger()
                return@retryWhen e
                    .takeWhile{e->counter.getAndIncrement() != 100}
                    .flatMap { e->
                        return@flatMap Flowable.timer(
                            counter.get().toLong(),
                            TimeUnit.SECONDS
                        )
                    }
            }
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe(Consumer {

                val map = HashMap<String,Ticker>()
                var cnt = 0

//                coins.value?.run {
//                    Log.d("accept", "coins.getValue().size(): " + this.size)
//                }
                Log.d("accept", "search_str: $search_str")
                Log.d("accept", "data.size: "+it.data.size)

                for(entry in it.data){

//                    if(cnt==183)
//                        break
//                    else
//                        cnt++

                    val name = entry.key!!
                    val obj = entry.value!!

                    if(name != "date"){ //api 마지막에 껴있는 불필요한 정보 date 제외
                        if( (search_str.length >= 2 && name.contains(search_str,ignoreCase = true))
                            || search_str.isEmpty()){

                            val gson = Gson()
                            val json = gson.toJson(obj)
                            val ticker = gson.fromJson(json,Ticker::class.java)
                            ticker.name = name
                            ticker.sub_name = name

                            map[name] = ticker
                        }
                    }
                }
                Log.d("accept", "data.size(): " + map.size)
                coins.value = map //여기서 set해주면 viewmodel의 observer를 통해 adapter를 갱신
            })
    }
}

받아온 data를 쭉 받아오고 검색내용에 따라서 전처리(Any?였던 부분을 Ticker로 바꿔줌, 검색내용 필터링)를 해주어 갱신합니다.

 

위 같은 방식으로 값을 set해주면 메인에서 정의했던 LiveData의 Observer가 apdapter를 갱신해줍니다.