로그를 위한 Timber나 Architecture component들처럼 기본적으로 사용하는 것들이 Android Studio로 생성한 프로젝트에는 없다. 나만의 템플릿을 만들어놓고 사용하고 싶지만, 방법은 모르겠으니 기본적인 프로젝트 셋팅을 해놓고 이걸 복사해서 새 프로젝트에 써야겠다고 생각했다…만, 실제로 해보니 이것역시 프로젝트 파일들에서 프로젝트 이름, 패키지 이름등을 찾아 변경해줘야한다. 이 부분은 자동화 할 수 있지 않을까 생각은 드는데, 아직까지는 뭐가 더 간편할지 모르겠다. 여기서는 뭘 해야할지, 내용만 정리해 보겠다.
Android Studio에서 새 프로젝트를 생성하고 추가할 라이브러리 및 플러그인 관련은 다음과 같다.
- Timber log
- view binding or data binding
- navigation
- safe args
- viewmodel and livedata
1. Timber log
Timber (https://github.com/JakeWharton/timber ) 는 개선된 Android log library이다. SDK에서 제공하는 Log보다 사용이 쉽다. 사용하기 위해선 라이브러리를 모듈의 build.gradle 에 추가한다.
// Timber
implementation 'com.jakewharton.timber:timber:4.7.1'
Timber는 어플리케이션에서 딱 한번 초기화가 필요하다. 명시되어 있진 않지만, Android에서는 Application 클래스가 글로벌로 사용된다. Timber의 초기화는 사용자 정의 Application 클래스의 onCreate()에서 해주는걸 추천하고 있다. 간단히 Application 클래스를 상속받아 다음과 같이 override해주고 AndroidManifest.xml 에 명시적으로 이 클래스를 표기해준다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
//if(BuildConfig.DEBUG){
if(ApplicationConstants.TIMBER_DEBUG_LOG){
Timber.plant(Timber.DebugTree())
}else{
Timber.plant(ReleaseTree())
}
}
}
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
// Don't log VERBOSE, DEBUG and INFO
if (priority == Log.VERBOSE) {
return
}
if (priority == Log.ERROR){
val t = throwable ?: Exception(message)
// Crashlytics
// Crashlytics.setInt(CRASHLYTICS_KEY_PRIORITY, priority)
// Crashlytics.setString(CRASHLYTICS_KEY_TAG, tag)
// Crashlytics.setString(CRASHLYTICS_KEY_MESSAGE, message)
// Crashlytics.logException(t)
}
}
companion object {
private val CRASHLYTICS_KEY_PRIORITY = "Priority"
private val CRASHLYTICS_KEY_TAG = "Tag"
private val CRASHLYTICS_KEY_MESSAGE = "Message"
}
}
object ApplicationConstants {
const val TIMBER_DEBUG_LOG = true
}
<application
android:name="com.example.mynewapplication.MyApplication"
...
>
...
</application>
보통은 BuildConfig.DEBUG 로 Debug시에 로그를 찍고 Release시에는 Error 로그만 따로 처리하도록 ReleaseTree 를 만들어 사용한다. 나같은 경우, 항상 debug 빌드로 테스트 하는 것도 아니라서 별도로 상수 플래그를 만들었다. ReleaseTree는 다른 사람이 만든걸 따왔는데, 예외처리 부분은 다 주석처리 해놨으므로 이 부분에서 적절하게 자신의 예외처리 코드를 추가하면 된다.
2. View Binding and Data Binding
Data Binding은 이전에 다뤘으니 그걸 참고하고, 단순히 findViewById()만 피하고자 한다면 좀 더 간단한 View Binding을 사용하면 된다. 이를 사용하기 위해선, build.gradle에 다음과 같이 buildFeatures를 추가한다.
android {
...
buildFeatures {
viewBinding true
}
}
view binding은 빌드시점에 xml에 표시된 view layout에 대한 클래스를 미리 만들어준다. Activity나 fragment 안에서 이 클래스들을 참조해 사용하면 된다. MainActivity 클래스에서 사용하는 예는 다음과 같다.
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("MainActivity) onCreate()")
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
...
자동 생성되는 클래스 이름은 xml 파일의 이름을 따르고 뒤에 Binding이 접미어로 붙는다. 위의 예에선 ActivityMainBinding으로 되어 있는걸 볼 수 있다. 기존과 다르게, setContentView()에 xml을 넘겨주지 않고, 생성한 binding 객체를 넘겨주고 있다. 이 후 view를 참조할 때도, findViewById()를 안쓰고 binding객체를 이용한다.
3. Navigation
Android Studio 3.3 부터 에디터까지 지원하기 시작한 것으로, IOS 의 storyboard기능이 이미 오래전에 해오던 것과 유사하게 앱 화면간 전환을 쉽게 할 수 있도록 구현되었다. 왜 진작 이렇게 안했을까 싶을 정도. navigation 기능 설명을 위한게 아니니까 자세한건 공식 사이트를 참고하고 바로 라이브러리 추가 부분을 살펴보자.
최신의 Android Studio를 사용중이라면, 아마 자동으로 Navigation 부분을 추가해줄 것이다. 수동으로 해주는 경우 다음과 같다. 우선, build.gradle 파일에 다음 라이브러리를 추가한다.
dependencies {
...
// Kotlin navigation
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
// Feature module support
implementation 'androidx.navigation:navigation-dynamic-features-fragment:2.3.4'
// Testing Navigation
androidTestImplementation 'androidx.navigation:navigation-testing:2.3.4'
...
라이브러리는 추가되었고, 실제로 사용할 파트를 추가해보자. Navigation에는 Navigation Graph, NavHost, NavController의 세가지가 필요하다. Navigation graph는 xml파일로 Android Studio의 비주얼 에디터로 화면간 이동을 어떻게 할지 정의하는 부분이다. NavHost는 화면들(보통은 Fragment들)이 들어갈 컨테이너같은 것이다. NavController는 NavHost내에서 실제 화면간 전환을 처리해준다.
Navigation은 라이브러리 추가 이후는 각 앱마다 구현의 문제가 된다. 라이브러리만 추가하고 구현은 알아서 해야겠지만, Basic Activity template기준으로 나머지 부분도 간단히 알아보자.
Basic Activity template에는 기본으로 생성되어 있는 Navigation graph를 추가해보자. Android Studio에서 res 디렉토리에 마우스 우클릭으로 팝업 메뉴를 띄우고 New > Android Resource File을 선택한다. New Resource File dialog에서 “nav_graph”로 이름을 만들고(이름은 바꿀 수 있다), Resource Type을 Navigation으로 지정해 생성한다.

리소스파일을 추가하면, 아래와 같이 res 밑에 폴더와 해당 파일이 생성된걸 확인 가능하다. xml파일을 선택하면, Navigation Editor가 뜬다.

Basic Activity template로 생성한 경우, nav_graph.xml은 다음과 같다.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.example.mynewapplication.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.example.mynewapplication.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
navigation 태그안에 fragment 태그는 각 destination fragment들을 표시하고 있으며, 그 안에 action태그는 그 action이 실행될 때 어떤 destination fragment로 이동할지 표시하고 있다. 여기서는 두개의 fragment와 각각 서로를 destination으로 삼는 action을 하나씩 가지고 있다.
Basic Activity template로 생성한 프로젝트기준으로 content_main.xml 레이아웃 파일을 살펴보면, 다음 부분을 볼 수 있다. 현재 Android Studio는 <fragment 태그를 생성하는데, 이 역할을 하기위해 FragmentContainerView가 새로 생겼으며 보다 적절해 수정했다.
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
주목할 부분은 android:name 이 NavHostFragment로 지정되어있다. 이는 라이브러리에서 기본으로 제공하는 NavHost이다. app:defaultNavHost가 true인건 이게 default NavHost라는 것을 말해주며, app:navGraph는 생성했던 nav_graph를지정하고 있다.
그럼 NavController는 어떻게 사용할까? Kotlin기준으로 Activity, Fragment나 View에 findNavController() 메소드가 존재한다. 이를 이용해서 NavController를 얻은 후, navigate(action) 메소드를 이용해 nav_graph.xml에 정의된 action을 수행하고 destination fragment로 이동하게 된다. Basic Activity template 기준으로 FirstFragment를 보면, 코드의 사용은 다음과 같다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
버튼의 클릭 리스너에서 NavContoller를 얻은 후, navigate로 action을 실행하고 있다.
4. Safe args
앞서 navigation에서 화면간 전환시, 값을 전달 할 수 있는데 android에선 bundle을 통해 이 작업을 해주고 있다. 문제는 bundle안의 값을 사용자가 알아서 ‘잘’ 처리해야만한다. 이를 보완하기 위해 type safety를 체크해주는 Safe args 플러그인을 제공한다. 여기서 사용법을 다루지는 않겠다.
주의할점은 플러그인을 추가하기위해 Project level의 build.gradle에 classpath를 추가해야한다.
buildscript {
ext.kotlin_version = "1.4.31"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0-beta06"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// navigation safe args. safe args is plug-in so here.
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4" // <-- here
}
}
위와 같이 classpath를 추가한 후, 앱 모듈 build.gradle에 해당 plugin을 추가해야한다.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs' // <-- here
id 'kotlin-kapt'
}
...
5. viewmodel and live data
build.gradle에 다음 라이브러리를 추가한다. live data, viewmodel의 사용에 대해서 여기선 다루지 않겠다.
dependencies {
...
// viewmodel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
// livedata
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
}
기본적으로 필요한 코드들을 살펴봤다. 이 내용을 기반으로 새 프로젝트를 생성할 때 해당 내용들을 추가해주면된다. 이상적인 방법은 이걸 template로 만드는 것이나 현재로서는 방법이 없는거 같다. 프로젝트를 생성해놓고 복사해서 쓰는 방법을 생각해 봤으나, 실제로 해본결과 손이 많이가는건 마찬가지로 보인다. 뭐 기존 프로젝트를 변경하는 경우도 있을테니 그래도 알아보자면, 아래에 이어지는 다음 파트의 내용을 참고하자.
Project name 및 package name 변경하기
0. Clean Project
템플릿용 프로젝트에서 생성된 파일을 제거하기 위해, Android Studio > Build > Clean Project 를 실행한다.
1. Project 폴더 복사 및 이름 변경
템플릿용 프로젝트를 복사해서 카피본을 만든 후, 폴더명을 새로만들 프로그램 이름으로 변경한다.
2. Project name 변경
- Android Studio에서 복사 후 이름을 변경한 프로젝트 폴더를 New > import project 로 폴더를 지정해서 연다.
- settings.gradle 파일에서 rootProject.name을 새 이름으로 변경한다.
rootProject.name = "MyNewApplication"
3. App name 변경
res > values > strings.xml 에 있는 “app_name” 값을 새로운 앱 이름으로 변경한다.
4. Package name 변경
package name은 Android Studio의 refactor 기능을 이용한다. package name에 팝업메뉴를 띄우고 refactor > rename을 선택한다. 프로젝트 탐색기에서 package directory 또는 AndroidManifest.xml 에 있는 package 이름에 우클릭으로 팝업메뉴를 띄워도 된다.


package 이름을 바꿀 것인지, directory 이름만 바꿀 것인지 물어보는데, Rename package 를 선택한다.

자동으로 여기저기 분산된 package 이름들이 변경될 것이다.
5. applicationId 변경
모듈의 build.gradle(:app) 파일을 연다. defaultConfig 에 있는 applicationId를 변경해준다.

6. 새로 추가한 Application 클래스 이름 변경
Timber를 추가하면서 추가했던 Application class를 변경한다. 이렇게 변경하지 않고 다 똑같은 이름을 사용해도 될 거 같긴하다. 역시나 Android Studio에서 팝업메뉴를 띄워 refactor > rename을 선택한다.

7. theme 이름 변경
다 됐나 싶었는데, Theme 이름이 예전 package 명으로 되어있다. res > values > themes > themes.xml 파일을 연다. style 이름에 우클릭하여 popup 메뉴를 띄우고 refactor > rename 으로 이름을 변경해준다.

보다시피, 새 프로젝트에 라이브러리들을 추가하는게 더 간편할지도 모르겠다. 이게 템플릿화 되야 편할텐데, 왜 못하게 됐을까.