지난 번에는 AndroidAnnotations(AA) 오픈소스 라이브러리의 @Click 어노테이션을 흉내내봤습니다. 이번에는 @Background@UiThread 어노테이션을 흉내내보죠.


먼저 @Background와 @UiThread 어노테이션 클래스를 작성합니다.


(1) Background.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 백그라운드 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 *

 * ※ NOTE

 * - 이 어노테이션은 작업진행상태 스핀다이얼로그 표시, 작업취소 처리, 작업 콜백메소드 등의 기능을 지원하지 않는다.

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.METHOD)

public @interface Background {

}


(2) UiThread.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 UI 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ delay

 * - delay 파라미터 설정시 해당 밀리초 이후에 실행된다. 기본값은 0이다.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface UiThread {

    long delay() default 0;

}


자 이제 위 어노테이션이 붙은 메소드를 백그라운드 쓰레드나 UI쓰레드에서 실행시키기 위해서 AOP(AsepctJ)의 힘을 빌려봅니다.


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;


/**

 * <pre>

 * {@link Background}, {@link UiThread} 어노테이션이 붙은 메소드가

 * 백그라운드 및 UI쓰레드에서 실행되도록 애스펙트로 처리한다.

 * </pre>

 */

@Aspect

public class BackgroundAnnotationAspect {


    private Handler uiHandler = new Handler(Looper.getMainLooper());


    private Executor threadExecutor = Executors.newCachedThreadPool();


    @Around("execution(@com.mycompany.async.Background * *.*(..))")

    public void proceedInBackground(final ProceedingJoinPoint pjp) {

        threadExecutor.execute(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (final Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        });

    }


    @Around("@annotation(uithread) && execution(@com.mycompany.async.UiThread * *.*(..))")

    public void proceedInUiThread(final ProceedingJoinPoint pjp, UiThread uithread) {

        runInUiThread(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        }, uithread.delay());

    }



    /**

     * {@link Runnable}을 UI쓰레드에서 실행한다.

     *

     * @param run

     * @param delayMillis 지연시간. 밀리초

     */

    private void runInUiThread(Runnable run, long delayMillis) {

        if (delayMillis <= 0) {

            // 현재 쓰레드가 UI 쓰레드인 경우 그냥 run 실행.

            if (Looper.getMainLooper().getThread() == Thread.currentThread()) {

                run.run();

                return;

            }


            uiHandler.post(run);

            return;

        }


        uiHandler.postDelayed(run, delayMillis);

    }

}


다 됐습니다. 아래처럼 쓰면 됩니다. (꼭 액티비티 내의 메소드에 국한되지 않고 아무 클래스에서든 사용가능합니다)


public class TestAsyncActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ... ...

        Button test = ....;
        test.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                doBackground();
            }
        });
    }

    @Background

    private void doBackground() {

        URL url = new URL("http://mycompany.com/xxx");

        URLConnection conn = url.openConnection();

        byte[] contents = FileCopyUtils.copyToByteArray(conn.getInputStream());

        doUiThread(new String(contents));

    }


    @UiThread

    private void doUiThread(String contents) {

        Toast.makeText(getApplicationContext(), contents, Toast.LENGTH_LONG).show();

    }


유용하게 쓰시길 바랍니다.



Posted by 에코지오
,

RoboGuice와 더불어 안드로이드 코딩을 굉장히 심플하게 만들어주는 AndroidAnnotations(AA) 오픈소스 라이브러리는 @Click 이라는 아주 유용한 어노테이션을 제공합니다. 

뷰의 ID가 설정된 어노테이션을 메소드에 선언해주면 설정된 뷰에 click 이벤트가 발생할 때 해당 메소드가 실행됩니다. 

기존에는  view.setOnClickListener(new OnClickListener() { ...}) 와 같은 OnClickListener 익명 클래스로 도배된 지저분한 코딩이 필요했었죠.


@Click(R.id.myButton)  

void myButtonClicked() { 

 ... ... // R.id.myButton을 누르면 이 메소드가 호출됩니다.

}  


@Click

void anotherButton(View clickedView) {

 ... ... // R.id.anotherButton 버튼을 누르면 이 메소드가 호출됩니다.

}


@Click({R.id.myButton1, R.id.myButton2})  

void myButtonsClicked() { 

 ... ... // R.id.myButton1 또는 R.id.myButton2를 누르면 이 메소드가 호출됩니다.

}  


AA를 사용하지 않는(사용하기 싫은?) 환경을 위해 비슷한 기능을 제공하는 우리만의 @Click을 만들어 보겠습니다. 

먼저 Click 어노테이션을 작성합니다. value는 클릭 이벤트가 발생하는 뷰의 id 값입니다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Click {

    int[] value() default -1;

}


@Click 어노테이션을 선언하는 메소드는 아래의 제약사항을 준수해야 합니다(AA의 @Click도 마찬가지입니다).

- 리턴 타입은 void만 가능

- 파라미터는 없거나 View 타입의 1개만 가능

- Exception을 던지는 것이 가능하지만 메소드 caller한테는 throw되지 않음 (AA에서는 아예 checked Exception도 못던집니다).


