: 예제를 통해 Retrofit2를 배워보겠다. Retrofit2를 배우기 전에 RxJava와 RxAndroid, JSON에 대한 기본 지식이 필요하다. 이에 대해 모른다면 최소 다음의 포스트들을 읽기 바란다.


"1. RxJava - 시작"

"2. RxJava - Observer와 DefaultObserver란"

"3. RxJava - DisposableObserver, SingleObserver, CompletableObserver, Maybe"

"7. RxAndroid - Schedulers(스케줄러)"


"제이슨(JSON) 기본부터 안드로이드 제이슨까지"


레트로핏은 개발자가 손쉽게 REST API와 통신할 수 있게 해준다. 레트로핏은 RxJava를 사용하지 않아도 되지만 RxJava를 사용한다면 더 간결해지고 쉽게 코딩이 가능하다. 레트로핏을 이용한 예제를 통해 레트로핏에 대해 배워보자.


ex)

: 우리는 StackExchange API를 이용해 StackOverflow에서 가장 유명한 사람들을 검색해 리스트로 보여주는 앱을 구현할 것이다. 리스트에 나온 사람을 클릭하면 개인 웹사이트나 StackOverflow 사용자 프로필을 보여줄 것이다. 결과는 아래와 같다. 메인 액티비티에 RecyclerView로 목록을 보여주고 목록은 CardView를 사용한다. 메인 액티비티의 레이아웃은 사진 아래의 코드와 같다. id나 layout_width, margin 같은 건 생략했다.




<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ....
    tools:context="com.example.user.example.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
	......			
					>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/main_recyclerview"
            android:clipToPadding="false"
	    .....
					/>
    </android.support.v4.widget.SwipeRefreshLayout>
</android.support.constraint.ConstraintLayout>

: SwipeRefreshLayout은 프로그레스바를 가진 레이아웃인데 RecyclerView를 아래로 스크롤하면 프로그레스 바가 나타나 특정 작업을 진행 중임을 알려준다. 그 때 별도의 작업을 명시할 수 있다. 우리는 RecyclerView를 아래로 스크롤하면 프로그레스 바가 나타나고 API를 통해 웹 서버에서 JSON을 가져오는 작업을 할 것이다. 즉, 새로고침하는 것이다. 각 RecyclerView의 목록은 CardView를 사용했다. 이에 대한 자세한 XML 코드는 생략하겠다.


1. Dependencies 설정

: build.gradle 파일에 다음과 같이 Dependencies를 설정한다. RxJava와 RxAndroid는 알 것이다. RxBinding은 뷰에 이벤트 Observable을 붙이기 위해 사용한다. 원래는 RxAndroid에 있던 ViewObservable인데 해당 부분이 RxBinding의 RxView로 바꼈다. 이에 대한 부분은 뒷 부분에서 알아볼 것이다. 우리는 StackChange API를 이용해 JSON 데이터를 받는 데 JSON을 쉽게 다루기 위해 구글에서 제공하는 GSON을 이용한다. GSON도 뒷 부분에서 알아볼 것이다. Retrofit의 경우 GSON과 RxJava2를 사용하려면 아래와 같은 Dependency가 필요하다.

//RxJava, RxAndroid
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'

//GSON
compile 'com.google.code.gson:gson:2.2.4'

//Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'

//UniversdalImageLoader
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'


2. Internet 권한 추가

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

3. API를 통해 가져온 데이터를 저장할 모델 설정

: API를 통해서 가져온 JSON 데이터를 저장할 클래스를 만들어야 한다. 그래야 사용할 수 있기 때문이다. JSON 데이터를 쉽게 클래스화 하는 방법에 대해 알아보자. 먼저 우리가 URL을 통해 데이터를 요청할 때 정리된 JSON 형태로 오지 않고 raw data 형식으로 오기 때문에 raw data를 JSON형식을 바꿔져야 한다. 이를 쉽게 해주는 게 "Postman"이라는 툴이다. 구글에 검색하면 "Postman" 설치법이 바로 나온다. 설치 후에 우리가 사용할 아래의 URL(StackOverflow에서 제일 유명한 사람 데이터를 불러오는 URL)을 넣고 JSON 데이터를 얻는다. 아래 사진과 같이 URL을 넣고 Send 버튼을 누르면 아래에 JSON으로 변형된 데이터를 얻을 수 있다.

