Saturday, October 10, 2015

[Androidannotations 사용하기] 3. How It Works ?!

이번 포스팅에서는 더 많은 annotation들을 사용하기 전에, Android Annotations가 어떻게 동작하는지 가볍게 보고 가도록하자.

개요

AndroidAnnotations는 매우 간단하게 동작한다!
표준 Java Annotation Processing Tool을 사용하여, 소스 코드를 생성하는 여분의 컴파일 단계를 추가한다.

여기서 생성되는 소스 코드가 뭘까??
예를 들어 각각의 @EActivity annotated activity가 있다고 해보자. 이 Activity의 서브 클래스는 같은 이름 끝에 추가 접미어 '_'가 붙어서 생성된다.

* 지난 포스팅 참조 - [Androidannotations 사용하기] 1. Activity에 적용하기

예를 들어, 아래의 class는

package com.some.company;
@EActivity
public class MyActivity extends Activity {
  // ...
}

같은 패키지의 다른 소스 폴더에 다음과 같이 generate 된다.

package com.some.company;
public final class MyActivity_ extends MyActivity {
  // ...
}

이 서브 클래스는 super를 부르는 것을 위임하기 전에 몇 가지 메소드들(ex. onCreate(), ...)을 overrinding하여 사용자의 Activity에 동작을 추가한다.

이게 바로 AndroidManifest.xml파일에서 Activity 이름 뒤에 반드시 '_' 접미어를 붙여야 하는 이유이다~~! 바로 요로케~~^0^


그래서 지난 포스팅처럼 Activity를 시작할 때 generate된 클래스를 호출해야 하는 것이다~~!

참고 논문 및 사이트

1. androidannotations wiki 중 "HowItWorks" https://github.com/excilys/androidannotations/wiki/HowItWorks#starting-an-annotated-activity

Robolectric 사용하기 - 2

이번 포스팅에서는 직접 Robolectric 을 사용해보도록 하겠습니다.

1. Project 생성

저는 현재 AndroidStudio 1.4. 버전을 사용하고 있으며 해당 소스코드는 GitRepostiory를 통하여 제공하고 있습니다.




2. Robolectric 설정

Robolectric 사용하기 - 1에서 작성되어 있던것 처럼 dependency를 추가하고 "Build Variants"를 변경해줍니다. "Build Variants"은 좌측하단에 존재합니다.

3. 코드 작성

코드 또한 앞선 포스팅에 했던 Roblectric Homepage를 기반으로 작성하였습니다.

4. 몇가지 문제점

가장 먼저 Robolectric 의 경우 compileSdkVersion 21 이상일 경우에 build가 정상 수행되지 않았습니다. 따라서 compileSdkVersion을 21로 낮추었고 dependency의 경우에도 'org.assertj:assertj-core:1.7.0'를 추가해주어야 ExampleUnitTest가 작동하는 로직이 정상 수행됩니다.

5. 소스코드

WelcomeActivity .class

package com.juranoaa.robolectric;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class WelcomeActivity extends Activity{

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

        Button button = (Button) findViewById(R.id.login);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
            }
        });
    }
}

LoginActivity.class

package com.juranoaa.robolectric;

import android.app.Activity;
import android.os.Bundle;

public class LoginActivity extends Activity{

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

}

welcome_activity.xml




    

login_activity.xml





AndroidManifest.xml



    

        
            
                
                
            
        

        
        

    


ExampleUnitTest.class

package com.juranoaa.robolectric;

import android.content.Intent;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.robolectric.Shadows.shadowOf;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class ExampleUnitTest {
    @Test
    public void clickingLogin_shouldStartLoginActivity() throws Exception {
        WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
        activity.findViewById(R.id.login).performClick();

        Intent expectedIntent = new Intent(activity, LoginActivity.class);
        assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
    }
}

[Androidannotations 사용하기] 2. Annotated activity 시작하기

이번 포스팅은 아주 간단하다~_~ annotate 된 Activity를 시작하는 방법이다.

annotated activity 시작하기

앞의 포스팅의 덧을 참고한다면 아주 쉽게 이해하고 할 수 있다. 본 포스팅을 건너뛰어도 좋다:D

안드로이드에서 Activity를 시작할 때 아래와 같이 해왔을 것이다!

startActivity(this, MyListActivity.class);

그러나 AndroidAnnotations를 사용한다면 반드시 MyListActivity_가 호출되어 시작되어야 한다!

startActivity(this, MyListActivity_.class);