그리고 공통 액티비티 클래스에서 android.app.Activity의 onContentChanged() 메소드를 오버라이딩합니다.


    /**

     * 어노테이션에 설정된 뷰에 대해 클릭 이벤트 발생시 해당 메소드가 실행되도록 리스너 등록.

     *

     */

    @Override

    protected void onContentChanged() {

        super.onContentChanged();

        processClick(this);

    }


    public static void processClick(final Activity activity) {

        Class clazz = activity.getClass();


        for (final Method m : clazz.getDeclaredMethods()) {

            Click click = m.getAnnotation(Click.class);//m.isAnnotationPresent(Click.class)

            if (click == null)

                continue;


            Class<?>[] params = m.getParameterTypes();

            final boolean hasViewParameter = (params.length > 0);


            int[] ids = click.value();

            for (int id : ids) {

                View view = activity.findViewById(id);

                if (view != null) {

                    view.setOnClickListener(new OnClickListener() {

                        public void onClick(View view) {

                            try {

                                if (hasViewParameter) {

                                    m.invoke(activity, view);

                                } else {

                                    m.invoke(activity);

                                }

                            } catch (IllegalAccessException e) {

                                throw new RuntimeException(e);

                            } catch (InvocationTargetException e) {

                                throw new RuntimeException(e);

                            }

                        }

                    });

                }

            }


        }

    }


다 됐습니다. 공통 액티비티를 상속받은 액티비티에서 AA의 그것과 동일한 기능을 수행하는 @Click 어노테이션을 사용할 수 있습니다. 간단하죠?


※ 단, AndroidAnnotations의 @Click과 차이점이 몇가지 있습니다.

- AA에서는 setContentView()에 @Click 처리코드가 작성되지만, 위에서는 onContentChanged()에서 @Click을 처리하도록 했습니다. 

- AA의 @Click은 뷰의 id가 주어지지 않은 경우 메소드 이름으로 뷰 id를 자동으로 찾지만 위 코드는 그러한 기능은 제공하지 않습니다.

- AA에서는 컴파일타임에 Java Annotation Processing에 의해 @Click 처리 코드가 생성되지만 위 코드는 런타임에 @Click이 붙은 메소드를 이벤트리스너를 등록합니다.



Posted by 에코지오
,

1. RoboGuice 2.0

- RoboActivity의 onContentChanged() 메소드에서 View 주입(inject)


    @Override

    public void onContentChanged() {

        super.onContentChanged();

        RoboGuice.getInjector(this).injectViewMembers(this); //View 주입

        eventManager.fire(new OnContentChangedEvent());

    }



2. AndroidAnnotations 2.5

- 언더바 붙은 Activity(Java Annotation Processing에 의해 생성)의 setContentView() 메소드에서 View 주입 및 @Click 처리


    @Override

    public void setContentView(int layoutResID) {

        super.setContentView(layoutResID);

        afterSetContentView_();

    }



    private void afterSetContentView_() {

        //View 주입 코드

        mSaveBtn = ((Button) findViewById(id.save_btn)); 

        // ... ...


        // @Click 처리 코드

        {

            View view = findViewById(id.save_btn);

            if (view!= null) {

                view.setOnClickListener(new OnClickListener() {



                    public void onClick(View view) {

                        save();

                    }


                }

                );

            }

        }

        //... ...


        init();

        //... ...

    }


Posted by 에코지오
,

안드로이드 코딩시 도움이 되는 대표적 오픈소스 프레임워크/라이브러리인 RoboGuice와 AndroidAnnotations에 대한 비교 메모입니다.


1. RoboGuice (RG)

- Injection 대상 : View, Resource, 시스템서비스, POJO 등

- 향상된 유틸리티 클래스 제공 : RoboAsyncTask, Ln 등

- 액티비티 생명주기 Event 전달을 통한 모듈화 메커니즘 제공 : 액티비티에 대한 강한 커플링을 제거.

- Activity는 RoboActivity를 상속받아야 함

- Activity가 아닌 클래스에서도 안드로이드 컴포넌트 inject 가능

- 구글 Guice에 의존 : 강력하고 충실한 inject 기능 제공

- 런타임시에 inject 및 어노테이션을 처리하므로 성능하락 존재

- 용량 큼 : 590KB



2. AndroidAnnotations (AA)

- Injection 대상 : View, Resource, 시스템서비스, POJO(제한적) 등

- 다양한 유틸리티성 어노테이션 제공. inject외에 다양한 작업을 어노테이션을 통해 처리

 : 쓰레드작업(UiThread, Background,..), 이벤트처리(Click, ItemClick,...), REST통신 등

- 컴파일 타임에 inject 및 어노테이션 처리한 자식클래스를 생성 : 런타임 성능하락 없음

- 특정 Activity 상속 필요없음

- java annotation processing 설정 필요

- 새 액티비티 클래스 작성후 Android Manifest.xml 수정이 필요 => 액티비티 클래스명 끝에 언더바를 추가해줘야함

 : 예를들어 ".xxx.MyActivity" -> ".xxx.MyActivity_" 

- 명시적으로 액티비티 사용시에 언더바 붙은 액티비티를 사용해야 함

- 용량 작음 : 53KB



* 참고자료

http://blog.springsource.org/2011/08/26/clean-code-with-android/

http://blog.softwaregeeks.org/archives/647

Posted by 에코지오
,