https://api.stackexchange.com/2.2/users?order=desc&sort=reputation&site=stackoverflow

우리가 얻은 JSON 데이터를 쉽게 클래스화 해주는 "jsonschema2pojo"를 사용해보자. 우리가 "Postman"에서 얻은 JSON 데이터는 여러 "items"로 되있기 때문에 하나의 "items"만 가져와서 "jsonschema2pojo" 사이트에 아래와 같이 붙여넣는다. Package에는 items JSON 객체가 클래스로 될 때의 클래스 패키지, Class name은 해당 클래스 Name을 적으면 된다. Source type은 JSON, 아래에는 Gson으로 한 후 사이트 밑에 있는 Preview 버튼을 누른다. 그러면 JSON 데이터가 클래스로 변한 걸 알 수 있다. 그 결과를 복사해서 프로젝트에 붙이면 된다. 우리는 "items" json 객체는 UsersResponse 클래스, item 객체는 User 클래스로 할 것이다. 

UsersResponse나 User, BadgeCounts 모두 Get/Set이 함께 있는 데 레트로 람다 라는 라이브러리를 사용하면 Get/Set을 명시하지 않고 어노테이션 한 줄만으로 간략하게 할 수 있다. 레트로 람다에 대해서는 다른 웹사이트에 잘 설명되있으므로 찾아보길 바란다.


4. API와 통신할 객체 만들기

: Retrofit를 이용해 API 통신을 하려면 먼저 Retrofit.Builder()로 Retrofit 객체를 만들어야 한다. Retrofit에서 RxJava2와 Gson을 사용하려면 Builder에 추가적으로 아래와 같이 add~ 메서드를 호출해야 한다. StackExchangeService는 API를 이용한 HTTP 통신을 위한 메서드를 가지는 인터페이스다. 코드를 보다시피 GET, POST, DELETE, PUT, PATCH 애노테이션을 통해 HTTP Request를 할 수 있다. 매개변수의 @Query는 url 뒤에 pagesize=howmay가 붙여진다. GET 애노테이션 안의 URL은 앞에 Retrofit 객체를 생성할 때 명시해준 baseUrl이, 뒤에는 @Query에 명시한 부분이 추가되어 HTTP Request 처리를 한다. HTTP 처리 결과는 Flowable<UsersResponse> 로 반환된다. 실제는 JSON 데이터를 받지만 GSON의 도움으로 UsersResponse에 값이 대입된다. Flowable은 Observable과 거의 비슷한데 다른 점이 back pressure이 지원된다는 점이다. back pressure이란 데이터의 처리 순서를 보장해주는 로직이다. Observable은 back pressure가 보장되지 않아 아이템이 순서대로 발행된다고 보장되지 않는다. 하지만 Flowable은 보장된다. 따라서 Observable 보단 Flowable을 사용하는 걸 권장한다. 마지막으로 retrofit.create(StackExchangeService.class); 를 통해 생성된 객체를 통해 외부에서 API를 이용한 HTTP 통신을 할 수 있다. 

public class StackExchangeManager {
    private final StackExchangeService mStackExchangeService;

    public StackExchangeManager(){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.stackexchange.com")
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        mStackExchangeService = retrofit.create(StackExchangeService.class);
    }
}

public interface StackExchangeService {
    @GET("/2.2/users?order=desc&sort=reputation&site=stackoverflow")
    Flowable<UsersResponse> getMostPopularSOusers(@Query("pagesize") int howmany);
}


5. HTTP Request 결과 처리하기

: HTTP Request로 받은 결과를 프로그램에 맞게 처리해야 한다. 우리는 StackChange API를 통해 받은 데이터를 RecyclerView에 전달해 메인 스레드에서 UI를 변경해야 한다. 따라서 다음 코드와 같이 한다. map을 통해서 UsersResponse를 List<Users>로 바꿔주고 발행된 데이터가 메인 스레드에서 작업되도록 한다. 
public class StackExchangeManager {
    private final StackExchangeService mStackExchangeService;

