목록Android (3)

JitPack.io 와 Github 를 이용한 안드로이드 오픈소스 라이브러리 배포하기

안드로이드 앱 개발을 하다보면 특정 작업을 수행하기 위한 코드가 반복되는 경향이 있다. 너무나도 간단하거나 일반화하기 힘든 경우엔 어쩔 수 없이 매번 코딩을 하지만, 그 반대의 경우에는 해당 코드들을 갈무리 해뒀다가 재사용한다. 하지만 재사용도 한 두번이지 그 횟수가 늘어나면 귀찮아진다. 가장 중요한 것은 귀찮다는 것. 또한 해당 코드에 유지보수가 필요하다면 그러한 귀찮음은 배가된다. 처음에는 문제가 없었는데 다른 프로젝트를 진행하다보면 이 부분은 이렇게 바꾸면 더 좋을 것 같고, 이건 어떻게 바꾸고. 매번 클래스를 복붙하는 것에 스트레스를 받기 시작한다. 안드로이드에서 가장 많이 사용되는 통신 및 데이터 파싱 라이브러리인 레트로핏을 이런 식으로 사용해야 한다면 그 고통은 더욱 더 커질 것이다. 클래스가 한 두가지도 아니고... 다행스럽게도 이러한 고통에서 벗어나기 위한 방법이 있다. [JitPack.io](https://jitpack.io/) 와 GitHub 를 이용하여 내가 자주 사용하는 코드들을 라이브러리화 하는 것이다. 이 포스트의 방법을 이용하면 아래와 같은 방식으로 내 프로젝트에서 모듈을 사용할 수 있다. ```gradle compile or implementation 'com.github.깃허브사용자이름:레포지터리이름:버전' ``` ## 1. GitHub Public Repository 생성하기 가장 중요한 것은 이름이다. 저장소를 생성했다면 1단계는 끝이다. ## 2. Android Project 생성하기 보통 GitHub 에서 볼 수 있는 오픈소스 라이브러리의 구조는 다음과 같다. * ... * app or sample * library * ... ![repository tree](/images/gallery/1512968473907_repository_tree.png) app 또는 sample 폴더에는 이 라이브러리를 사용하기 위한 방법이 담겨있는 샘플 프로젝트 코드가 담겨있고, library 폴더에는 이 라이브러리를 구성하는 코드가 담겨있다. 우선은 안드로이드 프로젝트를 생성하는데, 앱 개발을 위해 프로젝트를 생성하는 것처럼 동일한 방법으로 진행한다. 이 부분에서 생성되는 부분이 바로 이 라이브러리의 샘플 코드 역할을 수행하는 모듈이다. ![make project](/images/gallery/1512968497276_make_project_1.png) 개인적으로 여기서의 패키지 네임은 다음과 같이 지정한다. com.xxx.example.라이브러리 이름 ## 3. 생성한 Project 에 모듈 추가하기 ![make module 1](/images/gallery/1512968576799_new_module_1.png) ![make module 2](/images/gallery/1512968587219_new_module_2.png) ![make module 3](/images/gallery/1512968597202_new_module_3.png) 개인적으로 패키지 네임은 다음과 같이 지정한다. com.xxx.라이브러리 이름 ## 4. Project Level gradle 설정하기 JitPack.io 와 GitHub 를 통하여 라이브러리를 배포하려면 다음과 같은 코드를 추가해줘야 한다. ```gradle buildscript { ... dependencies { ... classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' } } allprojects { repositories { ... maven { url "https://jitpack.io" } } } ``` ## 5. App Level Gradle 설정하기 (라이브러리 모듈) ```gradle ... dependencies { ... } apply plugin: 'com.github.dcendents.android-maven' group='com.github.깃허브 사용자 이름' ``` dependencies 내부에는 내가 배포할 라이브러리에서 사용할 종속성을 지정한다. 예를들어 RecyclerView 관련 라이브러리를 배포할 것이라면 dependencies 내부에 해당 내용을 적어줘야 한다. (라이브러리에서 관련 클래스를 사용할 것이므로) 또한 이렇게 배포할 라이브러리 모듈에 종속성을 쭉쭉 추가해주면, 나중에 이 라이브러리를 다른 프로젝트에서 사용할 때 app 모듈에서도 그대로 이용할 수 있다. 즉, 예를들어 Glide 라이브러리를 배포할 라이브러리의 종속성으로 추가하면 굳이 Glide 를 App Level 종속성에 추가하지 않아도 Glide 를 이용할 수 있다는 것이다. 만약 App Level 에 Glide 를 추가했는데, App Level 에서도 Glide 를 추가하여 빌드하면 문제의 소지가 있을 수 있다. 지금까지 경험상 버전이 동일하다면 문제가 발생하지 않았지만, 그렇지 않다면 Gradle 관련 에러메세지가 출력되었다. ## 6. App Level Gradle 설정하기 (샘플 프로젝트 모듈) ```gradle ... dependencies { ... compile project(':library') ... } ``` 샘플 프로젝트 모듈의 Gradle 설정에 위의 코드를 넣어준다. 싱크하게 되면 library 모듈에 정의된 코드를 이용할 수 있다. 어차피 동일한 프로젝트에 앱 모듈과 라이브러리 모듈이 포함되어 있으므로, 샘플 앱을 작성할 때 굳이 외부에서 종속성을 땡겨올 필요는 없다. ## 7. 코드 작성 및 push 라이브러리 코드를 작성하고 샘플 프로젝트에서 해당 코드를 테스트 한 뒤 이상이 없으면 1번에서 생성한 저장소에 코드를 push 해야 한다. push 할 때 스테이지에 add 할 파일들은 이것저것 따지지도 말고 다음과 같이 지정한다. ![git add](/images/gallery/1512968649360_git_add.png) Android Studio 오래된 버전에서는 별도의 플러그인을 통하여 .gitignore 파일을 작성하거나 추가해야 했는데 최근부터는 기본적으로 쓸모없는 파일이 걸러지기 때문에 위의 이미지처럼 모든 파일을 add 하면 된다. 보통 왼쪽 탐색기에서 빨간색으로 출력되는 파일들이 모두 추가될 것이다. ## 8. GitHub 에서 Release Tag 지정하기 ![release tag 1](/images/gallery/1512968677259_github_release_tag_1.png) ![release tag 2](/images/gallery/1512968692071_github_release_tag_2.png) ![release tag 3](/images/gallery/1512968703433_github_release_tag_3.png) JitPack.io 를 통하여 라이브러리를 배포하려면 JitPack 에서 라이브러리를 원격 빌드해야 하는데, 여기서 버전을 인식할 수 있는 부분이 GitHub 저장소의 Release Tag 이다. 처음은 물론이고 추후 라이브러리의 코드가 변경되어 업데이트가 필요할 때도 Release Tag 를 갱신해야 한다. ## 9. [JitPack.io](https://jitpack.io/) 에서 GitHub 레포지터리 검색 및 빌드 ![jitpack search result](/images/gallery/1512968724566_jitpack_search_result.png) 위의 순서대로 모든 작업을 진행했다면 JitPack 에서 깃허브 사용자 이름/배포할 라이브러리의 저장소 이름으로 검색했을 경우 위와 같은 화면을 볼 수 있다. Status 탭의 Get it 버튼을 누르면 원격 빌드가 진행되는데 빌드 종료까지 다소 시간이 걸릴 수 있다. 빌드가 종료되었고, 에러가 있다면 Log 탭의 아이콘이 빨간색으로 나오고 에러가 없다면 초록색으로 나온다. 이때 해당 페이지의 하단을 보면 다음과 같은 화면을 확인 할 수 있다. ![jitpack how to](/images/gallery/1512968740154_jitpack_how_to.png) JitPack 을 이용하여 배포한 라이브러리를 사용하려면 Project Level 에 jitpack 저장소를 추가하고 App Level 에서 종속성 부분에 기존 라이브러리를 이용하던 것 처럼 추가하여 사용할 수 있다. ## 10. 기타 9번까지의 작업이 정상적으로 완료되었다면 그때부터 라이브러리를 이용할 수 있다. 추가적으로 해당 라이브러리 저장소의 ReadMe.md 를 작성하여 라이브러리 추가 방법과 JitPack 배지를 넣어줄 수 있다.

Android 2017.12.11 5:16
Realm 과 Android Architecture Components (AAC) 를 이용한 간단한 메모장 만들기

AAC 에 대해 평소 호기심이 있어 해당 라이브러리를 이용하여 토이 프로젝트를 하나 만들어 봤다. 2016년 부터 이슈가 되기 시작한 MVP, MVVM 패턴이 있는데 구글에서는 MVVM 을 쉽게 구현할 수 있는 AAC 를 제공해줘서 이와 관련된 모듈들의 간단한 사용 예제를 정리해보았다. * MVP 패턴이나 MVVM 패턴에 대한 고찰은 담겨있지 않다. # 1. 프로젝트 관련 환경 및 종속성 ## A. 프로젝트 환경 * Android Studio 3.0 ## B. project level build.gradle ```gradle buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath 'io.realm:realm-gradle-plugin:4.1.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ``` ## C. app level build.gradle ```gradle ... apply plugin: 'realm-android' ... dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.0.1' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' implementation 'com.android.support:recyclerview-v7:27.0.1' implementation 'com.android.support:cardview-v7:27.0.1' implementation 'android.arch.lifecycle:extensions:1.0.0' annotationProcessor 'android.arch.lifecycle:compiler:1.0.0' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } ... ``` # 2. LiveRealmData - Realm 을 위해 LiveData 를 상속받은 클래스 ```java /** * https://github.com/ericmaxwell2003/android-persistence/blob/master/app/src/main/java/com/example/android/persistence/codelab/realmdb/utils/LiveRealmData.java */ public class LiveRealmData<T extends RealmModel> extends LiveData<RealmResults<T>> { private RealmResults<T> results; private final RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { setValue(results); } }; public LiveRealmData(RealmResults<T> realmResults) { results = realmResults; setValue(realmResults); } @Override protected void onActive() { System.out.println("LiveRealmData.onActive(): " + results); results.addChangeListener(listener); } @Override protected void onInactive() { System.out.println("LiveRealmData.onInactive()"); results.removeChangeListener(listener); } } ``` 이 프로젝트는 [Room Persistence Library](https://developer.android.com/topic/libraries/architecture/room.html) 를 이용하지 않고 Realm 을 이용하기 때문에 특정 RealmResults 에 데이터 변동시 쓰라고 Realm 에서 제공해주는 RealmChangeListener 를 사용하여 구성된 코드를 발견할 수 있다. 이 클래스가 상속하고 있는 LiveData 는 Activity 나 Fragment 따위의 수명주기를 인식할 수 있는 데이터 홀더 클래스이다. Realm 에서는 질의 결과를 계속해서 관찰하기 위해 리스너를 연결하는데, Lifecycle 에 따라 이 리스너를 제거해줘야 한다. 다행스럽게도 LiveData 에서는 이러한 시점을 onActive 와 onInActive 를 통해 알려준다. onActive 메소드는 LiveData 인스턴스를 옵저빙하는 리스너의 수가 0에서 1이 되었을 때 호출되며, onInActive 메소드는 LifeCycle 이 데이터를 출력하기에 유효하지 않은 상태 (액티비티가 백스택에 들어갔거나 화면에 안보일 때 등등) 에 호출된다. # 3. BaseDao ```java public class BaseDao<T extends RealmModel> { protected Realm mRealm; public BaseDao(Realm mRealm) { this.mRealm = mRealm; } // https://github.com/ericmaxwell2003/android-persistence/blob/master/app/src/main/java/com/example/android/persistence/codelab/realmdb/utils/Realm%2BDao.kt protected LiveRealmData<T> asLiveData(RealmResults<T> data) { return new LiveRealmData<>(data); } } ``` 이 클래스는 모든 DAO 클래스가 상속받는 클래스인데 Realm 의 질의 결과를 LiveRealmData 로 감싸서 뱉어주는 asLiveData 메소드를 포함하고 있다. # 4. LifecycleListener - LifecycleObserver 맛보기 클래스 ```java public class LifecycleListener implements LifecycleObserver { private Context mContext; public LifecycleListener(Context mContext) { this.mContext = mContext; } @OnLifecycleEvent(Event.ON_CREATE) void onCreate() { Toast.makeText(mContext, "LifecycleListener.onCreate()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_RESUME) void onResume() { Toast.makeText(mContext, "LifecycleListener.onResume()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_STOP) void onPause() { Toast.makeText(mContext, "LifecycleListener.onPause()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_START) void onStart() { Toast.makeText(mContext, "LifecycleListener.onStart()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_STOP) void onStop() { Toast.makeText(mContext, "LifecycleListener.onStop()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_DESTROY) void onDestroy() { Toast.makeText(mContext, "LifecycleListener.onDestory()", Toast.LENGTH_SHORT).show(); } } ``` 이 클래스는 LifecycleObserver 를 사용하는 맛보기 클래스인데, 이 클래스가 관찰하고 있는 컴포넌트(LifecycleOwner)의 라이프사이클 변화에 따라 특정 메소드를 자동으로 호출해준다. 구글의 개발자 페이지의 [라이프사이클 핸들링 관련 문서](https://developer.android.com/topic/libraries/architecture/lifecycle.html) 에서는 LocationListener 를 제어하는 모습을 볼 수 있으나, 위의 코드에선 단순히 Toast 메세지만 띄워준다. 이 클래스는 MainActivity 의 Lifecycle 을 관찰하는데, MainActivity 는 AppCompatActivity 를 상속받고 있다. 서포트 라이브러리 26.1.0 부터는 프래그먼트와 액티비티가 이미 LifecycleOwner 인터페이스를 구현하고 있는데, 예시로 든 AppCompatActivity 는 다음과 같은 상속구조를 가진다. ![상속도](/images/gallery/1511418846256_hierarchy.png) 위로 쭉 올라가다보면 SupportActivity 가 있는데 해당 클래스의 선언부는 다음과 같다. ```java public class SupportActivity extends Activity implements LifecycleOwner ``` 따라서 일반적인 경우에는 Activity 나 Fragment 따위가 LifecycleOwner 를 추가적으로 구현할 필요는 없다. 일반적이지 않은, 커스텀이 필요한 경우는 위의 문서를 참고하기 바란다. # 5. MainActivity ```java public class MainActivity extends AppCompatActivity { // 메모 RecyclerAdapter private MemoAdapter mMemoAdapter; // 뷰 모델 private MainViewModel mMainViewModel; // 라이프사이클 변화에 따른 토스트 메시지를 팝업해주는 리스너 private LifecycleListener mLifecycleListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 인자로 LifecycleOwner 를 넣어 생성한다. mLifecycleListener = new LifecycleListener(this); // 옵저버로 위의 리스너 인스턴스를 지정한다. getLifecycle().addObserver(mLifecycleListener); mMemoAdapter = new MemoAdapter(mOnMemoItemClickListener, mOnMemoItemUpdateClickListener, mOnMemoItemDeleteClickListener); RecyclerView recyclerView = findViewById(R.id.rv); recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext())); recyclerView.setAdapter(mMemoAdapter); findViewById(R.id.confirmBtn).setOnClickListener(mOnMemoAddClickListener); // 뷰 모델 생성 mMainViewModel = ViewModelProviders.of(this).get(MainViewModel.class); // 뷰 모델에서 LiveData 획득 후 관측 시작 mMainViewModel.getMemos().observe(this, new Observer<RealmResults<Memo>>() { @Override public void onChanged(@Nullable RealmResults<Memo> memos) { System.out.println("RealmResults<Memo>.onChanged: " + memos); mMemoAdapter.onUpdate(memos); } }); } private View.OnClickListener mOnMemoAddClickListener = new OnClickListener() { @Override public void onClick(View view) { MemoActionDialog.show(null, MainActivity.this, new Callback() { @Override public void onItemAdd(Memo memo) { mMainViewModel.addMemo(memo); } @Override public void onItemUpdate(Memo oldOne, Memo newOne) { } }); } }; private View.OnClickListener mOnMemoItemClickListener = new OnClickListener() { @Override public void onClick(View view) { } }; private View.OnClickListener mOnMemoItemUpdateClickListener = new OnClickListener() { @Override public void onClick(View view) { Memo oldOne = (Memo) view.getTag(); MemoActionDialog.show(oldOne, MainActivity.this, new Callback() { @Override public void onItemAdd(Memo memo) { } @Override public void onItemUpdate(Memo oldOne, Memo newOne) { mMainViewModel.onItemUpdateClick(oldOne, newOne); } }); } }; private View.OnClickListener mOnMemoItemDeleteClickListener = new OnClickListener() { @Override public void onClick(View view) { Memo clickedItem = (Memo) view.getTag(); mMainViewModel.onItemDeleteClick(clickedItem); } }; } ``` 위의 코드 중 mMainViewModel.getMemos().observe(...); 이 코드 덕분에 메모를 추가하거나 수정하거나 삭제하여 데이터가 변동되면 바로 알 수 있다. # 6. MainViewModel ```java public class MainViewModel extends ViewModel { private Realm mRealm; private MemoDao mMemoDao; private LiveData<RealmResults<Memo>> mMemos; public MainViewModel() { System.out.println("MainViewModel.constructor()"); mRealm = Realm.getDefaultInstance(); mMemoDao = new MemoDao(mRealm); subscribeMemos(); } private void subscribeMemos() { System.out.println("MainViewModel.subscribeMemos()"); mMemos = mMemoDao.findAllMemos(); } public LiveData<RealmResults<Memo>> getMemos() { System.out.println("MainViewModel.getMemos(): " + mMemos.getValue()); return mMemos; } public void addMemo(Memo memo) { mMemoDao.add(memo); } public void onItemDeleteClick(Memo memo) { mMemoDao.delete(memo); } public void onItemUpdateClick(Memo oldOne, Memo newOne) { mMemoDao.update(oldOne, newOne); } @Override protected void onCleared() { System.out.println("MainViewModel.onCleared()"); mRealm.close(); super.onCleared(); } } ``` 위의 뷰 모델에서는 최초 생성시 Realm 에 저장되어 있는 모든 메모 목록을 LiveData 로 불러와서 옵저빙을 할 수 있게 제공해주며, 데이터의 추가, 수정, 삭제 기능들을 Dao 클래스를 통해 제공한다. # 7. 프리뷰 ![preview](/images/gallery/1511420747863_realm_with_aac_preview.png) # 샘플 프로젝트 * [깃허브](https://github.com/prChoe/RealmMemoWithAAC) # 관련 링크 * [AAC 관련 개발자 문서](https://developer.android.com/topic/libraries/architecture/index.html) * [구글 안드로이드 개발자 블로그](https://developers-kr.googleblog.com/2017/06/android-and-architecture.html) * [Realm과 함께하는 안드로이드 아키텍쳐 컴포넌트](https://academy.realm.io/kr/posts/android-architecture-components-and-realm/)

Android 2017.11.23 7:27