: 이 포스트는 "안드로이드 개발 레벨업 교과서" 책을 요약한 내용이다. 구체적인 내용이 궁금하다면 이 책을 구매해서 읽어보면 좋겠다. 해당 포스트에서 활용할 예제 소스는 https://github.com/wikibook/advanced-android-book/tree/master/tech05/NewGitHubRepos 이다. 예제는 Git 웹 서비스 API를 이용해서 프로젝트의 리포지토리를 추출하는 앱이다. 예제에는 rxjava, Glide, Retrofit 등이 쓰였는 데 무엇인지 몰라도 된다. 소스코드 이해를 못해도 상관없다. 여기서는 mvp와 mvvm이 어떻게 구성되는 지 이해하는 게 중요하다. 아래 사진은 예제의 화면을 보여준다. 메인 엑티비티는 RecyclerView를 이용해 각 프로젝트 목록을 나타낸다. 툴바에는 스피너를 부착해 각 언어별로 프로젝트를 보여주도록 한다. RecyclerView의 목록을 누르면 마지막 사진과 같이 해당 프로젝트에 대한 세부 내용이 나온다. 먼저 mvp와 mvvm을 사용하지 않는 기본적인 구현 방법에 대해서 살펴보자. 기본적인 구현한 프로젝트를 mvp와 mvvm 설계 기법을 이용한 프로젝트로 수정해 나갈 것이다. 




Ex). mvp와 mvvm을 사용하지 않은 기본 구현(예제 소스의 app-original)



: app-original의 패키지를 보면 우리가 하는 일반적인 구조이다. RepositoryListActivity는 메인 액티비티이고 RecyclerView를 사용하기에 RepositoryAdapter를 이용한다. DetailActivity는 RecyclerView 의 목록을 누르면 나타나는 세부 내용 Activity이다. Git API 접근은 GitHubService와 NewGitHubReposApplication에서 한다. 


1. RepositoryListActivity

: 아래 코드는 RepositoryListActivity의 onCreate이다. 뷰 초기화 및 RecyclerView Adapter 연결을 한다. 스피너는 선택한 언어별에 따른 프로젝트를 보여줘야 되 Listener에서 loadRepositories 메서드를 호출한다.  

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_repository_list);

    // View를 설정한다
    setupViews();
  }

  private void setupViews() {
   
    ....View 초기화 및 Adpater 설정    


    languageSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        // 선택시 뿐만 아니라 처음에도 호출된다
        String language = (String) languageSpinner.getItemAtPosition(position);
        loadRepositories(language);
      }

      @Override
      public void onNothingSelected(AdapterView<?> parent) {

      }

    });
  }