    public StackExchangeManager(){
        ...
    }

    public Flowable<List<User>> getMostPopularSQysers(int howmany){
        return mStackExchangeService
                .getMostPopularSOusers(howmany)
                .map(new Function<UsersResponse, List<User>>() {
                    @Override
                    public List<User> apply(@NonNull UsersResponse usersResponse) throws Exception {
                        Log.d("debug","map");
                        return usersResponse.getUsers();
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

아래의 나머지 코드는 List<User> 데이터를 가지고 RecyclerView를 갱신하는 코드이다.


public class MainActivity extends AppCompatActivity implements MainAdapter.ViewHolder.OpenProfileListener { private SwipeRefreshLayout mSwipeLyt; private RecyclerView mRecyclerView; private MainAdapter mAdapter; private StackExchangeManager mManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSwipeLyt = (SwipeRefreshLayout)findViewById(R.id.main_lyt); mRecyclerView = (RecyclerView)findViewById(R.id.main_recyclerview); mAdapter = new MainAdapter(new ArrayList<User>()); mAdapter.setOpenProfileListener(this); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.setAdapter(mAdapter); ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(this)); mManager = new StackExchangeManager(); mSwipeLyt.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refreshList(); } }); refreshList(); } private void refreshList(){ showRefresh(true); mManager.getMostPopularSQysers(10) .subscribe(new DefaultSubscriber<List<User>>() { @Override public void onNext(List<User> users) { showRefresh(false); mAdapter.updateUsers(users); } @Override public void onError(Throwable t) { showRefresh(false); } @Override public void onComplete() {} } ); } private void showRefresh(boolean show){ mSwipeLyt.setRefreshing(show); int visibility = show ? View.GONE : View.VISIBLE; mRecyclerView.setVisibility(visibility); } @Override public void open(String url) { Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } }

아래 코드에서 추가로 볼게 RxView.clicks(mView)가 있다. 이 메서드는 mView에 클릭 이벤트를 등록하는 것이다. RxBinding라이브러리를 통해 이용할 수 있다. 

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

    private static ViewHolder.OpenProfileListener mProfileListener;
    private List<User> mUsers = new ArrayList<>();
    
    public MainAdapter(List<User> users) {mUsers=users;}

    public void updateUsers(List<User> users){
        mUsers.clear();
        mUsers.addAll(users);
        notifyDataSetChanged();
    }
    
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_item,parent,false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        if(position<mUsers.size()){
            User user = mUsers.get(position);
            holder.setUser(user);
        }
    }

    @Override
    public int getItemCount() {
        return mUsers == null ? 0 : mUsers.size();
    }

    public void setOpenProfileListener(ViewHolder.OpenProfileListener listener) {
        mProfileListener = listener;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private final View mView;
        private TextView name;
        private TextView city;
        private TextView reputation;
        private ImageView user_image;


        public ViewHolder(View view){
            super(view);
            mView = view;
            name = (TextView) view.findViewById(R.id.name);
            city = (TextView) view.findViewById(R.id.city);
            reputation = (TextView) view.findViewById(R.id.reputation);
            user_image = (ImageView) view.findViewById(R.id.user_image);
        }

        public void setUser(final User user){
            name.setText(user.getDisplayName());
            city.setText(user.getLocation());
            reputation.setText(String.valueOf(user.getReputation()));

            ImageLoader.getInstance().displayImage(user.getProfileImage(), user_image);

            RxView.clicks(mView).subscribe(new DefaultObserver<Object>(){
                @Override
                public void onNext(@NonNull Object o) {
                    Preconditions.checkNotNull(mProfileListener, "Empty Listener");
                    String url = user.getWebsiteUrl();
                    if(url != null)
                        mProfileListener.open(url);
                    else
                        mProfileListener.open(user.getLink());
                }

                @Override
                public void onError(@NonNull Throwable e) {}

                @Override
                public void onComplete() {

                }
            });
        }

        public interface OpenProfileListener{
            public void open(String url);
        }
    }
}


+ Recent posts