일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- parentfragment
- Android
- 중첩네비게이션
- 자바
- MVVM
- Kotlin
- SAA
- 아키텍쳐
- 파이썬
- 스택
- 내부프레그먼트
- fragmentcontainer
- 너무 어렵다
- 사무실
- innernavigation
- rxandroid
- 패스트파이브
- media3 transformer
- 알고리즘
- 공유오피스
- 백준
- 가든웨딩
- Stack
- 안드로이드
- 후기
- 패파
- media3
- 재밌긴함
- 코틀린
- 더베일리하우스 삼성점
삽질도사
[안드로이드] 빗썸 api 가져오기 (Retrofit2 + Rxjava) using Kotlin 본문
빗썸에서 가져온 정보를 코인어플처럼 뿌려줄 것입니다. 결과부터 보시죠.
지속적으로 코인의 api를 가져와서 갱신해주고 검색을 하였을 때에 해당 코인의 정보를 다시 지속적으로 갱신해주는 방식입니다.
살펴보기 전에 api docs를 봅시다.
api docs에 나온 규칙대로 요청을 하면됩니다.
그럼 기능구현에 필요한 클래스들을 한번 살펴보도록 합시다.
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를 갱신해줍니다.