loadRepositories() 메서드는 다음과 같다. 아래 코드를 이해 못해도 된다. 아래 메서드가 Retrofit라는 라이브러리를 이용해 Git웹 서비스에 액세스해 프로젝트 데이터를 가져온다는 것과 UI 변경만 알면 된다. UI 변경에 대해서 보자. 일단 웹에 접근하기 전에 로딩 시간이 있으므로 로딩중이라는 걸 알려주기 위해 progressBar.setVisibility(View.visible);을 메서드 처음에 호출한다. 웹 서비스에서 데이터를 정상적으로 가져온다면 아래 코드에서 onNext 콜백 메서드가 호출된다. 데이터를 다 가져왔으므로 로딩 중임을 나타내는 프로그레스 바를 View.GONE으로 표시하지 않는다. 데이터를 RecyclerView에 보여줘야 되므로 repositoryAdapter.setItemsAndRefresh()를 호출하고 인자로 가져온 데이터를 전달한다. 만약 웹 서비스에서 데이터를 가져오는 과정 중 오류가 발생한다면 onError 콜백 메서드가 호출 되 스낵바를 보여준다. 이것만 알면 된다. 아래 코드의 구체적인 부분은 몰라도 된다.   

  /**
   * 지난 1주일간 만들어진 라이브러리의 인기순으로 가져온다
   * @param language 가져올 프로그래밍 언어
   */
  private void loadRepositories(String language) {
    // 로딩 중이므로 진행바를 표시한다
    progressBar.setVisibility(View.VISIBLE);

    // 일주일전 날짜의 문자열 지금이 2016-10-27이면 2016-10-20 이라는 문자열을 얻는다
    final Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DAY_OF_MONTH, -7);
    String text = DateFormat.format("yyyy-MM-dd", calendar).toString();

    // Retrofit를 이용해 서버에 액세스한다
    final NewGitHubReposApplication application = (NewGitHubReposApplication) getApplication();
    // 지난 일주일간 만들어지고 언어가 language인 것을 요청으로 전달한다
    Observable<GitHubService.Repositories> observable = application.getGitHubService().listRepos("language:" + language + " " + "created:>" + text);
    // 입출력(IO)용 스레드로 통신하고, 메인스레드에서 결과를 수신하게 한다
    observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<GitHubService.Repositories>() {
      @Override
      public void onNext(GitHubService.Repositories repositories) {
        // 로딩이 끝났으므로 진행바를 표시하지 않는다
        progressBar.setVisibility(View.GONE);
        // 가져온 아이템을 표시하고자 RecyclerView에 아이템을 설정하고 갱신한다
        repositoryAdapter.setItemsAndRefresh(repositories.items);
      }

      @Override
      public void onError(Throwable e) {
        // 통신 실패 시에 호출된다
        // 여기서는 스낵바를 표시한다(아래에 표시되는 바)
        Snackbar.make(coordinatorLayout, "읽어올 수 없습니다.", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();
      }

      @Override
      public void onCompleted() {
        // 아무것도 하지 않는다
      }
    });
  }
RecyclerView의 목록을 클릭할 시 DetailActivity가 나타나야 한다. 다음은 이에 대한 소스이다.
  /**
   * 상세화면을 표시한다
   * @see RepositoryAdapter.OnRepositoryItemClickListener#onRepositoryItemClickListener
   */
  @Override
  public void onRepositoryItemClick(GitHubService.RepositoryItem item) {
    DetailActivity.start(this, item.full_name);
  }

위 메서드는 RepositoryAdapter안에 OnRepositoryItemClickListener 인터페이스를 상속해 구현했다. 액티비티에서 클릭리스너를 구현한 후 RepositoryAdapter에 넘기면 RepositoryAdapter는 각 목록 별로 해당 Lister을 연결한다. 다음에 살펴볼 RepositoryAdpater.java 구현 부분을 보면 알 것이다. GitHubService.RepositoryItem은 윗 부분에서 본 loadRepositories 메서드에서 Git 웹 서비스에 접근하여 얻은 데이터를 담는 RecyclerView 목록별 데이터 객체이다. 



2. RepositoryAdapter.java

: 생성자에서 RecyclerView의 클릭 리스너를 전달 받는 걸 알 수 있고 해당 리스너 인터페이스도 정의되 있다. 위에서 loadRepositories 메서드 부분에 setItemsAndRefresh(repositories.item) 메서드가 호출됬었다. 아래에 해당 메서드의 구현이 있다. 인자로 웹 서비스에서 가져온 데이터를 전달 받고 notifyDataSetChanged()로 RecyclerView에 적용된 데이터가 변경됬다는 걸 알려 RecyclerView내부 데이터를 refresh 한다. 마지막 부분은 ViewHolder 클래스이다.  

  public RepositoryAdapter(Context context, OnRepositoryItemClickListener onRepositoryItemClickListener) {
    this.context = context;
    this.onRepositoryItemClickListener = onRepositoryItemClickListener;
  }

  /**
   * 리포지토리의 데이터를 설정해서 갱신한다
   * @param items
   */
  public void setItemsAndRefresh(List<GitHubService.RepositoryItem> items) {
    this.items = items;
    notifyDataSetChanged();
  }

  interface OnRepositoryItemClickListener {
    /**
     * 리포지토리의 아이템이 탭되면 호출된다
     */
    void onRepositoryItemClick(GitHubService.RepositoryItem item);
  }

  /**
   * 뷰를 저장해 둘 클래스
   */
  static class RepoViewHolder extends RecyclerView.ViewHolder {
    private final TextView repoName;
    private final TextView repoDetail;
    private final ImageView repoImage;
    private final TextView starCount;

    public RepoViewHolder(View itemView) {
      super(itemView);
      repoName = (TextView) itemView.findViewById(R.id.repo_name);
      repoDetail = (TextView) itemView.findViewById(R.id.repo_detail);
      repoImage = (ImageView) itemView.findViewById(R.id.repo_image);
      starCount = (TextView) itemView.findViewById(R.id.repo_star);
    }
  }

