이해를 돕기 위해 2가지의 예제를 통해 RxJava의 실습 코드를 포스팅 해보려고 합니다. 해당 내용은 다른 블로그의 내용을 참조하여 작성했습니다.
두개의 예제 모두 RxJava+ Retrofit을 사용한다는 점에서 내용은 유사합니다.
첫번째 예제
Java + RxJava2 + Retrofit2 + LiveData를 이용하여 HTTP 웹 통신을 하는데, Json 결과로 주고 받는 내용입니다.
LiveData를 가져다 사용함으로써 Acitivty에서 깔끔한 코드를 구성할 수 있는 것 같습니다.
완성된 예제는 https://github.com/keepseung/Country-MVVM/tree/02-Add-Retrofit-Rxjava 에서 볼 수 있습니다.
App 모듈의 build.gradle은 아래와 같은 라이브러리가 추가되어야 합니다.
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와 ConverFactory를 설정합니다.
addCallAdapterFactory는 받은 응답을 옵저버블 형태로 반환해주는 역할을 하며,
addConvertFactory의 경우 서버에서 json 형식으로 보내고 이를 파싱해서 받아오게 도와주는 역할을 합니다.
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 MutableLiveData<List<CountryModel>> countries = new MutableLiveData<>();
// 국가 데이터를 가져오는 것에 성공했는지를 알려주는 데이터
public MutableLiveData<Boolean> countryLoadError = new MutableLiveData<>();
// 로딩 중인지를 나타내는 데이터
public MutableLiveData<Boolean> loading = new MutableLiveData<>();
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 업데이트를 하기 위해 AndroidSchedulers.mainThread()로 선택합니다.
4. subscribe
이제, 네트워킹을 통해서 가져온 데이터를 뿌렸으니 주워야합니다.
성공했을 때, 실패했을 때에 맞는 처리를 하면 됩니다.
5. CompositeDisposable
앱이 통신 중에 프로세스가 종료될 경우(앱이 destroy 됨)
메모리 손실을 최소화하기 위해 백그라운드 스레드에서 통신 작업을 중단하기 위해 사용합니다.
Activity에서 UI 업데이트 하기
public class MainActivity extends AppCompatActivity {
@BindView(R.id.countriesList)
RecyclerView countriesList;
@BindView(R.id.list_error)
TextView listError;
@BindView(R.id.loading_view)
ProgressBar loadingView;
@BindView(R.id.swipeRefreshLayout)
SwipeRefreshLayout refreshLayout;
// 뷰모델은 뷰가 어디 뷰에 사용될 지 모른다.
// 하지만 뷰는 어떤 뷰모델을 사용할지 알아야 한다.
private ListViewModel viewModel;
private CoutryListAdapter adapter = new CoutryListAdapter(new ArrayList<>());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
// viewModel 초기화는 ViewModelProviders를 통해서 한다.
// 액티비티 destroy 되고 다시 create 되더라도 뷰모델에 있는 데이터를 보여주기 위함이다.
// 액티비티가 다시 생성되더라도 이전에 생성한 뷰모델 인스턴스을 줄 수 있다.
viewModel = ViewModelProviders.of(this).get(ListViewModel.class);
// 국가 데이터를 가져온다.
viewModel.refresh();
// 리사이클러뷰에 어뎁터를 설정한다.
countriesList.setLayoutManager(new LinearLayoutManager(this));
countriesList.setAdapter(adapter);
refreshLayout.setOnRefreshListener(() -> {
// 리프레이 될 때마다 새로운 데이터를 가져온다.
viewModel.refresh();
refreshLayout.setRefreshing(false);
});
observerViewModel();
}
private void observerViewModel() {
/**
* 뷰(메인 화면)에 라이브 데이터를 붙인다.
* 메인 화면에서 관찰할 데이터를 설정한다.
* 라이브 데이터가 변경됐을 때 변경된 데이터를 가지고 UI를 변경한다.
*/
viewModel.countries.observe(this, countryModels -> {
// 데이터 값이 변할 때마다 호출된다.
if (countryModels != null){
countriesList.setVisibility(View.VISIBLE);
// 어뎁터가 리스트를 수정한다.
adapter.updateCountires(countryModels);
}
});
viewModel.countryLoadError.observe(this, isError -> {
// 에러 메세지를 보여준다.
if (isError != null){
listError.setVisibility(isError? View.VISIBLE: View.GONE);
}
});
viewModel.loading.observe(this, isLoading -> {
if (isLoading!= null){
// 로딩 중이라는 것을 보여준다.
loadingView.setVisibility(isLoading?View.VISIBLE:View.GONE);
// 로딩중일 때 에러 메세지, 국가 리스트는 안 보여준다.
if (isLoading){
listError.setVisibility(View.GONE);
countriesList.setVisibility(View.GONE);
}
}
});
}
}
두번째 예제
Kotlin + RxJava2 + Retrofit2를 이용하여 HTTP 웹 통신을 하는데, Json 결과로 주고 받는 내용입니다.
완성된 예제는 https://github.com/ldhcjs/KotlinOKHttpRetrofitRxJava 에서 볼 수 있습니다.
App 모듈의 build.gradle은 아래와 같은 라이브러리가 추가되어야 합니다.
// App 모듈의 build.gradle
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 기본 Kotlin 확장
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// OkHttp3
implementation 'com.squareup.okhttp3:okhttp:4.0.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.0.1'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.0.1'
// Retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.1'
// Json 파싱을 위한 Gson
implementation 'com.google.code.gson:gson:2.8.5'
// RxJava2
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.11'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
// 중략..
}
Android Manifest에는 INTERNET 권한만 주면 되겠죠.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ldhcjs.kotlinokhttpretrofitrxjava">
<!-- 인터넷 권한은 당연히 필수 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
https://api.github.com/repos/[owner]/[repo]/contributors 의 API의 Json 응답은 아래와 같습니다.
응답 형식은 Json Array 이네요.
[
{
"login": "ldhcjs",
"id": 29829392,
"node_id": "QPS6VXTlcjE3MSE0NZP0",
"avatar_url": "https://avatars2.githubusercontent.com/u/17514504?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/ldhcjs",
"html_url": "https://github.com/ldhcjs",
"followers_url": "https://api.github.com/users/ldhcjs/followers",
"following_url": "https://api.github.com/users/ldhcjs/following{/other_user}",
"gists_url": "https://api.github.com/users/ldhcjs/gists{/gist_id}",
/* 중략.. */
}
]
이 응답을 가지고 Contributors POJO 클래스를 만들어 준다.
SerializeName은 Json의 element와 매칭시켜주겠다는 것이고, Expose는 element의 값이 NULL일 경우 생략하겠다는 소리입니다.
// 너무 길어서 코드는 중략했습니다. Github의 전체 소스 참고하세요.
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
class Contributors {
@SerializedName("login")
@Expose
private var login: String? = null
@SerializedName("id")
@Expose
private var id: Int? = null
@SerializedName("node_id")
@Expose
private var nodeId: String? = null
/* 중략... */
fun getLogin(): String? {
return login
}
fun setLogin(login: String) {
this.login = login
}
fun getId(): Int? {
return id
}
fun setId(id: Int?) {
this.id = id
}
fun getNodeId(): String? {
return nodeId
}
fun setNodeId(nodeId: String) {
this.nodeId = nodeId
}
/* 중략... */
}
Retrofit 인터페이스를 만들어줍니다.
@Path는 {파라미터}에 값을 넣을 수 있게 해줍니다.
아래 예제에서는 {owner}에 owner로 들어오는 값을 넣을 수 있게 해주게 되죠.
Github로 보면 User id가 owner의 값으로 들어가게 됩니다.
응답을 받아서 파싱해서 보여주고 끝이기 때문에 Single로 받았습니다.
또한 Json Array 형식이기 때문에 제네릭 타입은 Array<Contributors>로 했네요, ArrayList<Contributors>로 해도 됩니다.
import retrofit2.http.GET
import io.reactivex.Single
import retrofit2.http.Path
interface KotlinRetrofitInterface {
@GET("repos/{owner}/{repo}/contributors")
fun requestContributors(
@Path("owner") owner:String,
@Path("repo") repo:String
) : Single<Array<Contributors>>
}
이제 OkHttp Manager 클래스를 하나 만들어 줍니다.
필요하다면 SSL 인증서를 무시하는 기능을 추가해야 할 수도 있습니다. 여기서는 필요가 없어서 넣지는 않았습니다.
object 클래스로 선언한 이유는 OkHttp 인스턴스를 한번만 생성해서 계속 사용해야 하는 경우가 많이 때문입니다. 음 마치 static 변수 같은 느낌이죠.
object KotlinOKHttpRetrofitRxJavaManager {
val CONNECT_TIMEOUT: Long = 15
val WRITE_TIMEOUT: Long = 15
val READ_TIMEOUT: Long = 15
val API_URL: String = "https://api.github.com/"
var mOKHttpClient: OkHttpClient
var mRetrofit: Retrofit
var mKotlinRetrofitInterface: KotlinRetrofitInterface
init {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
mOKHttpClient = OkHttpClient().newBuilder().apply {
addInterceptor(httpLoggingInterceptor)
connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
}.build()
mRetrofit = Retrofit.Builder().apply {
baseUrl(API_URL)
client(mOKHttpClient)
addCallAdapterFactory(RxJava2CallAdapterFactory.create())
addConverterFactory(GsonConverterFactory.create())
}.build()
mKotlinRetrofitInterface = mRetrofit.create()
}
fun getInstance(): KotlinRetrofitInterface {
return mKotlinRetrofitInterface
}
}
그리고 이제 마지막 MainActivity 네요.
사실 이 예제에서는 따로 UI가 없고, 앱이 실행하자마자 응답이 제대로 넘어오면 Good이라는 토스트 팝업을 띄우고, 에러가 발생하면 doOnError와 같은 내용의 토스트 팝업을 띄우는 예제입니다.
class MainActivity : AppCompatActivity() {
val TAG: String = "MainActivity"
@SuppressLint("CheckResult")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = KotlinOKHttpRetrofitRxJavaManager.getInstance()
// "ldhcjs", "GetPackagesName" 에는 각자의 Github id와 Repository를 넣으셔도 됩니다.
adapter.requestContributors("ldhcjs", "GetPackagesName")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnError {
Toast.makeText(this, "doOnError", Toast.LENGTH_SHORT).show()
Log.d(TAG, "doOnError")
}
.unsubscribeOn(Schedulers.io())
.onErrorReturn { t: Throwable ->
Log.d(TAG, "onErrorReturn : " + t.message)
arrayOf(Contributors())
}
.subscribe { result ->
if ("User" == result[0].getType()) {
Toast.makeText(this, "Good", Toast.LENGTH_SHORT).show()
Log.d(TAG, "subscribe good")
} else {
Log.d(TAG, "subscribe bad")
}
}
}
}
대략 이런식으로 Kotlin, RxJava, Retrofiy, OkHttp를 연동시켜서 HTTP 통신을 할 수가 있습니다.
출처
(첫번째 예제)
https://develop-writing.tistory.com/38
모던 안드로이드 앱 만들기 (3) - Retrofit, RxJava를 이용한 네트워크 통신
Retrofit2, RxJava RxJava는 asynchronous event based communications (비동기 이벤트 기반 통신)을 위한 라이브러리입니다. 기존 Retrofit 사용 시에는 주로 Call 인터페이스의 enqueue 메서드를 통해서 비동기적으로 AP
develop-writing.tistory.com
(두번째 예제)
https://like-tomato.tistory.com/231
[Android] Kotlin + RxJava + Retrofit + OkHttp 로 http 통신하기
이번 포스팅은 Kotlin + RxJava2 + Retrofit2 + OkHttp3 를 이용하여 HTTP 웹 통신을 하는데, Json 결과로 주고 받는 내용입니다. 예제는 Github의 Contributor 를 불러올 수 있는 API로 구성되어 있습니다. 완성된 예
like-tomato.tistory.com
https://dev-eunji.tistory.com/20
[Android] RxJava2 + Retrofit2
RxJava와 가장 많은 조합으로 사용된다고 알고 있는 Retrofit 사용법을 정리해보고자 합니다. RxJava 스타일대로 Retrofit을 사용할 수 있게 해주는 adapter 라이브러리가 RxJava3 버전으로 아직 나오지 않아
dev-eunji.tistory.com
https://black-jin0427.tistory.com/34
[Android, Retrofit, RxJava2] RxJava2 을 사용한 Retrofit 통신
안녕하세요. 블랙진입니다. 안드로이드 통신을 하는데 Retrofit 을 많이 사용합니다. 아래는 Retrofit 에 대한 문서입니다. - 한글http://devflow.github.io/retrofit-kr/ - 영어https://square.github.io/retrofit/ 저는 RxJ
black-jin0427.tistory.com
https://www.notion.so/Retrofit-3-Callback-RxJava-Coroutine-01d56c2b6b4a47f287b530673ea54bd5
Retrofit에서 통신 방법 3가지(Callback, RxJava, Coroutine)
이번 포스팅은 안드로이드 개발에서 네트워크 통신을 위해 정말 많이 사용하는 Retrofit Library로 통신하는 3가지 방법에 대해 소개합니다. Retrofit 사용에 관한 자세한 설명은 하지 않습니다.
www.notion.so
'Android > RxJava' 카테고리의 다른 글
[Android] RxJava 병렬 처리 (flatmap, merge, zip) (1) | 2023.09.26 |
---|---|
RxJava (12) - Backpressure와 Flowable (0) | 2023.06.18 |
RxJava (11) - 스케줄러 (0) | 2023.06.18 |
RxJava (10) - Observable 디버깅하기 (0) | 2023.06.18 |
RxJava (9) - Observable 오류 처리하기 (0) | 2023.06.18 |