Intent Builder

  • Since AndroidAnnotations 2.4
  • AndroidAnnotations 2.4버전 부터는 생성 된 Activity를 시작할 수 있도록 static helper를 제공한다.

    // Starting the activity
    MyListActivity_.intent(context).start();
    
    // Building an intent from the activity
    Intent intent = MyListActivity_.intent(context).get();
    
    // You can provide flags
    MyListActivity_.intent(context).flags(FLAG_ACTIVITY_CLEAR_TOP).start();
    
    // You can even provide extras defined with @Extra in the activity
    MyListActivity_.intent(context).myDateExtra(someDate).start();
    
  • Since AndroidAnnotations 2.7
  • Since AndroidAnnotations 2.7버전 부터는 startActivityForResult()도 동일하게 사용할 수 있다 ^_^

    MyListActivity_.intent(context).startForResult(REQUEST_CODE);
    

    결과 코드 및 추가 값을 얻기 위해 @OnActivityResult annotation을 사용할 수 있다~

    @OnActivityResult(REQUEST_CODE)
    void onResult(int resultCode) {
    }
    
  • Since AndroidAnnotations 3.3
  • AndroidAnnotations 3.3버전 부터는 intent builder를 이용해서 options을 Bundle parameter로 넘길 수 있당~!

    MyListActivity_.intent(context).withOptions(bundle).start();
    
  • Since AndroidAnnotations 4.0
  • AndroidAnnotations 4.0버전 부터는 Activity transition animations를 the fluent intent builder를 사용하여 추가 할 수 있다.

    MyListActivity_.intent(context).start().withAnimation(enterAnimRes, exitAnimRes));
    

    다음 포스팅에서는 Android Annotations의 동작 원리를 다시 살펴본 후, 사용하기를 계속 진행하도록 하겠다 ^_^

    참고 논문 및 사이트

    1. androidannotations wiki 중 "HowItWorks" https://github.com/excilys/androidannotations/wiki/HowItWorks#starting-an-annotated-activity

    [Androidannotations 사용하기] 1. Activity에 적용하기

    본 포스팅은 Androidannotations wiki를 기반으로 작성되었습니다 :D ~

    1. Android Annotations 환경 설정하기

    환경 설정은 앞의 포스팅을 참고해주세요^_^~!

    * AndroidAnnotations Intellij(or Android Studio 1.3) Setting 편 - http://juranosaurus.blogspot.kr/2015/08/android-annotation-setting.html

    2. Android Annotations 시작하기

    앞의 포스팅을 참고하여 환경 세팅이 모두 되었다면 이제 Android Annotations를 즐기면 된당 ^0^~

    • 1. 새로운 Activity 생성한다! (또는 이미 존재하는 Activity를 사용해도 된다~!)
    • 2. Activity에 @EActivity, @ViewById, @Click 을 사용한다! - 아래의 예제를 보고 따라해보자^_^ :
    import android.app.Activity;
    import android.widget.EditText;
    import android.widget.TextView;
    
    import org.androidannotations.annotations.Click;
    import org.androidannotations.annotations.EActivity;
    import org.androidannotations.annotations.ViewById;
    
    @EActivity(R.layout.main)
    public class MyActivity extends Activity {
    
        @ViewById(R.id.myInput)
        EditText myInput;
    
        @ViewById(R.id.myTextView)
        TextView textView;
    
        @Click
        void myButton() {
             String name = myInput.getText().toString();
             textView.setText("Hello "+name);
        }
    }
    

    main.xml은 평소 작성하던대로 작성하면 된다~! 아래는 정말 기본적인 예제이다~!

    
    
    
        
    
                
    
        
        
    
    
    • 3. 파일을 저장한다. (컴파일을 하고 우리가 만든 Activity의 이름 뒤에 '_'가 붙은 이름의 서브 클래스를 generate 한다. "MyActivity"의 generate된 서브클래스 이름은 "MyActivity_")
    • 4. Manifest에 MyActivity 대신에 MyActivity_를 등록한다. 이렇게~! :
    
    

    Android Annotations Activity에 적용하기 끄읏~!

    덧,

    AndroidManifest.xml 파일

    AndroidAnnotations가 Annotation이 적용된 각각의 Activity마다 서브클래스를 generate 하기 때문에 우리는 항상 Android Manifest에 액티비티이름 뒤에 '_' 접미사를 붙여서 등록해야한다!

    같은 패키지, 같은 이름에 그냥 _ 접미사만 붙이면 된다. AndroidManifest.xml에 등록하는거 깜빡해도 AndroidAnnotations가 알려주니 걱정말자~ ^_^

    AndroidManifest.xml 파일 찾기

    AndroidAnnotations는 generate 된 소스 폴더로부터 반복적으로 이동하여 AndroidManifest.xml 파일을 찾는다.

    AndroidAnnotations 2.7 부터는 프로젝트 구조에 적합하지 않는 경우 프로세서에 androidManifestFile 옵션을 제공함으로써 AndroidManifest.xml의 절대 경로를 지정할 수 있다!

    • javac - 옵션 추가: -AandroidManifestFile=/path/to/AndroidManifest.xml
    • Eclipse - "Properties > Java Compiler > Annotation Processing"으로 가서 Processor options에서 add
    • 다른 빌드 시스템, IDE - customization page를 살펴보길

    연관된 포스팅

    [AndroidAnnotations_EnhancedComponent] - @EActivity - http://juranosaurus.blogspot.kr/2015/08/androidannotationsenhancedcomponent.html

    위의 포스팅을 참고하여 Enhance activities에 대해서 더 알아보자 ^0^

    참고 논문 및 사이트

    1. androidannotations wiki 중 "FirstActivity" - https://github.com/excilys/androidannotations/wiki/FirstActivity

    Robolectric 사용하기 - 1

    Android Annotations Framework에 작동여부 테스트는 전부 Robolectric 2.4로 작성되어 있다. 현재는 Robolectric는 3.0 버전이 나온상태이며 내가 한번 Upgrade 해볼까 했지만 Upgrade to Robolectric 3.0에 이미 대기중이라고 Issue가 올라온 상태였다. 따라서 Robolectric에 대한 사용방법에 대한 숙지 및 추후에 Android 개발을 할때 사용하기 위해서 Robolectric에 대한 이해를 하고 넘어가기로 결정하였다. 가장 먼저 이번 포스팅에서는 Robolectric이 사용하는 방법에 대한 설명이 나와있는 Robolectric 홈페이지에 대한 번역을 진행해보았다.

    Robolectric

    Android Code에서 테스트 지향 개발을 하도록 도와준다.

    Android emulator나 device를 이용하여 테스팅하는것은 매우 느립니다. App에 대한 Building, Deploying, Launching 과정은 1분에서 그 이상 걸리기 마련입니다. 그것을 TDD로 수행할 방법은 따로 존재하지 않습니다. 그래서 조금 더 나은 방법을 Robolectric이 제공해줍니다. IDE안 안에서 Android Test를 직접 수행해보려고 해보셨나요? 아마 시도를 해봤다면 java.lang.RuntimeException: Stub 때문에 실패를 했을 것 입니다. Robolectric은 Android SDK에서 테스트 지향 개발을 할 수 있도록 도와주는 unit test framework 입니다. Test는 JVM에서 몇초안에 수행됩니다. Robolectric 코드는 아래와 같습니다.

    @RunWith(RobolectricTestRunner.class)
    public class MyActivityTest {
    
      @Test
      public void clickingButton_shouldChangeResultsViewText() throws Exception {
        MyActivity activity = Robolectric.setupActivity(MyActivity.class);
    
        Button button = (Button) activity.findViewById(R.id.button);
        TextView results = (TextView) activity.findViewById(R.id.results);
    
        button.performClick();
        assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");
      }
    }
    

    Robolectric은 Android SDK classes를 재사용이 가능하도록 만들어 주고 있습니다.

    SDK, Resources, Native Method에 대한 Emulation이 가능합니다

    Robolectric은 view, resource loading 과 같이 C에서 구현되는 native코드를 다룰 수 있습니다. 이것은 우리가 실제 디바이스에서 하는것 처럼 테스팅을 할 수 있도록 도와줍니다. 또한 이것은 분명한 SDK method를 개발하는 것을 쉽게 할 수있도록 해줍니다.

    Emulator 밖에서 테스트 할 수 있습니다.

    Robolectric은 emulator없이 컴퓨터에서 또는 CI JVM 환경에서 지속적인 테스트 개발이 가능합니다. 따라서 packaging, installing 과 같은작업이 필요하지 않고 몇분이 소요되는 작업에 대해서 빠르게 처리하고 리팩토링하는데 도웁을 줍니다.

    Mock을 만드는 작업이 필요하지 않습니다.

    Robolectric을 대체하는 다른 프로젝트 Mockito같은 것은 Android SDK에 대한 Mock작업이 필요합니다. 이것은 유효한 방법이지만, 종종 App개발에 있어 테스트 케이스가 더 많이 구현되는 상황이 발생됩니다. Robolectric은 black box testing 을 지원하기 때문에 Android 구현에 있어 Refactroing과 같은 작업에 초점을 맞출수 있도록 도와줍니다.


    Get Started

    Get Started

    Robolectric는 Gradle 과 Maven 모두 잘 동작합니다. 프로젝트를 새로 추가하게 된다면 Gradle 빌드 방식으로 하는 것을 권장합니다. (따라서 이후에 나오는 Maven 에 대한 내용은 생략하였다. 어차피 다들 이제 Gradle쓰니까). Robolectric은 Build Tool 이 무엇이든지 사용가능합니다.

    Gradle에서 Build 수행하기

    가장 먼저 Robolectric을 사용하기 위해서는 build.gradle에 아래의 Dependency를 추가해야 한다.

    testCompile "org.robolectric:robolectric:3.0
    

    그리고 우리가 사용하는 테스트 상단에 아래 코드와 같이 Annotation을 작성해주면 된다.

    @RunWith(RobolectricGradleTestRunner.class)
    @Config(constants = BuildConfig.class)
    public class SandwichTest {
    }
    

    Build system에서 생성되는 BuildConfig.class가 가지고 있는 constant field에 대해서 반드시 기술해줘야 합니다. Robolectric은 Gradle을 사용하여 프로젝트를 Build 할때 output path를 계산하기 위하여 class 안의 constant를 사용하기 때문입니다. 이 값들이 만약에 정의 되지 않았다면, Robolectric 프로젝트는 manifest, resources 또는 assets을 찾을 수가 없기 때문에 반드시 작성해주어야 합니다.

    Android Studio에서 Build 수행하기

    Robolectric은 Android Studio 1.1.0 과 그 이상에서 동작합니다. Gradle에서 Build를 수행하는 방법에 대해서 동일하게 작성을 해줍니다. 그리고 Build Variants Tab에 있는 Unit test support를 활성화 시킨뒤 Test 를 실행하면 됩니다.

    Sample Project

    Robolectric Sample 은 https://github.com/robolectric/robolectric-samples 여기서 확인 가능합니다. Gradle Starte Project는 https://github.com/robolectric/robolectric-samples여기서 확인 가능합니다.


    첫번째 테스트 케이스 작성하기

    가장 먼저 애플리케이션 환영 페이지 layout을 생성합니다.

    
    
    
        

    다음 Click 버튼을 통하여 LoginActivity가 켜지는 Activity을 작성하도록 하겠습니다.

    public class WelcomeActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.welcome_activity);
    
            final View button = findViewById(R.id.login);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
                }
            });
        }
    }
    

    해당 Activity에서 테스트 할 부분은 "Login" 버튼을 클릭하였을 때 그것이 정상적인 intent로 수행되는지 여부를 판단해야 합니다. Roblectric unit testing framework를 이용하여 LoginActivity가 실제 켜지지 않아도 WelcomeActivity가 정상적으로 intent 됬는지를 아래 코드로써 확인이 가능합니다.

    @RunWith(RobolectricTestRunner.class)
    public class WelcomeActivityTest {
    
        @Test
        public void clickingLogin_shouldStartLoginActivity() {
            WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
            activity.findViewById(R.id.login).performClick();
    
            Intent expectedIntent = new Intent(activity, WelcomeActivity.class);
            assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
        }
    }
    

    참고 논문 및 사이트

    - Robolectric http://robolectric.org/

    [Android Databass] SQLiteOpenHelper, ContentProvider, OrmLite, AndroidAnnotations @OrmLite - 2

    Android에서 Sqlite를 사용하는 것에 있어서 두번째 포스팅입니다. 기본적인 OrmLite Library사용과 AndoridAnnotations를 적용하여 OrmLitef를 사용하는 방법에 대하여 포스팅하도록 하겠습니다.

    3. OrmLite

    ORM 이라는 의미는 Object Realational Mapping의 약자입니다. OrmLite는 많은 표준 Library의 복잡성과 오버헤드를 피하면서 SqlDatabase를 접근 할 수 있게 하는 경량 Library입니다. 이것은 다양한 JDBC를 지원할뿐만아니라, Sqlite와 함께사용하는 Android OS Database API의 native call 또한 지원한다.(Homepage참고) Orm 에 Java 표준 기술은 Java Persistence API로 JPA가 요즘 많이 사용되고 있다고 한다. Spring과 같은 웹 프로젝트에서 이런 Java Library를 사용하는 것과 같이 Android 계열에서 Sqlite 사용시 Orm 을 사용할 경우 더 편하게 사용할 수있으니 해당 Library는 앞으로 봤을때 사용성이 높은 Library라고 생각합니다.

    Github Repository 에 가면 아래의 작성한 소스코드 전체를 확인할 수 있습니다. OrmLite에 대한 상세한 설명은 더 파악한 후에 포스팅하도록 하겠습니다. 이 포스팅에서는 사용법 위주로 설명하도록 하겠습니다.

    가장 먼저 gradle에 ormlite에 대한 dependency를 추가해주어야 합니다.

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.0.1'
        ...
    
        compile 'com.j256.ormlite:ormlite-android:4.46'
        ...
    }
    

    저는 OrmLiteDBHelper라고 class 이름을 설정하고 해당 class는 OrmLiteSqliteOpenHelper를 상속받아야 합니다. 또한 상속받게 되면 아래와 같이 생성자와 onCreate, onUpgrade에 대해서 implement를 반드시 해줘야 한다. 그리고 SQLiteOpenHelper 에서 해줬던것 처럼 데이터베이스 이름과 버전을 설정해줘야한다. 저는 이 부분에 대해서 Constant Class를 따로 두고 받아오도록 구현하였습니다.

    package com.juranoaa.sqlite.common;
    
    public class Constant {
        public static final class SQLite {
            public static final Integer ORM_DB_VERSION = 1;
            public static final String ORM_DB_NAME = "ORMTest.db";
        }
    }
    
    package com.juranoaa.sqlite.ormlite;
    
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    
    import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
    import com.j256.ormlite.support.ConnectionSource;
    import com.juranoaa.sqlite.common.Constant;
    
    public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{
    
        public OrmLiteDBHelper(Context context) {
            super(context, Constant.SQLite.ORM_DB_NAME, null, Constant.SQLite.ORM_DB_VERSION);
        }
    
        @Override
        public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
    
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
    
        }
    }
    

    그리고 onCreate와 onUpgrade method에 아래와 같이 구현해야 합니다. onCreate에는 Database생성, onUpgrade에는 Database 삭제후 생성과 같은 함수 내용을 작성해야 합니다. 이 부분을 아래와 같이 작성해줍니다. 이때 사용되는 것은 " TableUtils " 에 정의 되어있고 데이터베이스를 생성해주는 쿼리문을 작성해줍니다.

    package com.juranoaa.sqlite.ormlite;
    
    public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{
    
    //...
    
    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            TableUtils.createTable(connectionSource, House.class);
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }
    // ...
    
    }
    

    여기서 House.class 와 같은 경우는 아래와 같이 정의가 필요합니다. 이렇게 작성해주게 되면 자동으로 CREATE 문을 생성하여 Sqlite Database에 입력해줍니다. 자세한 내용은 Database를 참고 하시기 바랍니다.

    package com.juranoaa.sqlite.ormlite;
    
    public class House {
    
        // auto increment
        @DatabaseField(generatedId = true)
        Integer id;
    
        @DatabaseField
        String name;
    
        public House() {
            // must exist
        }
    }
    

    그리고 두가지 method를 정의하여 사용할 수 있습니다. getDao(class)의 경우에는 SQLExcpetion이 throw 되는 method이고 getRuntimeExceptionDao(class)은 Exception 에 대해서 RuntimeException으로 처리하여 사용하는 method가 제공됩니다. 이에 대한 구현은 아래와 같이 표현됩니다.

    package com.juranoaa.sqlite.ormlite;
    // ...
    
    public class OrmLiteDBHelper extends OrmLiteSqliteOpenHelper{
        // ...    
        private RuntimeExceptionDao<House, Integer> houseRuntimeExceptionDao = null;
        // ...
        
        public RuntimeExceptionDao<House, Integer> getHouseDataDao() {
            if (houseRuntimeExceptionDao == null) {
                houseRuntimeExceptionDao = getRuntimeExceptionDao(House.class);
            }
            return houseRuntimeExceptionDao;
        }
        // ...
    }
    

    이렇게 구성하면 OrmLite를 사용할 수 있게됩니다. 또한 해당 Library는 apt를 통해 compile 시점에 작동하는 library가 아닌 runtime시에 작동하는 library입니다. 그리고 Activity에서 아래 코드와 같이 사용하면 사용할 수 있습니다.

    
    @EActivity(R.layout.main)
    public class AASQLiteActivity extends OrmLiteBaseActivity<OrmLiteDBHelper> {
    
        @ViewById
        TextView textView;
    
        @AfterViews
        void afterViews(){
            RuntimeExceptionDao<House, Integer> houseDao = getHelper().getHouseDataDao();
            List<House> houseList = houseDao.queryForAll();
            textView.setText(houseList.toString());
    
            for(House house : houseList){
                House updateHouse = new House(house.getId(), house.getName()+"_update");
                houseDao.update(updateHouse);
            }
            List<House> updateHouseList = houseDao.queryForAll();
            textView.setText(textView.getText()+updateHouseList);
        }
    }
    
    

    Activity는 OrmLiteBaseActivity를 상속받고 Generic 부분은 앞에서 작성한 클래스 이름이 작성되게 됩니다. (OrmLiteDBHelper) 그리고 getHeleper()를 통하여 OrmLiteDBHelper에서 작성한 getHouseDataDao method를 호출하여 사용하면 된다.

    해당 Dao에 대해서 create, update, delete, queryForAll 과 같은 method을 이용하여 SQLite 데이터 베이스에 CRUD 작업을 할 수 있습니다. 이렇게 사용할 경우 상속받을 Activity에 type과 method를 작성을 해야하는데 이런 불편함을 조금 더 개선한게 AndroidAnnotations에 @OrmLite입니다.

    4. AndroidAnnotations @OrmLite @EProvider

    가장먼저 AndroidAnnotations의 @OrmLite를 사용하기 위해서 Gradle에 추가정보를 입력해야 합니다. 해당 부분은 4.0-SNAPSHOT 기준입니다. 현재 Release 버전인 3.3.2를 사용한다면 기존 apt만 사용해도 사용할 수 있습니다.

    def AAVersion = "3.3.2"
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.0.1'
        apt "org.androidannotations:androidannotations:$AAVersion"
        compile "org.androidannotations:androidannotations-api:$AAVersion"
        compile 'com.j256.ormlite:ormlite-android:4.46'
    }
    
    def AAVersion = "4.0-SNAPSHOT"
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.0.1'
        apt "org.androidannotations:androidannotations:$AAVersion"
        compile "org.androidannotations:androidannotations-api:$AAVersion"
    
        compile 'com.j256.ormlite:ormlite-android:4.46'
        apt "org.androidannotations:ormlite:$AAVersion"
        compile "org.androidannotations:ormlite-api:$AAVersion"
    }
    

    @OrmLite 사용에 있어서 기존에 앞에 사용하던 Class는 그대로 유지를 하여 사용하면 됩니다. 그리고 객체를 반환했을때 사용할 수 있도록 하는 객체하나가 필요합니다.

    package com.juranoaa.sqlite.ormlite;
    
    import com.j256.ormlite.dao.Dao;
    import com.j256.ormlite.dao.RuntimeExceptionDao;
    
    public class HouseRuntimeExceptionDao extends RuntimeExceptionDao<House,Integer>{
    
        public HouseRuntimeExceptionDao(Dao<House, Integer> dao) {
            super(dao);
        }
    }
    

    위의 객체를 정의하고 Activity에서 아래와 같이 정의하고 사용하면 @OrmLite Annotation을 사용할 수 있습니다.

    @EActivity(R.layout.main)
    public class AASQLiteActivity extends Activity{
        ...
        @OrmLiteDao(helper = OrmLiteDBHelper.class)
        HouseRuntimeExceptionDao houseDao;
        ...
    }
    

    위의 코드는 Github Repository에서 전체 코드를 확인할 수 있습니다.

    이상으로 Android 에서 Database를 접근하는 방법에 대해서 두개의 포스팅에서 검토해보았습니다. ContentProvider의 경우 Android Docs를 보게되면 Application간에 데이터를 공유해야 되는 경우에만 필요하다고 작성되어있습니다. 따라서 CRUD에 대한 쿼리 작성이 불편한 점으로 인해 하나의 Application에서만 사용하는 DB를 ContentProvider로 쓰는것은 올바르지 않다고 생각합니다. 그래서 제 생각에는 OrmLite를 이용하면 조금 더 이런 코드에 대한 리팩토링을 할 수있다고 생각합니다. 이상 끝.

    Friday, October 9, 2015

    [Android Databass] SQLiteOpenHelper, ContentProvider, OrmLite, AndroidAnnotations @OrmLite - 1

    이 블로그는 4개의 Documentation 을 참고하여 작성되었습니다. [ SQLiteOpenHelper Docs, ContentProvider Docs, OrmLite Docs, AndroidAnnotations @EProvider ]

    팀원 중 한명이 AndroidAnnotations 를 기반으로한 주소록 어플을 빠르게 만들고 있는데, 그러던 중 SQLiteOpenHelper 클래스를 사용하여 쓰는 것을 볼 수 있었다. 초기 개발시에는 해당 클래스를 기반으로 개발하고 ContentProvider를 쓰고 @EProvider를 써본다고 해서.. 갑자기 3가지의 차이점이 궁금하게 되었다. 그래서 Android Docs에 가서 해당 내용에 대해서 분석해보았다.

    1. SQLiteOpenHelper

    먼저 SQLiteOpenHelper를 설명하자면 클래스 이름 그대로 Android 에서 내장 되어 사용되는 SQLite를 사용하는데 도와주는 Helper역할을 하는 클래스이다. Database Creation, Version Management, 즉, 데이터베이스 생성과 버전관리를 담당하는 부분이다. 해당 Class를 상속받는 클래스를 생성하면 아래의 코드와 같이 onCreate(SQLiteDatbase)onUpgrade(SQLiteDabase, int int) 2개의 method 구현이 반드시 이뤄져야 하며 생성자 또한 존재해야한다.

    package com.juranoaa.sqlite;
    
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    
    public class BaseSQLiteOpenHelper extends SQLiteOpenHelper{
    
        public BaseSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
            super(context, name, factory, version);
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
    
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
        }
    }
    

    1.1. Constructor(Context, String, SQLiteDabase.CursorFactory, int) 생성자

    해당 생성자의 경우에 중요한 부분이 있습니다. 역할은 당연히 database에 대한 create, open, manage하기 위한 부부이지만 해당 되는 부분은 getWritableDatabase(), getReadableDatabase() method를 부르기 전까지 데이터베이스 자체는 생성되지 않는 다는 점입니다. 앞으로 나올 부분에서도 해당 부분에 대한 언급을 계속하게 되기때문에 "데이터베이스 자체가 생성되지 않는다"라는 것은 반드시 숙지하고 넘어가시기 바랍니다.

    1.2. onCreate(SQLiteDatabase), onUpgrede(SQLiteDatabae)

    method 명과 같이 데이터 베이스를 생성하거나, 데이터베이스를 업그레이드 하는 역할을 하는 method입니다. 만약 Sqlite로 id, title, content를 가진 게사판을 구현한다고 하면 다음과 같이 작성됩니다.

    public class Constant {
    
        public static final class SQLite {
            public static final Integer DB_VERSION = 1;
            public static final String DB_NAME = "AATest.db";
    
            public static final String TABLE_NAME = "board";
            public static final String DB_CREATE =  "CREATE TABLE " + TABLE_NAME + " ("+
                                                    " id INTEGER PRIMARY KEY AUTOINCREMENT "+
                                                    " title VARCHAR NOT NULL"+
                                                    " content VARCHAR NOT NULL"+
                                                    ")";
            public static final String DB_DROP =    "DROP TABLE IF EXIST " + TABLE_NAME ;
        }
    }
    
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    
    public class BaseSQLiteOpenHelper extends SQLiteOpenHelper{
    
        public BaseSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
            super(context, name, factory, version);
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(Constant.SQLite.DB_CREATE);
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL(Constant.SQLite.DB_DROP);
            onCreate(db);
        }
    }
    

    1.3. getReadableDatabase(), getWritableDatabase()

    두 개의 method는 특징이 있습니다. SQLiteOpenHelper의 경우 onCreate, onOpen, onUpgrade method 들이 생성자 시점에 불리는 것이 아니라 해당 method들이 호출될때 불리게 됩니다. 이렇게 구현이 되는 이유는 생성 또는 업데이트 과정에서 시간이 올래걸리게 된다면 Application이 호출하는데 늦게 활성화가 되기 때문입니다. 따라서 해당 method가 Application main thread에서 불리게 하는것은 되도록 지양해야합니다.

    2. ContentProvider

    ContentProvider는 AndroidApplication에서 application에서 제공하는 가장 중요한 부분 중 하나 입니다.Data는 Encapsulation(은닉화)되어있으며, ContentResolver를 이용하여 인터페이스간 통신을 제공해줍니다. 따라서 ContentProvider는 Application간에 데이터를 주고 받고자할 때 필요한 부분입니다. 만약에 Application간에 Data share가 발생하지 않는다면 굳이 contentProvider를 사용하는 것이아닌 SQLiteDatabase를 직접적으로 사용하면 됩니다.

    (이 부분이 Docs에 쓰여져있긴 하지만 굳이 Database에 Directly 접근할 필요가 있을까? 라는 의문이 드네요).

    그리고 아래와 같이 ContentProvider를 사용하게 되면 6개의 method가 반드시 작성되어 있어야만합니다.

    public class BaseContentProvider extends ContentProvider{
        @Override
        public boolean onCreate() {
            return false;
        }
    
        @Nullable
        @Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
            return null;
        }
    
        @Nullable
        @Override
        public String getType(Uri uri) {
            return null;
        }
    
        @Nullable
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            return null;
        }
    
        @Override
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            return 0;
        }
    
        @Override
        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
            return 0;
        }
    }
    

    2.1. Constructor() 생성자

    저는 이 생성자가 가장 중요한 부분이라고 생각합니다. ContentProvider는 Application 실행시점에 application main thread를 사용하여 실행합니다. 따라서 아까 위에서 말했듯이 SQLiteOpenHelper를 통해 구현한 클래스에 대한 실행이 생성자 부분에서 실행되도록 하면 안됩니다. 해당 부분이 실행되게 되면 application이 늦게 시작이 될 수 있기 때문에 이부분이 가장 중요한 부분이라고 생각합니다.

    2.2. onCreate()

    Docs에 존재하는 이 method에 대한 설명 또한 시간이 오래걸리는 작업, Application 시작시 느려지는 것은 수행하지 않도록 해야 한다고 권장하고 있습니다. 따라서 SQLiteOpenHelper를 이용해서 Database의 생성부분을 관리하도록 짜는 것이 중요합니다. 또한 onCreate() method 내부에서 getReadableDatabase()getWriteabaleDatabase() method를 부른다면 Database 생성을 main thread에서 수행하는것과 마찬가지가 되기 때문에 이부분을 가장 중요시하게 다뤄야 합니다.

    ContentProvider를 작성하면 다음과 같다.

    Constant.class

    public class Constant {
    
        public static final class SQLite {
            public static final Integer DB_VERSION = 1;
            public static final String DB_NAME = "AATest.db";
        }
    }
    

    Board.class

    public class Board {
    
        private Integer id;
    
        private String title;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public Board() {}
    
        public Board(Integer id, String title) {
            this.id = id;
            this.title = title;
        }
    
        @Override
        public String toString() {
            return "Board{" +
                    "id=" + id +
                    ", title='" + title + '\'' +
                    '}';
        }
    }
    

    BoardProvider.class

    public class BoardProvider extends ContentProvider {
        private static final String TAG = BoardProvider.class.getSimpleName();
    
        public static final String TABLE_NAME_BOARD = "dbBoard";
        private static final String AUTHORITY = "com.juranoaa.sqlite";
    
        public static final Uri CONTENT_URI_BOARD = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME_BOARD);
    
        public static final String COL_ID = "_id";
        public static final String COL_TITLE = "title";
    
        /** create db string */
        public static final String DATABASE_CREATE_BOARD =
                "CREATE TABLE " + TABLE_NAME_BOARD +
                " (" + COL_ID + " INTEGER PRIMARY KEY,"
                + COL_TITLE + " TEXT );";
    
        private static BoardDataBaseHelper mDbHelperBoard;
        private SQLiteDatabase mDbBoard;
    
        @Override
        public boolean onCreate() {
            mDbHelperBoard = new BoardDataBaseHelper(getContext());
            return true;
        }
    
        @Override
        public Cursor query(Uri uri, String[] projection, String selection,
                            String[] selectionArgs, String sortOrder) {
            mDbBoard = mDbHelperBoard.getReadableDatabase();
            Cursor cursor = mDbBoard.query(TABLE_NAME_BOARD, projection, selection,
                    selectionArgs, null, null, sortOrder);
            return cursor;
        }
    
        @Override
        public String getType(Uri uri) {
            return null;
        }
    
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            mDbBoard = mDbHelperBoard.getWritableDatabase();
            mDbBoard.insertOrThrow(TABLE_NAME_BOARD, null, values);
            return null;
        }
    
        @Override
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            mDbBoard = mDbHelperBoard.getWritableDatabase();
            int count = mDbBoard.delete(TABLE_NAME_BOARD, selection, selectionArgs);
            return count;
        }
    
        @Override
        public int update(Uri uri, ContentValues values, String selection,
                          String[] selectionArgs) {
            mDbBoard = mDbHelperBoard.getWritableDatabase();
            int count = mDbBoard.update(TABLE_NAME_BOARD, values, selection, selectionArgs);
            return count;
        }
    
        private static class BoardDataBaseHelper extends SQLiteOpenHelper {
    
            private static final String TAG = BoardDataBaseHelper.class.getSimpleName();
    
            public BoardDataBaseHelper(Context context){
                super(context, Constant.SQLite.DB_NAME, null, Constant.SQLite.DB_VERSION);
            }
    
            @Override
            public void onCreate(SQLiteDatabase db) {
                db.execSQL(DATABASE_CREATE_BOARD);
    
            }
    
            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                Log.w(TAG, "invoked! " + oldVersion + " -> " + newVersion + ", it'll destroy old data");
                db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME_BOARD);
                onCreate(db);
    
            }
        }
    }
    

    BoardDao.class

    public class BoardDao {
    
        public static final String COLS_BOARD_ARR[] = {
                BoardProvider.COL_ID,
                BoardProvider.COL_TITLE
        };
    
        private BoardDao() {} 
    
        /**
         * Insert new Board to DB.
         */
        public static void insertNewBoard(Context ctx, Board board) {
            ContentValues Values = new ContentValues();
            ContentResolver cr = ctx.getContentResolver();
    
            Values.put(BoardProvider.COL_TITLE, board.getTitle());
    
            cr.insert(BoardProvider.CONTENT_URI_BOARD, Values);
        }
    
        public static List getAllBoards(Context ctx) {
            List boards = new ArrayList();
    
            ContentResolver cr = ctx.getContentResolver();
            Cursor c = cr.query(BoardProvider.CONTENT_URI_BOARD,
                    COLS_BOARD_ARR, null, null, null);
    
            if (c != null && c.getCount() > 0) {
                c.moveToFirst();
                while (c.isAfterLast() == false) {
                    boards.add(new Board(
                            c.getInt(c.getColumnIndex(BoardProvider.COL_ID)),
                            c.getString(c.getColumnIndex(BoardProvider.COL_TITLE))));
                    //LogUtil.w("map.put(" + uid + "," + persona_name+ ")");
                    c.moveToNext();
                }
            }
            c.close();
    
            return boards;
        }
    
        public static Board getBoardById(Context ctx, int id) {
            Board board = null;
    
            ContentResolver cr = ctx.getContentResolver();
            Cursor c = cr.query(BoardProvider.CONTENT_URI_BOARD,
                    COLS_BOARD_ARR, BoardProvider.COL_ID + "=?",
                    new String[]{String.valueOf(id)}, null);
            if (c != null && c.getCount() > 0) {
                c.moveToFirst();
                while (c.isAfterLast() == false) {
                    board = new Board(
                            c.getInt(c.getColumnIndex(BoardProvider.COL_ID)),
                            c.getString(c.getColumnIndex(BoardProvider.COL_TITLE)));
                    c.moveToNext();
                }
            }
            c.close();
    
            return board;
        }
    }
    

    AndroidManifest.xml

    
    
    
        
    
            
            
        
    
    
    

    Github 링크는 다음과 같습니다. https://github.com/JuranoSaurus/AndroidAnnotationsSampleProject/tree/sqlite.1.0

    -------------------------------------------

    다음 포스팅에서 OrmLite와 AndroidAnntation EProvide를 이용한 DB 구성에 대한 포스팅을 하도록 하겠습니다. 끝...

    참고 논문 및 사이트

    1. [Android/안드로이드] Content Provider ( 콘텐트 프로바이더 ) 에 대한 모든 것. http://aroundck.tistory.com/236

    Tuesday, October 6, 2015

    Gradle 기반으로 Android Annotation Apt Processor 만들기.

    이 포스팅은 Android Annotation Processing Setup using Gradle - Jens Diller을 기반으로 작성하였습니다.

    Annotation Processing은 다른 포스팅에서도 설명했듯이 Compile Time에 java에서 annotation 에 대한 스캔과 처리를 하는 것 입니다. 물론 AndroidAnnotations Framework를 사용하면 편리한 기능을 많이 사용할 수 있지만 개발자라면 누구나 이것이 어떻게 동작하고 만들어지는 방법에 대해서 이해를 할 것이라고 생각합니다. 그래서 Android Project에서 Annotation Processor를 빌드하는 방법을 설명하고자 합니다.

    1. Android Project 만들기

    "Start a new Android Studio project" 을 선택하여 새로운 프로젝트를 생성합니다.

    저는 Application Name : Sample , Company Domain : jurano.com , Package name : com.jurano.annotation으로 프로젝트를 진행하도록 하겠습니다. 이 부분은 사용자가 지정하기 나름이니 알아서 하시면 됩니다.

    알맞은 Minimum SDK Version을 선택한 뒤 Next

    일단 우리에게는 Activity가 필요없으니 "Add No Activity" 선택 하고 Finish를 선택해줍니다. 그러면 프로젝트 생성끝!

    2. 새로운 모듈 추가하기.

    Annotation Processor를 적용하려면 두개의 Module이 더 필요합니다. 생성할 모듈은 2개로 Annotation API를 정의하는 Java Library와 Annotation Processor 를 정의하는 라이브러리로 나누어지게 됩니다.File → New → New Module... 을 가면 아래와 같은 화면이 뜨게됩니다.

    여기서 Java Library를 선택해줍니다.

    선택한 뒤에 Library name : api, Java package name : com.jurano.annotation , Java class name : FullScreen 으로 정의한뒤 프로젝트를 생성시켜줍니다.그럼 API에 대한 Java library 생성이 완료된것이며 위와 똑같은 방식으로 Annotation Processor 프로젝트를 생성하여 줍니다.

    Library name : compiler, Java package name : com.jurano.annotation , Java class name : AnnotationProcessor


    3. Gradle Build 설정하기

    Annotation Processor를 사용하기 위해서는 총 4가지의 build.gradle 파일을 수정해야 합니다. 앞으로 상위 build, api, compiler, app 순으로 설정해야 할 정보들에서 설명하도록 하겠습니다.

    3.1. Project: Sample build.gradle (부모 Gradle 파일)

    상위 build 파일에서 먼저 android-apt 프로세스 사용을 위한 빌드 정보를 입력해주어야 합니다. 기존에 생성되는 코드는 아래와 같이 구성 됩니다. 첫 줄에 작성되 있는 것과 같이 해당 gradle 파일은 Top-level 빌드 파일입니다. 따라서 아래에 있는 build 파일들에 대한 repostiory 는 jcenter이며, 위에 작성되어 있는 buildscritp 부부은 android를 build할때 사용되는 build tool에 대한 version을 나타내게 됩니다.

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.3.0'
    
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    allprojects {
        repositories {
            jcenter()
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    

    위에 android-apt에 대한 모듈을 dependency에 추가시켜야 하위 gradle 파일에서 해당되는 프로세서를 사용할 수 있습니다. 따라서 아래의 라인을 dependecy에 추가시켜주어야 합니다.

    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:1.3.0'
            classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add
        }
    }
    

    현재 1.8 버전까지 나와 있으며 해당버전 사용에 있어 문제가 없기 때문에 최신버전을 이용하였습니다. But 1.7 버전 사용시 android processor가 정상적으로 실행되지 않았습니다. 이유는 잘 모르겠으나 1.7 버전은 사용하지 않는것이 좋습니다.(2틀날림...) 해당 프로젝트의 상위 모듈은 위의 한 줄만 추가하면 모두 끝났습니다.

    3.2. Api library

    api library의 경우에는 java이고 다른 library를 물리지 않고 사용자 정의 Annotation을 정의하는 부분이기 때문에 다른 사항은 필요하지 않습니다. 아래 코드는 아까 java library 생성시 생성된 코드로 다른 것을 추가하거나 제거할 필요는 없습니다.

    apply plugin: 'java'
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
    }
    

    하지만 위에 있는 포스팅에 "One more thing: If you're using Java 8 as your SDK, you need to ensure and tell the compiler to build using Java 7 or you will get the following error during pre-dexing:" 다음과 같은 문구가 존재합니다. 결국 Java 1.8을 사용하고 있다면 1.7을 사용하기 위해서 1.7하도록 하라는 것인데 이 부분에 있어 저는 1.7을 사용하기 때문에 추가하지 않았습니다.

    sourceCompatibility = JavaVersion.VERSION_1_7  
    targetCompatibility = JavaVersion.VERSION_1_7  
    

    3.3. Compiler library

    Compiler library의 경우에는 추가사항이 존재합니다. 먼저 기존 build정보는 아래와 같습니다.

    apply plugin: 'java'
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
    }
    

    위의 정보에서 내가 생성한 library인 api에 대한 사용권한을 추가해야합니다. 따라서 아래의 코드를 추가해줍니다. 아래의 dependency를 추가함으로써 compiler에서 api에서 생성한 annotation에 대한 사용이 가능하게 됩니다. 따라서 무조건 필요한 부분입니다.

    apply plugin: 'java'
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile project (':api') // add
    }
    

    3.4. App Sample

    마지막으로 App에 대한 build정보입니다. 해당 build 파일이 수정할 부분이 가장 많습니다. 먼저 아래는 기본 소스 코드 입니다.

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 23
        buildToolsVersion "23.0.1"
    
        defaultConfig {
            applicationId "com.jurano.annotation"
            minSdkVersion 22
            targetSdkVersion 23
            versionCode 1
            versionName "1.0"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.0.0'
    }
    

    가장먼저 추가해야 하는 것은 android-apt를 사용할 것이라는 정의 입니다. 2번째 라인에 추가되어 있는 부분으로 이를 사용함을 정의합니다. 그리고 dependency 에 내가 사용하는 내 로컬 library인 api와 compiler에 대한 빌드 정보를 아래와 같이 추가해줍니다. 컴파일러 library의 경우에는 apt 를 이용하여 빌드되게 아래 코드와 같이 구성해야 합니다.

    apply plugin: 'com.android.application'
    apply plugin: 'com.neenbedankt.android-apt'
    
    android {
        compileSdkVersion 23
        buildToolsVersion "23.0.1"
        ...
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:23.0.0'
        compile project(':api')
        apt project(':compiler')
    }
        
    

    위와 같이 작성하면 모든 build 파일에 대한 설정을 완료하였습니다. Annntation Processor에 대한 동작원리를 이해한다면 충분히 따라오실 수 있으셨을 겁니다.

    4. Java Class 생성하기

    이제 사용자가 정의한 새로운 Anntation과 그것에 대한 사용법을 보도록 하겠습니다. 이것도 동일하게 api → compiler → app 방식으로 진행하도록 하겠습니다.

    4.1. API 클래스 구현

    먼저 저희가 장으로 생성한 Class는 아래와같이 구성이 되어있습니다. 하지만 저희는 @interface가 필요 함으로 과감하게 바꿔줍니다. 또한 api라는 package안에 annotation을 만들도록 하겠습니다.

    package com.jurano.annotation;
    
    public class FullScreen {
    }
    
    package com.jurano.annotation.api;
    
    public @interface FullScreen {
    }
    

    또한 타켓이나 용도 지정이 가능해지는데 이부분에 있어서는 이 포스팅을 참고해주시기 바랍니다. [What is java annotation 03] - Creating your Own Annotations?


    4.2. Annotation Processor 정의

    이제 Annotation에 대한 정의는 위와 같이 하면 끝난것입니다. 이제 다음으로 Annotation Processor를 정의하러 Compiler 모듈로 이동합니다. 기존에 있는 소스코드를 package를 추가하여 아래와 같이 이동시켰습니다. 또한 Annotation Processor를 사용하기 위해서 시작이 되는 class는 AbstractProcessor class를 상속받아야 함으로 변경된 코드는 이를 반영하였습니다.

    package com.jurano.annotation;
    
    public class AnnotationProcessor {
    }
    
    package com.jurano.annotation.compiler;
    
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.element.TypeElement;
    
    public class AnnotationProcessor extends AbstractProcessor{
        @Override
        public boolean process(Set set, RoundEnvironment roundEnvironment) {
            return false;
        }
    }
    
    

    또한 AnnotationProcessor를 실행시키기 위해서는 몇가지 작업이 필요합니다. 따라서 "compiler/src/main/resources/META-INF/services"에 폴더를 생성해주고 "javax.annotation.processing.Processor"파일을 생성해주어야 합니다. 구조가 만들어지면 다음과 같은 프로젝트 형식이 되게 됩니다.

    해당 파일의 내부에는 Annotation Processor가 처음 실행되는 class의 이름을 작성해줍니다. 저는 Annotation Processor의 시작점은 AnnotationProcessor로 할것이기 때문에 아래와 같이 작성해주었습니.

    com.jurano.annotation.compiler.AnnotationProcessor
    

    위와같이 작성해주고 함수 내부는 클래스를 생성하는 함수를 작성해야 하는데 함수를 작성하는 모듈에 대한 내용은 다른 포스팅을 통하여 설명하겠습니다. 여기서는 간단하게 Annotation이 있을 경우에 특정경로에 파일이 작성되도록 하는 것으로 대체 하겠습니다. 코드는 아래와 같습니다.

    package com.jurano.annotation.compiler;
    
    import com.jurano.annotation.api.FullScreen;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.Collections;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    
    public class AnnotationProcessor extends AbstractProcessor{
    
        @Override
        public Set getSupportedAnnotationTypes() {
            return Collections.singleton(FullScreen.class.getCanonicalName());
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set set, RoundEnvironment roundEnvironment) {
            for(Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(FullScreen.class)){
                TypeElement annotatedClass = (TypeElement) annotatedElement;
                // TODO Code generate
                String annotatedClassName = annotatedClass.getSimpleName().toString();
    
                try {
                    FileOutputStream output = new FileOutputStream("E://log.txt");
                    output.write(annotatedClassName.getBytes());
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }
    
            return false;
        }
    }
    
    

    4.3. Android Application에서 빌드되는지 확인하기.

    마지막으로 Android Application에서 이것이 사용이 가능한 것인지 이제 구현하여 확인해보도록 하겠습니다. 만들어진 App에서 ConfirmFullScreen이라는 class 를 생성하고 Annotation을 위에다가 선언해 줍니다. 코드는 아래와 같습니다.

    package com.jurano.annotation;
    
    import com.jurano.annotation.api.FullScreen;
    
    @FullScreen
    public class ConfirmFullScreen {
    }
    

    빌드를 실행하게 되면 아래와 같이 파일에 텍스트가 작성되게 됩니다. 필요한 기능에 대해서 Java class를 generated 하도록 소스를 작성한다면 자주 반복되는 코드와 같은 기능을 손쉽게 작업을 할 수 있습니다.

    소스는 GradleAndroidAnnotationProcessor에서 확인할 수 있습니다. 다음 포스팅은 소스코드 생성에 대해서 다뤄보도록 하겠습니다. 끝

    5. 현재문제점

    Compiler 부분의 java process가 죽지않아서 매번 프로세스를 강제 종료하고 있습니다. 이부분은 해결해서 다시올리도록하겠습니다.. ㅠ 참고한 프로젝트에서도 compiler가 죽지 않는 문제가 있어서 이 부분은 확인이 필요한 부분입니다.