Adapter에서 실질적인 역할을 하는 onCreateViewHolder와 onBindViewHolder 메서드를 보자. onBindViewer 메서드를 보면 Acitivity에서 전달된 리스너를 각 목록에 연결한다. 아래에 보면 Glide 라이브러리를 사용한 게 보인다. 잘 몰라도 된다. 그냥 RecyclerView의 목록의 ImageView에 image 데이터를 동그랗게 보여주는 것이다. 

  /**
   * RecyclerView의 아이템 뷰 생성과 뷰를 유지할 ViewHolder를 생성
   */
  @Override
  public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    final View view = LayoutInflater.from(context).inflate(R.layout.repo_item, parent, false);
    return new RepoViewHolder(view);
  }

  /**
   * onCreateViewHolder로 만든 ViewHolder의 뷰에
   * setItemsAndRefresh(items)으로 설정된 데이터를 넣는다
   */
  @Override
  public void onBindViewHolder(final RepoViewHolder holder, final int position) {
    final GitHubService.RepositoryItem item = getItemAt(position);

    // 뷰가 클릭되면 클릭된 아이템을 Listener에게 알린다
    holder.itemView.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        onRepositoryItemClickListener.onRepositoryItemClick(item);
      }
    });

    holder.repoName.setText(item.name);
    holder.repoDetail.setText(item.description);
    holder.starCount.setText(item.stargazers_count);
    // 이미지는 Glide라는 라이브러리로 데이터를 설정한다
    Glide.with(context)
         .load(item.owner.avatar_url)
         .asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.repoImage) {
      @Override
      protected void setResource(Bitmap resource) {
        // 이미지를 동그랗게 만든다
        RoundedBitmapDrawable circularBitmapDrawable =
            RoundedBitmapDrawableFactory.create(context.getResources(), resource);
        circularBitmapDrawable.setCircular(true);
        holder.repoImage.setImageDrawable(circularBitmapDrawable);
      }
    });

  }

GitHubService와 NewGitHubReposApplication는 retrofit 라이브러리를 이용해 Git 웹 서비스에 접근한 후 데이터를 가져오는 역할을 하고 가져온 데이터를 저장할 객체를 가진다. 이 것만 알면 된다. DetailActivity의 코드는 안 봐도 상관 없다. 해당 포스트의 본 목적은 mvp와 mvvm 구조의 이해이기 때문이다.


해당 예제를 보면 대부분의 기능들이 액티비티에 집중해 있는 것을 알 수 있다. 즉, 프로젝트 크기가 비대해지면 액티비티가 비대해진다. 그리고 UI를 변경하는 부분과 UI에 대한 처리(ex. 웹에서 데이터를 불러오기)가 함께 있어 좋은 설계가 아니라고 볼 수 있다. 즉, 코드가 모듈화되지 않아 앱이 복잡해진다.


여기까지가 우리가 평상시 하는 기본적인 구현 방법으로 예제 프로젝트를 살펴봤다. 다음 포스트에서는 이번에 배운 예제를 mvp로 변경한 코드를 살펴 볼 것이다.   

+ Recent posts