Android/App개발2021. 5. 25. 13:48

하단에서 올라오는 팝업 다이얼로그

 

위와 같이 하단에서 솟구치는 팝업 다이얼로그는 Dialog를 상속하여 Animation을 적절히 활용하면 구현이 가능하다.

https://github.com/samse/SlideUpDialog

 

 

samse/SlideUpDialog

Contribute to samse/SlideUpDialog development by creating an account on GitHub.

github.com

 

 

Posted by 삼스
Android/App개발2021. 3. 26. 17:13

MultiWindow를 지원할지에 대해 프로그램 설계시 미리 고려할 사항이 있겠다.
지원하고 한다면 screenOrientaion로 가로/세로로 고정하지 말고 모든 사이즈의 화면에 대응되도록 레이아웃을 고려하여 작성해야한다.
액티비티의 최소사이즈를 지정하면 그 지정한 사이즈 이하로는 조정이 되지 않는다.

screenOrientation를 지정한 상태에서 실행중이 앱을 멀티윈도우로 진입시키면 액티비티가 재시작된다.
이를 막기 위해서는 다음 속성을 추가하면 되는데(https://medium.com/androiddevelopers/5-tips-for-preparing-for-multi-window-in-android-n-7bed803dda64)

android:configChanges="screenSize|smallestScreenSize
|screenLayout|orientation"

이 경우에도 일부 단말에서는 재시작되는것을 확인하였다.

만일 앱을 세로나 가로로 고정해야 한다면 난감한 상황이 될것인데 이 런 경우는 그냥 멀티윈도우 기능을 끄는것이 나을것이다.

android:resizeableActivity="false"

애초에 멀티윈도우를 지원하고자 한다면 고정하지 않고 화면을 설계하는것이 나을것 같다.

자세한 내용은 아래 링크 참조

https://developer.android.com/guide/topics/ui/multi-window

Posted by 삼스
Android/App개발2020. 4. 6. 14:45

https://medium.com/google-exoplayer/playback-notifications-with-exoplayer-a2f1a18cf93b

 

PlayerNotificationManager를 통해 미디어재생 시 알림센터에 정보를 표시하고 제어할 수 있다.

1. PlayerNotificationManager인스턴스 생성
2. PlayerNotificationManager.MediaDescriptionAdapter 로 재생중인 미디어 아이템 정보를 제공
3. 플레이어에 연결, 리소스가 소거되었을 때는 연결해제

PlayerNotificationManager 인스턴스 생성
 activity나 fragment의 onCreate에서 생성한다. 

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  ...
  playerNotificationManager = new PlayerNotificationManager(
      this,
      new DescriptionAdapter(), 
      CHANNEL_ID,
      NOTIFICATION_ID);
}

채널아이디, 알림아이디, 그리고 재생중인 미디어 정보를 제공하기 위해 MediaDescriptionAdapter를 확장한 DescriptionAdapter를 파라메터로 넘긴다.

private class DescriptionAdapter implements
    PlayerNotificationManager.MediaDescriptionAdapter {
    
  @Override
  public String getCurrentContentTitle(Player player) {
    int window = player.getCurrentWindowIndex();
    return getTitle(window);
  }

  @Nullable
  @Override
  public String getCurrentContentText(Player player) {
    int window = player.getCurrentWindowIndex();
    return getDescription(window);
  }

  @Nullable
  @Override
  public Bitmap getCurrentLargeIcon(Player player,
      PlayerNotificationManager.BitmapCallback callback) {
    int window = player.getCurrentWindowIndex();
    Bitmap largeIcon = getLargeIcon(window);
    if (largeIcon == null && getLargeIconUri(window) != null) {
      // load bitmap async
      loadBitmap(getLargeIconUri(window), callback); 
      return getPlaceholderBitmap();
    }
    return largeIcon;
  }

  @Nullable
  @Override
  public PendingIntent createCurrentContentIntent(Player player) {
    int window = player.getCurrentWindowIndex();
    return createPendingIntent(window);
  }
}

플레이어에 연결은 다음과 같이 한다.

player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
playerNotificationManager.setPlayer(player);

플레이어가 해제되기 전에 먼저 연결을 끊어야 한다.

playerNotificationManager.setPlayer(null);
player.release();
player = null;


커스텀하기

 앱의 테마에 맞도록 알림을 커스텀하는 다양한 방법이 존재한다. 재생컨트롤의 동작여부를 설정하고 알림속성을 설정할 수 있도록 매니져가 기능을 제공한다. 이 속성들은 기본값을 가지고 있고 쉽게 바꿀수 있다.

재생컨트롤 액션들

 기본 컨트롤액션을 제공한다. 재생/일시정지, FF/REW, Next/Previous, Stop등이 있으며 생략도 가능하다

// omit skip previous and next actions
playerNotificationManager.setUseNavigationActions(false);
// omit fast forward action by setting the increment to zero
playerNotificationManager.setFastForwardIncrementMs(0);
// omit rewind action by setting the increment to zero
playerNotificationManager.setRewindIncrementMs(0);
// omit the stop action
playerNotificationManager.setStopAction(null);

커스텀액션도 CustionActionReceiver를 확장하여 구현하여 PlayerNotificationManager 생성자의 5번째 파라메터로 넣어서 구현 가능하다.

알림 속성

 알림매니저는 UI와 알림의 동작의 setter를 제공한다. 이는 NotificationCompat.Builder의 속성에 상응한다.

manager.setOngoing(false);
manager.setColor(Color.BLACK);
manager.setColorized(true);
manager.setUseChronometer(false);
manager.setSmallIcon(R.drawable.exo_notification_small_icon);
manager.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE);
manager.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);

MediaSession

 최종적으로 Google Assistant등을 지원하기 위해 MediaSession API를 사용하고 있다면 media style 알림의 장점을 모두 취하기 위해 session에 token을 셋팅할수 있다.

 playerNotificationManager.setMediaSessionToken(token);




 

Playback Notifications with ExoPlayer

Displaying a notification with playback controls is a best practice for media apps on Android. For audio playback in the background it’s…

medium.com

 

Posted by 삼스
Android/App개발2020. 2. 12. 14:45

https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

 

코틀린 표준 함수들 일부는 어디에 사용하는지 명확하게 알지 못한다. 이에 대해 그 차이점을 명확하게 설명하고 어떤걸 사용해야 하는지 소개하고자 한다.

범위함수(Scoping functions)

run, with, T.run, T.let, T.also 그리고 T.apply에 대해 집중할거다. 

다음은 run함수의 범위지정기능을 설명하는 간단한 코드이다.

불러오는 중입니다...

 

fun test() {
  var mood = "I am sad"

  run {
    val mood = "I am happy"
    println(mood)   // I am happy
  }

  println(mood) // I am sad
}



test함수 내의 분리된 범위에서 mood는 재정의 되고 "I am happy"가 프린트된다. run 범위내에 제한된다.
이게 범위지정함수인데 별로 유용해보이지 않지만 범위지정으로써의 장점이 있다. 먼가를 반환할 수 있다.

아래코드를 보면 show()를 두번 호출하지 않고 두개의 view에 적용이 가능하다.

run {
  if (firstTimeVIew) introView
  else normalView
}.show()



범위지정함수의 3가지 속성

범위지정함수를 좀더 흥미있게 만들기 위해 3가지 속성으로 나누고 이를 통해 각각을 구별할 수 있다.

1. Normal vs. extension function

with, T.run 은 상당히 유사하다.

with(webview.settings) {
  javascriptEnabled = true
  databaseEnabled = true
}

webview.settings.run {
  javascriptEnabled = true
  databaseEnabled = true 
}



두가지 차이점은 일반함수냐 확장함수냐이다. 그래서 각각의 장점은 멀까?

webview.settings가 null일 수 있다고 생각해보자

// 별로다
with(webview.settings) {
  this?.javascriptEnabled = true
  this?.databaseEnabled = true
}

// 이게 더 좋아
webview.settings?.run {
  javascriptEnabled = true
  databaseEnabled = true
}


이 경우는 T.run 사용이 더 유용하다.


2. This vs. it 인자

strVal?.run {
  println("The length of this String is $length")
}

strVal?.let {
  println("The length of this String is $it.length")
}


T.run의 경우 block: T.()의 형태를 가지며 따라서 범위내에서 this로 참조가 가능하다. this의 경우 일반적으로 생략이 가능하므로 $this.length가 아니라 $length가 가능하다.

T.let의 경우 block: (T)의 형태를 가지며 인자를 따로 명시하지 않으면 it으로 접근이 가능하다.


위 예를 보면 T.run이 좀더 암시적이어서 우세해 보이나 T.let의 장점도 존재한다.

* let은 함수/멤버 변수를 외부 클래스 함수/멤버와 구별하기가 더 좋다.
* 파라메터로 this가 전달되면 생략이 불가한데 it이 더 짧고 명확하다
* let은 변수명을 it이 아니라 다른 이름으로 부여가 가능하다.

value?.let { myname -> 
  println("The name is $myname");
}


3. this 또는 다른 타입의 반환

value?.let {
  println("The length of val is ${it.length}")
}

value?.also {
  println("The length of val is ${it.length}")
}


두가지의 차이점은 let은 임의 타입을 반환하고 also는 동일한 this타입을 반환한다.
둘다 체이닝함수에 유용한다. T.let은 연산된 결과를 다음 체인에 전달하고 T.also는 동일한 객체를 전달한다.

val original = "abc"

original.let {
  prinln("The original string is $it") // 원래 문자열
  it.reversed() // 리버스된 문자열 반환
}.let {
  println("Ths reversed string is $it") 
  it.length. // 문자열길이 반환
}.let {
  println("The length is $it") // 전달받은 타입은 숫자
}

original.let {
  prinln("The original string is $it")
  it.reversed()
}.also {
  println("Ths reversed string is $it") // "abc" 프린트
  it.length
}.also {  
  println("The length is $it")  // "abc" 프린트
}

original.also {
  prinln("The original string is $it")
}.also {
  println("Ths reversed string is ${it.reversed()}") // 리버스된 문자열 프린트
}.also {  
  println("The length is ${it.length}")  // 문자열 길이 프린트
}


위 샘플을 보면 also가 더이상 무슨 소용이 있나 싶을 수 있지만 잘생각해 보면 좋은 장점이 있다.

1. 동일 객체에 대한 일련의 처리를 명확하게 구분할 수 있게 해준다.
2. 체이닝 빌드 연산을 만듦으로 객체를 스스로 조작하는데 파워풀한 기능을 제공한다.

두가지를 합쳐서 한번은 변환하고 이어서 그 객체를 조작하는 체이닝을 만들 수 있다.

// 일반적인 접근
fun makeDir(path: String): File {
  val result = File(path)
  result.mkdirs()
  return result
}

// 향상된 접근
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }


모든 속성 살펴보기

T.apply로 3가지 속성을 살펴보는것으로 더 많은것을 알 수 있다.

1. 함수의 확장이 필요한가?
2. 인자로서 this를 전달이 필요한가?
3. this를 반환이 필요한가?

// 일반적인 접근
fun createInstance(args: Bundle) : MyFragment {
  val fragment = MyFragment()
  fragment.arguments = args
  return fragment
}

// 향상된 접근
fun createInstance(args: Bundle) = MyFragment().apply { argument = args }
fun createInstance(args: Bundle) = MyFragment().also { it.argument = args }



this를 반환하기 때문에 체이닝이 가능하다.

// 일반적인 접근
fun createIntent(intentData: String, intentAction: String) : MyFragment {
  val intent = Intent()
  intent.action = intentAction
  intent.data = Uri.parse(intentData)
  return intent
}

fun createIntent(intentData: String, intentAction: String) = Intent()
    .apply { action = intentAction }
    .apply { data = intentData }

fun createIntent(intentData: String, intentAction: String) = Intent()
    .also { it.action = intentAction }
    .also { it.data = intentData }

위에서 설명한 표준함수들을 선택하는데 있어 선택장애자를 위한 표는 다음과 같다.

 




Posted by 삼스
Android/App개발2019. 6. 10. 14:15

대거2를 적용하는 3 가지 방법이 있다.

 

1번은 Component, Module, Scope으로 직접 모두 기술하는 방법이고

2번은 DispatchingAndroidInjector로 제공하는 방법

3번은 안드로이드에서 제공하는 기반클래스(DaggerActivity, DaggerFragment, ...)로 제공하는 방법이 있다.

 

여기서는 1번 방식으로 작업하는 방식을 설명한다.

여러가지 구성을 하는 방법이 있겠지만 내가 작업한 방식을 설명한다. 틀린 부분이 있다면 누구든 지적을 해주었으면 한다.

 

안드로이드에서 대거는 전역 및 로컬범위로 다양하게 scope을 지정하여 객체를 주입할 수 있다.

Application레벨은 보통 전역 객체를 Activity레벨은 로컬객체를 주입한다.

 

전역에서 GuiManager를 로컬에서 ItemAdapter를 주입할것이다. 

 

Module을 정의한다.

 

in AppModule.java

 

@Module

public class AppModule {

  private MyApplication mApp;

  public AppModule(MyApplication app) { mApp = app; }

  

  @Provides

  @Singletone

  GuiManager provideGuiManager() { return new GuiManager(); }

}

 

MyApplication을 멤버로 가지고 있으며 Scope을 Singletone으로 GuiManager를 주입하기 위한 Provider를 구현한다.

provide를 prefix로 하여 메서드를 정의하며 해당 클래스의 인스턴스를 생성하여 반환한다.

 

Component를 구현한다.

 

in AppComponent

 

@Singletone

@Component(modules = { AppModule.class }

public interface AppComponent {

  void inject(MyApplication app);

  void inject(MainActivity activity);

}

 

Application에서 GuiManager를 주입하는 예이다.

in MyApplication.java

 

public class MyApplication extends Application {

 

  AppComponent mComponent;

 

  @Inject

  protected GuiManager mGuiManager;

 

  @Override

  public void onCreate() {

    super.onCreate();

    mComponent = DaggerAppComponent.builder().appModule(new AppModule()).build();

    mComponent.inject(this);

  }

 

  public AppComponent getComponent() {

    return mComponent;

  }

 

대거는 주석을 기반으로 코드를 생성하며 Dagger를 prefix로하여 Component builder로 객체를 생성할 수 있다. 이렇게 만들어진 ㅡㅐ여component의 inject()를 호출하면 Module에서 provide prefix로 노출한 객체들이 바인딩되어 진다. 

 

이번에는 액티비티 스콥의 주입예이다. ItemAdapter를 MainActivity에서 주입하여 사용할것이다.

 

ActivityModule을 정의한다.

 

@Module

public class ActivityModule {

  private Activity mActivity;

  public ActivityModule(Activity activity) {
    mActivity = activity; 

  }

  

  @Provides @ActivityScope

  Activity activity() { return mActivity; }

 

  @Provides @ActivityScope

  ItemAdapter provideItemAdapter() {

    return new itemAdapter();

  }

}

 

provide prefix로 itemAdapter를 노출하고 있다.

ActivityComponent를 구현한다.

 

@ActivityScope

@Component(dependencies={AppComponent.class}, modules={ActivityModule.class})

public interface ActivityComponent {

  void inject(Activity activity);

}

 

dependencies를 추가하였다. AppComponent를 정의하여 GuiManager도 사용이 가능하다.

MainActivity에서 주입을 시전한다.

 

public class MainActivity extends AppCompatActivity {

  @Inject

  GuiManager guiManager;

  @Inject

  ItemAdapter itemAdapter;

 

  @Override

  public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

 

    ((MyApplication)getApplication()).getComponent().inject(this);

    DaggerActivityComponent activityComponent = DaggerActivityComponent.builder().appComponent(new AppComponent()).build();

    activityComponent.inject(this);

 

    guiManager.showAlert("hi!");

    itemAdapter.setDataSource(new MySource()); 

  }

  ...

}

 

MyApplication의 getComponent()를 가져와서 inject함으로써 GuiManager가 주입되었고 DaggerActivityComponent를 build하여 ItemAdapter를 주입하였다.

이 후 guiManager.showAlert("hi")나 itemAdapter.setDataSource(new MySource()) 같이 주입된 객체를 사용할 수 있다.

guiManager는 singletone의 스콥을 가지며 itemAdapter는 ActivityScope을 가진다.

 

ActivityScope이 무엇인가 싶을텐데.. 아래와 같이 정의된다.

 

@Scope

@Retention(RUNTIME)

public @interface ActivityScope {}

 

Scope을 하나 정의했는데 저리하면 액티비티에 종속되는 범위를 갖는 스콥 아노테이션이 만들어진다. 아노테이션 세계는 참..

 

의존성을 여러개로 나누어서도 관리할 수 있다. 모듈과 콤포넌트를 용도에 맞게 구성하여 사용하면 된다.

이번에는 UserData를 주입하기 위한 예제를 살펴보겠다.

 

@Module

public class UserModule {

  private Activity mActivity;

  public UserModule(Activity act) { mActivity = act; }

 

  @Provides @ActivityScope

  Activity activity() { return mActivity; }

 

  @Provides @ActivityScope

  UserData provideUserData() { reutrn new UserData(); }

}

UserData를 주입하기 위해 provideUserData메서드가 정의되었다.

 

@ActivityScope

@Component(dependencies={AppComponent.class}, modules={ActivityModule.class, UserModule.class})

public interface UserComponent {

  void inject(Activity activity);

}

 

AppComponent, ActivityModule등의 의존성도 포함하고 있으며 주입을 위해 inject가 정의되어 있다.

이 모듈을 사용하기 위해 SubActivity는 다음과 같이 구현된다.

 

public class SubActivity extends Activity {

  @Inject

  ItemAdapter itemAdapter;

  @Inject

  UserData userData;

 

  @Override

  public onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    DaggerActivityComonent.builder().appComponent((MyApplication)getApplication()).getComponent()).build().inject(this);

    DaggerUserData.builder().appComponent((MyApplication)getApplication()).getComponent()).build().inject(this);

  }

  ...

}

 

itemAdapter를 사용하기 위해 DaggerActivityComponent.builder()를 사용했고 userData를 사용하기 위해 DaggerUserData.builder()를 사용했다.

 

이와 같이 전역 및 지역적으로 객체를 주입해서 사용하는 일반적인 방법을 알아 보았다.

 

이 방법은 GuiManager, ItemAdapter, UserData등에 대해 모두 정확히 알고 있어야 하며 코드도 많다. 이 보다 적은 코드로도 적용이 가능한데 위에서 말한대로 두가지 방법이 더 있다. 이 에 대해서는 추가로 공부하여 정리하도록 할것이다.

 

 

 

Posted by 삼스
Android/App개발2019. 6. 10. 11:03

Dagger and android

 

https://dagger.dev/android

 

Dagger2 다른 의존성주입 프레임웍들에 비해 주요장점중에 하나는 엄격하게 만들어졌다는것이며 이는 안드로이드앱에서도 사용될 있음을 의미한다. 하지만 대거를 안드로이드 앱에서 사용하기 위해서는 고려할것들이 있다.

 

안드로이드가 자바로 작성이 되지만 스타일면에서 다르다. 보통 이런 차이점은 모바일의 성능고려를 수용하기 위해 존재한다.

하지만 많은 패턴들이 이와는 반대인 경우가 많다. Effective Java 많은 어드바이스들이 안드로이드에 적절하게 적용가능하다.

 

이런 목표를 달성하기 위해 대거는 컴파일된 바이트코드를 후처리하기 위해 프로가드에 의존한다. 이는 대거가 코드를 서버와 안드로이드 둘다 자연스럽게 코드를 만들어낼 있게 한다. 서로 다른 툴체인과 바이트코드를 만들어냄에도 불구하고

대거는 이른 보장하기 위해 프로가드 최적화에 호환되는 코드를 만들어낸다.

따라서 대거는 프로가드를 사용해야 한다.

 

Recommended ProGuard Settings

 

dagger.android

 

대거를 사용하는 안드로이드앱을 작성하는 가장 어려움중에 하나는 액티비티나 프래그먼트같은 안드로이드 프래임웍 클래스들이 OS 의해서 인스턴스화되지만 대거는 삽입된 모든 객체를 생성할 있는 경우 가장 잘동작한다는것이다. 대신에 생명주기 메서드내에서 멤버주입을 수행해야 한다. 이는 많은 클래스들이 다음의 형태와 같다는것을 의미한다.

 

public class FrombulationActivity extends Activity {

  @Inject Frombulator frombulator;

 

  @Override

  public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    // 작업을 먼저 하지 않으면 frombulator null일것이다.

    ((SomeApplicationBaseType) getContext().getApplicationContext())

        .getApplicationComponent()

        .newActivityComponentBuilder()

        .activity(this)

        .build()

        .inject(this);

    // ... now you can write the exciting code

  }

}

 

이는 몇가지 문제가 있다.

 

  1. 복사붙이기가 나중에 리팩터링하기 힘들다. 많은 개바자들이 그렇게들 하는데 일부만 멀하는지 이해한다.
  2. 근본적으로 이는 타입요청 주입이다. 이는 인젝터가 타입을 알아야 한다. 각각 타입이 아니라 인터페이스를 통해 이른 해결한다 해도 이는 의존성주입의 핵심원리를 깬다. 클래스는 어떻게 주입되는지 아무것도 몰라야 한다.

 

dagger.android 클래스들은 패턴을 단순화하기 위한 하나의 접근법을 제공한다.

 

 

Injecting Activity objects

 

  1. 애플리케이션 구성요소에 AndroidInjectionModule 설치해서 이러한 기본 유형에 필요한 모든 바인딩을 사용할수 있도록 하라.
  2. AndroidInjector<YoutActivity> 구현하는 @Subcomponent작성을 시작해라.  AndroidInjector.Factory<YourActivity> 확장한 @Subcomponent.Factory 있다.

 

@Subcomponent(modules = ...)

public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> {

  @Subcomponent.Factory

  public interface Factory extends AndroidInjector.Factory<YourActivity> {}

}

 

3. 서브콤포넌트를 정의하고 나서 모듈을 정의하여 콤포넌트 구조를 추가해라. 모듈은 서브콤포넌트 팩토리를 바인딩하여 애플리케이션에 주입할 콤포넌트를 추가한다.

 

@Module(subcomponents = YourActivitySubcomponent.class)

abstract class YourActivityModule {

  @Binds

  @IntoMap

  @ClassKey(YourActivity.class)

  abstract AndroidInjector.Factory<?>

      bindYourActivityInjectorFactory(YourActivitySubcomponent.Factory factory);

}

 

@Component(modules = {..., YourActivityModule.class})

interface YourApplicationComponent {}

 

2단계에서와 달리 서브콤포넌트와 팩토리가 메서드나 수퍼타입을 갖고 있지 않은 경우 @ContributesAndroidInjector 사용할 있다. 2, 3단계대신에 abstract모듈 메세드를 @ContributesAndroidInjector 함께 추가하여 당신의 액티비티를 반환할 있다. 그리고 서브콤포넌트에 설치하고자 하는 모듈을 기술하라.

서브콤포넌트가 스콥이 필요하면 스콥주석을 사용할수 있다

 

 

@ActivityScope

@ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })

abstract YourActivity contributeYourActivityInjector();

 

4. 이제 당신의 애플리케이션에 HasActivityInjector 구현하고 @Inject  DispatchingAndroidInjector<Activity> 지정하여 activityInjector()로부터 반환한다. 

 

public class YourApplication extends Application implements HasActivityInjector {

  @Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector;

 

  @Override

  public void onCreate() {

    super.onCreate();

    DaggerYourApplicationComponent.create()

        .inject(this);

  }

 

  @Override

  public AndroidInjector<Activity> activityInjector() {

    return dispatchingActivityInjector;

  }

}

 

5. 마지막으로 Activity.onCreate()메서드에서 AndroidInjection.inject(this) super.onCreate()호출전에 호출한다.

 

public class YourActivity extends Activity {

  public void onCreate(Bundle savedInstanceState) {

    AndroidInjection.inject(this);

    super.onCreate(savedInstanceState);

  }

}

 

 

어떻게 동작하나?

 

AndroidInjection.inject() Application으로부터 DispatchingAndroidInjector<Activity> 얻고 inject(Activity) 액티비티에 전달된다. 

DispatchingAndroidInjector 당신의 액티비티의 클래스(YourActivitySubcomponent.Factory) 위해 AndroidInjector.Factory 찾고, AndroidInjector(YourActivitySubComponent) 생성하고, inject(yourActivity) 당신의 액티비티를 전달한다.

 

프래그먼트 객체의 주입

 

프래그먼트를 주입하는것은 액티비티를 주입하는것 만큼 단순하다. 같은 방식으로 서브콤포넌트를 정의하고 HasActivityInjector HasFragmentInjector 변경하면 된다.

 

onCreate()에서 액티비티를 주입하는것 대신에 onAttach(..)에서 주입하면 된다.

 

액티비티에 정의된 모듈과 달리, 프래그먼트에 모듈을 설치할 위치를 선택할수 있다. 당신의 프래그먼트콤포넌트를 다른 프래그먼트 콤포넌트, 액티비티 콤포넌트 또는 애플리케이션 콤포넌트의 서브콤포넌트로 만들수 있다. 이는 당신의 프래그먼트를 필요로 하는 다른 바인딩에 의존한다. 콤포넌트의 위치를 결정하고 나서 대응되는 타입의 HasFragmentInjector 구현한다. 예를 들어 당신의 프래그먼트가 YourActivitySubcomponent에서 바인딩이 필요하면 다음과 같이 코딩할것이다.

 

public class YourActivity extends Activity

    implements HasFragmentInjector {

  @Inject DispatchingAndroidInjector<Fragment> fragmentInjector;

 

  @Override

  public void onCreate(Bundle savedInstanceState) {

    AndroidInjection.inject(this);

    super.onCreate(savedInstanceState);

    // ...

  }

 

  @Override

  public AndroidInjector<Fragment> fragmentInjector() {

    return fragmentInjector;

  }

}

 

public class YourFragment extends Fragment {

  @Inject SomeDependency someDep;

 

  @Override

  public void onAttach(Activity activity) {

    AndroidInjection.inject(this);

    super.onAttach(activity);

    // ...

  }

}

 

@Subcomponent(modules = ...)

public interface YourFragmentSubcomponent extends AndroidInjector<YourFragment> {

  @Subcomponent.Factory

  public interface Factory extends AndroidInjector.Factory<YourFragment> {}

}

 

@Module(subcomponents = YourFragmentSubcomponent.class)

abstract class YourFragmentModule {

  @Binds

  @IntoMap

  @ClassKey(YourFragment.class)

  abstract AndroidInjector.Factory<?>

      bindYourFragmentInjectorFactory(YourFragmentSubcomponent.Factory factory);

}

 

@Subcomponent(modules = { YourFragmentModule.class, ... }

public interface YourActivityOrYourApplicationComponent { ... }

 

 

Base Framework Types

 

DispatchingAndroidInjector 런타임에 해당클래스가 전용 AndroidInjector.Factory 찾기 때문에 기반클래스는 AndroidInjection.inject() 호출하고 HasActivityInjector/HasFragmentInjector/등을 구현해야 한다. 모든 서브클래스들이 해야 하는 일은 대응되는 @Subcomponent 바인딩해야 하는것이다. 대거는 복잡한 클래스구조가 없다면 DaggerActivity DaggerFragment같은 몇가지 기반클래스를 제공한다. 대거는 같은 목적으로 DaggerApplication 제공한다. 당신이 해야 일은 이를 확장하고 applicationInjector()메서드를 오버라이드하여 application 주입하는 콤포넌트를 반환한다.

 

다음 타입들 또한 제공된다

 

  • DaggerService DaggerIntentService
  • DaggerBroadcastReceiver
  • DaggerContentProvider

 

DaggerBroadcastReceiver AndroidManifest.xml 등록되어 있어야 한다. 

코드로 BroadcastReceiver 생성되면 대신에 생성자주입을 선호합니다.

 

지원라이브러리는 dagger.android.support 패키지이다

 

Build.gradle 다음을 추가해야 한다.

 

dependencies {

  compile 'com.google.dagger:dagger-android:2.x'

  compile 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries

  annotationProcessor 'com.google.dagger:dagger-android-processor:2.x'

}

 

 

When to inject

 

생성자 주입은 가능하면 선호된다. javac 셋팅되기전에 필드가 참조되지 않아서 널포인트를 피할수 있도록 해주기 때문에 선호된다.

멤버주입이 필요한 경우(위에서 언급된) 가능하면 빨리 주입하는것이 선호된다. DaggerActivity super.onCreate() 호출되기 전에 AndroidInjection.inject() 바로 호출하기 때문에 선호된다. 프래그먼트의 경우는 onAttach()에서 한다.

 

super.onCreate()보다 전에 AndroidInjection.inject() 먼저 호출하는것이 중요하다. 반대로 하면 컴파일에러가 발생한다.

 

Posted by 삼스
Android/App개발2019. 6. 5. 16:44

https://dagger.dev/users-guide

 

Dagger FactoryFactory 클래스를 대체한다. 보일러플레이트를 배제한 의존성주입을 구현한다. 이는 쓸모없는 오버엔지니어링을 방지하고 의미 있는 클래스에 집중할수 있게 한다. 의존성을 정의하고 어떻게 구성되는지 기술하면 된다.

 

테스트 아주 유용할뿐 아니라 재사용 가능하고 모듈의 변경이 가능하다. AuthenticationModule 여러 앱에서 사용 가능하고 DebugLoggingModule ProdLoggingModule 상황에 맞게 구성하여 사용할 있다.

 

Dagger2 최초료 풀스택 코드생성방식으로 구현되었다. 

 

커피메이커 샘플(https://github.com/google/dagger/tree/master/examples/simple/src/main/java/coffee) 대거를 설명하겠다.

 

대거는 당신의 클래스들과 이에 관련된 의존성을 정의하는 것으로 구성이 된다. Javax.inject.Inject 어노테이션으로 생성자와 필드를 지정한다.

 

클래스 생성자에 @Inject 사용하면 대거가 클래스인스턴스를 만들때 사용한다. 새로운 인스턴스를 요청하면 요구되는 파라메터들을 얻어내어 생성자를 만든다.

 

class Thermosiphon implements Pump {

  private final Heater heater;

 

  @Inject

  Thermosiphon(Heater heater) {

    this.heater = heater;

  }

 

  ...

}

 

대거는 필드를 바로 주입할수 있다. 다음 샘플은 두개의 필드를 얻어낸다.

 

class CoffeeMaker {

  @Inject Heater heater;

  @Inject Pump pump;

 

  ...

}

 

클래스가 @Inject 필드는 있는데 생성자가 @Inject 없다면 대거는 필요하면 필드를 주입하게 되겠지만 새로운 인스턴스를 생성하지는 않을 것이다.  인자가 없는 생성자를 추가해서 대거가 인스턴스를 생성할 있게 해주어야 한다.

 

메서드 주입도 지원하는데 생성자나 필드주입이 선호된다.

 

@Inject아너테이션이 없는클래스는 대거가 생성할 없다.

 

기본적으로 위에서 기술한 타입의 인스턴스를 생성함으로써 의존성을 만족시킨다. CoffeeMaker 요청하면  new CoffeeMaker() 인스턴스를 만들고 필드에 주입하도록 설정한다.

 

@Inject 항상 동작하지는 않는다.

  • Interface 생성될수 없다.
  • 3rd party 클래스는 어도테이션할수 없다.
  • 설정가능한 객체는 반드시 설정하여 사용해야 한다.

 

@Providers 어노테이션은 메서드에 의존성을 부여한다. 해당 메서드의 반환타입이 의존성객체이다.

 

예를 들어 provideHeader() Header 필요할대 마다 호출된다.

 

@Module

class DripCoffeeModule {

  @Provides static Heater provideHeater() {

    return new ElectricHeater();

  }

 

  @Provides static Pump providePump(Thermosiphon pump) {

    return pump;

  }

}

 

 

@Inject @Provides 어노테이트된 클래스는 의존성에 따른 객체 그래프를 만든다. main메서드나 안드로이드의 Application 그래프의 루트로 사용될수 있다.

 

대거2에서 의존성은 다음 샘플처럼 인수가 없고 원하는 형식을 반환하는 메서드가 있는 인터페이스로 정의되며 @Component주석과 모듈유형을 매개변수로 전달한다. 그러면 명세에 따라 코드가 생성된다.

 

@Component(modules = DripCoffeeModule.class)

interface CoffeeShop {

  CoffeeMaker maker();

}

 

프리픽스로 Dagger 붙은 동일한 이름의 클래스가 생성되며 builder().build()메서드로 인스턴스를 얻는다.

 

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()

    .dripCoffeeModule(new DripCoffeeModule())

    .build();

 

콤포넌트가 최상위타입이 아니면 생성된 콤포넌트 이름은 타입의 이름을 포함하여 밑줄로 구분하여 이름이 정해진다.

 

class Foo {

  static class Bar {

    @Component

    interface BazComponent {}

  }

}

 

경우 DaggerFoo_Bar_BazComponent

 

접근가능한 생성자를 갖는 모듈은 builder 생략될 있다. 

모든 @Provides메서드가 static이면 인스턴스의 구현이 필요없다. 때도 create()메서드로 builder없이 생성이 가능하다.

 

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

 

커피앱에서 대거가 생성한 구현체가 주입된 CoffeeShop객체를 사용하는 예이다.

 

public class CoffeeApp {

  public static void main(String[] args) {

    CoffeeShop coffeeShop = DaggerCoffeeShop.create();

    coffeeShop.maker().brew();

  }

}

 

샘플은 콤포넌트를 어떻게 생성하는지에 대한 몇가지 방법을 설명한다. 다른 바인딩방법을 알아보자

 

다음은 의존성으로 사용 가능하며 구성된 콤포넌트를 생성하는데 사용될 있다.

 

  • @Component.modules 의해 직접 참조되거나 @Module.includes 통해 참조죄는 @Module내에 @Provides메서드로 선언된 모듈
  • scope 지정되지 않는 @Inject생성자가 있는 타입이거나 콤포넌트 scope 일치하는 @Scope 주석이 있는 타입
  • 콤포넌트 의존성의 콤포넌트 제공 메서드
  • 콤포넌트 자체
  • 임의의 포함된 서브콤포넌트의 규정되지 않는 빌더
  • Provider 또는. Lazy wrapper 
  • Lazy Provider( Provider<Lazy<CoffeeMaker>>)
  • 모든 타입의 memberInjector

 

 

@Singletone주석을 갖는 @Provides 단일인스턴스를 사용한다.

 

@Provides @Singleton static Heater provideHeater() {

  return new ElectricHeater();

}

 

싱글톤을 사용할떄는 멀티스레드에 대해 대비해야 한다.

 

@Singleton

class CoffeeMaker {

  ...

}

 

대거2 콤포넌트 구현 범위를 연관시키기 때문에 이를 선언해야 한다. @Singletone @RequestScoped바인딩을 동일한 콤포넌트에 포함하는것은 의미가 없다. 수명주기가 다르기 때문인데 유의하여 콤포넌트 인터페이스 정의 범위 주석을 적용해야 한다.

 

@Component(modules = DripCoffeeModule.class)

@Singleton

interface CoffeeShop {

  CoffeeMaker maker();

}

 

콤포넌트는 여러 범위 주석을 적용할 있다. 

 

 

간혹 @Inject 생성되는 클래스가 @Provides메서드로 인스턴스로 만드는데 회수를 제한하고자 할수 있다. 하지만 모든 개개의 콤포넌트나 서브콤포넌트에서 사용되는 인스턴스가 동일한지 보장할 필요 없다. 이는 안드로이드 같은 메모리가 비용이 비싼 경우 유용하다.

 

경우 @Reusable 범위를 사용할 있다. @Reusable 사용되면 다른 범위와 다르게 모든 단일 콤포넌트에 연관되지 않는다 대신에 바인딩을 실제로 사용하는 콤포넌트들은 캐시하거나 객체를 생성한다.

 

이는 콤포넌트에 @Reusable 바인딘된 모듈을 정의하고 서브콤포넌트만 실제 해당 바인딩을 사용한다면 서브콤포넌트는 해당 바인딩을 캐시하게 된다. 조상을 공유하지 않는 두개의 서브콤포넌트가 바인딩 한다면 각각 따로 캐시할것이다. 조상이 이미 객체를 캐시한 경우 서브컴포넌트는 재사용한다.

 

콤포넌트가 바인딩을 한번만 호출한다는 보장이 안되므로 변경가능한 객체를 반환하는 바인딩에 @Reusable 적용하거나 동일 인스턴트를 참조하는것은 위험하다. 할당 횟수와 상관없는 불변객체에 대해 사용하는것이 안전하다.

 

@Reusable // It doesn't matter how many scoopers we use, but don't waste them.

class CoffeeScooper {

  @Inject CoffeeScooper() {}

}

 

@Module

class CashRegisterModule {

  @Provides

  @Reusable // DON'T DO THIS! You do care which register you put your cash in.

            // Use a specific scope instead.

  static CashRegister badIdeaCashRegister() {

    return new CashRegister();

  }

}

 

@Reusable // DON'T DO THIS! You really do want a new filter each time, so this

          // should be unscoped.

class CoffeeFilter {

  @Inject CoffeeFilter() {}

}

 

Lazy injections

 

지연된 주입이 가능하다. 샘플 참조.

 

class GrindingCoffeeMaker {

  @Inject Lazy<Grinder> lazyGrinder;

 

  public void brew() {

    while (needsGrinding()) {

      // Grinder created once on first call to .get() and cached.

      lazyGrinder.get().grind();

    }

  }

}

 

Provider Injections

 

여러개의 인스턴스가 필요한 경우가 있을 것이다. 경우 사용이 가능하다.

 

class BigCoffeeMaker {

  @Inject Provider<Filter> filterProvider;

 

  public void brew(int numberOfPots) {

  ...

    for (int p = 0; p < numberOfPots; p++) {

      maker.addFilter(filterProvider.get()); //new filter every time.

      maker.addCoffee(...);

      maker.percolate();

      ...

    }

  }

}

 

 

Qualifier

 

같은 클래스의 다른 인스턴스가 필요한 경우가 있다. 경우 qualifier주석을 사용할 있다. 

 

@Qualifier

@Documented

@Retention(RUNTIME)

public @interface Named {

  String value() default "";

}

 

자신만의 qualifier 만들거나 그냥 @Named 사용할수 있다. 필드와 관심있는 파라메터에 모두 사용가능하다.

 

class ExpensiveCoffeeMaker {

  @Inject @Named("water") Heater waterHeater;

  @Inject @Named("hot plate") Heater hotPlateHeater;

  ...

}

 

대응되는 @Provides메서드가 제공되어야 한다.

 

@Provides @Named("hot plate") static Heater provideHotPlateHeater() {

  return new ElectricHeater(70);

}

 

@Provides @Named("water") static Heater provideWaterHeater() {

  return new ElectricHeater(93);

}

 

 

Optional bindings

 

콤포넌트에 포함되지 않음에도 디펜던시를 추가하고 싶다면 @BindsOptionalOf 사용할 있다.

 

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

 

이는 @Inject생성자와 멤버 그리고 @Provides메서드는 Optional<CoffeeCozy>객체에 의존한다. 콤포넌트에  CoffeeCozy 바인딩되어 있으면 Optional 존재하고 없으면 존재하지 않을것이다.

 

다음중 하나를 주입할수 있다.

 

  • Optional<CoffeeCozy> : CoffeeCozy 바인딩에 @Nullable 없다면(아래 참조)
  • Optional<Provider<CoffeeCozy>>
  • Optional<Lzay<CoffeeCozy>
  • Optional<Provider<Lazy<CoffeeCozy>>>

 

Provider, Lazy 또는 Lazy Provider 주입할수 있지만 유용하지는 않다.

 

CoffeeCozy바인딩이 있고 바인딩이 @Nullable이면 Optional<CoffeeCozy>에서 컴파일에러가 발생한다. 왜냐하면 Optional null일수 없기 때문이다. Provider Lazy get 메서드에서 항상 null 반환할 있기 때문에 항상 다른 값이 주입될 있다.

 

콤퍼넌트에 optional 바인딩이 없어도 서브콤포넌트에 있을 있다. 서브콤포넌트에 기저타입의 바인딩이 있다면

 

 

Binding Instances

 

종종 콤포넌트를 빌드하는 시점에 유효한 데이터를 가질수 있다. 예를 들어 커맨드라인 인자를 필요로 하는 앱이 있는 경우 콤포넌트에 인자를 전달하고자 할것이다.

 

단일 인자로 이름을 받고 @UserName문자열로 주입하고자 한다면 콤포넌트 생성시 값을 전달하기 위해 @BindsInstance 주석을 추가할수 있다.

 

@Component(modules = AppModule.class)

interface AppComponent {

  App app();

 

  @Component.Builder

  interface Builder {

    @BindsInstance Builder userName(@UserName String userName);

    AppComponent build();

  }

}

 

위와 같이 정의하고 앱에서는 아래와 같이 사용한다.

 

public static void main(String[] args) {

  if (args.length > 1) { exit(1); }

  App app = DaggerAppComponent

      .builder()

      .userName(args[0])

      .build()

      .app();

  app.run();

}

 

콤포넌트에 주입되는 @UserName 문자열은 메서드가 호출될 빌더에 제공되는 인스턴스에 사용하게 될것이다. 콤포넌트를 빌드하기 전에 모든 @BindsInstance메서드들이 반드시 호출되어야 하면 null 아닌값이 전달되어야 한다(@Nullable 바인딩의 경우 제외).

 

@BindsInstance메서드가 @Nullable 경우 바인딩은 @Provides메서드의 경우처럼 nullable 간주된다. @Nullable 해야 하며 떄는 바인딩에 null 허용된다. Builder 유저는 호출을 생략할수 있으며 경우 콤포넌트는 null 다루어진다.

 

@BindsInstance메서드는 @Module 생성자 인자로 작성되어야 하고 해당 값이 바로 제공되어져야 한다.

 

Compile-time Validation

 

대거 아노테이션 프로세서는 업격해서 유효하지 않거나 완성되지 않는 바인딩에 대해 컴파일에러를 발생한다. 예를 들면 다음 예는 Executor 위한 바인딩이 누락상태의 모듈을 콤포넌트에 인스톨하고 있다.

 

@Module

class DripCoffeeModule {

  @Provides static Heater provideHeater(Executor executor) {

    return new CpuHeater(executor);

  }

}

 

컴파일하면 javac 바인딩 누락으로 반려하게 된다.

 

[ERROR] COMPILATION ERROR :

[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.

 

콤포넌트내의 아무 모듈에든 executor 위한 @Provides 주석을 추가해서 문제를 해결할 있다. @Inject, @Module그리고 @Provides주석이 각각 유효하면 @Component단계에서 바인딩간의 관계에 대한 모든 유효성검사가 발생한다. 대거1 모듈단계에서 유효성이 업격하게 책임지없다면 대거2 단계는 생략한다.

 

Compile time Code Generation

 

대거는 컴파일 CoffeeMaker_Factory.java CoffeeMaker_MembersInjector.java같은 이름의 파일들을 만들어 낸다. 파일들은 대거 구현의 디테일이다. 이들을 바로 사용할일은 없으며 디버깅은 해볼수 있겠다.코딩 참조할만한 코드들은 콤포넌트에 프리픽스로 Dagger 붙이것들이다.

 

Using Dagger In Your Build

 

Dagger-2.x.jar 런타임에 필요하다.  dagger-compiler-2.x.jar 코드자동생성을 위해 요하다. 자새한것은 다음 링크 참조 https://github.com/google/dagger/blob/master/README.md#installation

 

Posted by 삼스
Android/App개발2019. 6. 4. 14:02

https://github.com/google/dagger

 

google/dagger

A fast dependency injector for Android and Java. Contribute to google/dagger development by creating an account on GitHub.

github.com

의존성주입 라이브러리인 Dagger2를 알아보기 전에 Dagger1을 먼저 알아보자


2012년에 자바로 의존성을 주입하는 아이디어를 좋아한 스퀘어 개발자 몇명이 만들었다. 결국 어노테이션 기반의 코드생성과 guice와 비슷한 api를 갖지만 더 유연하고 빠르게 만들었다.
Dagger의 동작방식은 주입하고자 하는 모든 의존성을 위한 프로바이더를 포함하는 모듈들을 정의하여 객체그래프에 모듈을 로딩하고 결국 필요한 타겟에 내용을 주입하는 방식이다. 충분히 단순한구조(구현은 단순하지 않음)는 개발자가 코드를 분리할 수 있게 도움을 주고 클래스 상단에 객체를 생성하는 보기싫은 코드들을 라이브러리가 생성하는 인젝터들에 옮겨준다.
하지만 이런 잇점과 동시에 빠른 풀리퀘스트(하나 혹은 둘)를 해결하지 못하는 문제가 있다. 이는 기본아키텍쳑와 관련되어 있다.

- 런타임에 그래프 구성 : 성능에 악영향. 
- 리플랙션(예:Class.forName()) : 읽기 힘든 자동생성 코드와 프로가드의 악몽 발생
- 보기 싫은 자동생성코드 : 팩토리로 부터 수동 생성한 경우와 비교하여 아주 보기 싫음

결국 이런 이유로 수많은 이슈들이 트래킹되었고 이 이슈들은 Dagger2에서 해결하는것으로 이슈가 클로즈 되었다.

Dagger2

스퀘어의 원작자와 구글의 코어라이브러리 팀이 Dagger의 다음 이터레이션을 가져왔다.
약속대로 여러 이슈들이 해결되어 새버전이 배포되었다.

- 리프랙션 없음 : 프로가드 지옥도 해결
- 런타입 그래프구성 없음 : 성능 향상
- 추적가능 : 더 좋은 코드와 리플랙션 제거로 인해 더 읽이 쉽고 따라하기 쉬워짐.

Module

Dagger2에서 가장 변경이 없는것이 모듈이다. 주입하고자 하는 의존성의 프로바이더 메서드를 정의하면 된다. 

SharedPreference가 필요하다고 외쳐보자!

@Module 
public class ApplicationModule { 
    private Application mApp; 
    public ApplicationModule(Application app) { 
        mApp = app; 
    } 
    @Provides
    @Singleton
    SharedPreferences provideSharedPrefs() { 
        return PreferenceManager.getDefaultSharedPreferences(mApp); 
    } 
}

Component

Dagger를 아는 사람은 모듈에서 ingect = {} 가 사라진것을 알수 있다. Dagger2는 콤포넌트가 이를 대체한다.

@Singleton 
@Component(modules = {ApplicationModule.class}) 
public interface ApplicationComponent { 
    void inject(DemoApplication app); 
    void inject(MainActivity activity); 
}

주입하고자 하는 타겟들을 정의하면 되며 만일 누락되면 "cannot find method" 컴파일에러가 날것이다.

Application

다음은 콤포넌트를 담을 컨테이너이다.
응용 프로그램에 따라 더 복잡한 방법으로 구성 요소를 저장할 수도 있지만이 예제에서는 응용 프로그램 클래스에 저장된 단일 구성 요소로 충분하다.

public class DemoApplication extends Application { 
    private ApplicationComponent mComponent; 
    @Override 
    public void onCreate() { 
        super.onCreate(); 
        mComponent = DaggerApplicationComponent.builder() 
                    .applicationModule(new ApplicationModule(this))
                    .build(); 
    } 
    public ApplicationComponent getComponent() { 
        return mComponent; 
    } 
}

특별한것이 없다 해당 콤포넌트를 build하고 인스턴스화 해서 저장한다.
위 코드에서 이해가 안되는 부분이 있을수 있는데 DaggerApplicationComponent가 어디서 나왔냐 하는것일것이다. 이는 Dagger%COMPONENT_NAME%형식으로 명명되어 자동생성된 클래스이다. 컴파일러가 자동으로 만들어준다. 

Injection

마지막으로 위에서 설정한 인젝션 설정을 SharedPreference에 접근하고자 하는 액티비티내에서 사용하게 된다.

public class MainActivity extends Activity { 
    @Inject SharedPreferences mSharedPrefs; 
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        ((DemoApplication) getApplication())
                    .getComponent()
                    .inject(this); 
        mSharedPrefs.edit()
                    .putString("status", "success!")
                    .apply(); 
    } 
}

예상한데로 @Inject어노테이션은 변경되지 않았고 인젝션내에서만 일부 변경이 있다.  

Named Injection


동일 타입의 여러 객체가 필요한 경우 예를 들어 SharedPreference를 용도가 다르거나 다른파일을 지정하는 등의 경우 어떻게 할것인가?
이 경우 이름을 부여할 수 있다. @Named 어노테이션을 추가하는것만으로 가능하다.

@Provides 
@Named("default") 
SharedPreferences provideDefaultSharedPrefs() { … } 

@Provides 
@Named("secret") 
SharedPreferences provideSecretSharedPrefs() { … }

주입 대상에서도 동일하게..

@Inject @Named("default") SharedPreferences mDefaultSharedPrefs; @Inject @Named("secret") SharedPreferences mSecretSharedPrefs;


Lazy Injection

성능을 고려하면 지연된 주입이 반드시 필요하다 모든 의존성을 앱실행 초기에 하면 앱이 늦게 시작될테니까. 이를 위해 지연된 주입을 지 원한다.

@Inject Lazy mLazySharedPrefs; 
void onSaveBtnClicked() { 
    mLazySharedPrefs.get()
                .edit().putString("status", "lazy...")
                .apply(); 
}

위와 같이 정의 두면 호출전까지 주입이 되지 않는다. 이 후에는 주입된 상태를 유지한다.

Provider Injection

팩토리 패턴이 가능하다. 객체를 주입하지 않고 여러 인스턴스를 만들어야 하는 경우... 이 경우 Provider가 가능하다.

@Inject 
Provider mEntryProvider; 
Entry entry1 = mEntryProvider.get(); 
Entry entry2 = mEntryProvider.get();

프로바이더가 Entry객체의 두가지 인스턴스를 만든다. 인스턴스가 모듈에서 어떠게 생성될지는 알아서...

짜증나게 하는 지점!

inject() 메서드는 강력한 타입체크를 수행한다. 이는 디버깅에 유리하지만 베이스클래스로부터 주입하는 경우 복잡하게 한다.
직관적으로 베이스클래스를 대상으로 하는 inject()메서드를 만들것이다 하지만 이는 서브클래스에 대해서는 동작하지 않는다.

해결책은 멀까? 

방법#1 은 베이스클래스에 abstract 메서드를 정의하는것이다. 

@Override 
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); 
    injectComponent(((DemoApplication) getApplication())
                .getComponent()); 

protected void injectComponent(ApplicationComponent component);

그리고 서브클래스에서 구현한다.

@Override
protected void injectComponent(ApplicationComponent component) { 
    component.inject(this); 
}

방법#2 는 리플랙션을 사용하는것이다. 이는 당신이 다루고자 하는 타입에 해 당하는 inject()메서드를 찾는것으로 귀결된다.

먼저 당신이 정의한 콤포넌트의 모든 메서드들을 타입별로 캐시한다.

// Keep the returned cache in some helper. 
Map<Class, Method> buildCache() { 
    Map<Class, Method> cache = new HashMap<>(); 
    for (Method m : ApplicationComponent.class.getDeclaredMethods()) { 
        Class[] types = m.getParameterTypes(); 
        if (types.length == 1) { 
            cache.put(types[0], m); 
        } 
    } 
    return cache; 
}

그리고 주입시에 getClass()로 캐시로부터 꺼내고 invoke를 호출한다.

// Use for injecting targets of any type. void inject(ApplicationComponent component, Object target) { 
    Method m = cache.get(target.getClass()); 
    if (m != null) { 
        m.invoke(component, target); 
    } 
}

기본클래스에서 모든 설정을 숨기거나 이것을 수행하는 코드를 다루는 누군가에게는 유용할것이다.

짜증2 는 콤포는트 구현이 재빌드를 요구하고 클래스가 사라질때는 컴파일에러가 발생한다는 것이다. 심간한 불편함은 아니지만 프로젝트 구성 초기에는 좀 귀찮을 수 있다.

원문 : https://blog.gouline.net/dagger-2-even-sharper-less-square-b52101863542

 

Dagger 2: Even Sharper, Less Square

It was only a matter of time before version 2.0 of the well-known dependency injection library Dagger hit production and that seemed like…

blog.gouline.net

한글로 된 잘 정리된 문서

https://medium.com/@jason_kim/tasting-dagger-2-on-android-%EB%B2%88%EC%97%AD-632e727a7998

Posted by 삼스
Android/App개발2014. 7. 16. 13:31


http://developer.android.com/guide/practices/screens-distribution.html

모든 스크린사이즈에 적절하게 표시되도록 화면을 디자인할것을 권고하지만 태블릿만, 폰만 또는 특정크기이상의 스크린만 지원하고자 하는 경우가 있다. 그러려면 지원할 스크린에 대한 설정을 manifest에 추가하여 구글플레이같은 외부서비스에 의해 필터링이 되도록 할 수 있다.

그전에 멀티스크린지원에 대한 정확한 이해가 필요하고 그에 따라 구현해야 한다.

폰만 지원하기

일반적으로 큰스크린에 잘 맞도록 시스템이 동작하기 때문에 큰스크린에 대한 필터는 필요치 않다. Best Practies for Screen Independence에 잘 따랏다면 태블릿에서 아주 잘 동작할것이다.  하지만 스케일업이 생각처럼 동작하지 않는것을 발견할 수 있으며 애플리케이션을 다른 스크린설정으로 두개의 버전을 배포하려고 할 수 있다. 이 경우 <compatible-screens> 로 screen size와 density의 조합으로 조정할 수 있다. Google Play는 이 정보를 참조하여 애플리케이션을 필터링한다. 그리하여 단말에 적합한경우에만 다운로드 설치가 가능하도록 한다.

<screen> 엘리먼트를 포함해야 하는데 각 <screen>은 스크린설정정보(스크린크기, 스크린밀도)를 담는다. 두가지(screenSize, screenDensity)정보를 모두 담아야 한다. 하나라도 빠져있다면 Google Play는 그 필드를 무시할것이다.

만일 small, normal size screen을 지원하고 density는 무시하고자 한다면 총 8개의 <screen> 엘리먼트가 필요하다. 각 사이즈별로 density가 4가지이기 때문이다. 여기서 정의되지 않은 조합은 호환되지 않는것으로 간주된다. 다음은 이 경우에 대한 manifest예이다.

<manifest ... >

    <compatible-screens>

        <!-- all small size screens -->

        <screen android:screenSize="small" android:screenDensity="ldpi" />

        <screen android:screenSize="small" android:screenDensity="mdpi" />

        <screen android:screenSize="small" android:screenDensity="hdpi" />

        <screen android:screenSize="small" android:screenDensity="xhdpi" />

        <!-- all normal size screens -->

        <screen android:screenSize="normal" android:screenDensity="ldpi" />

        <screen android:screenSize="normal" android:screenDensity="mdpi" />

        <screen android:screenSize="normal" android:screenDensity="hdpi" />

        <screen android:screenSize="normal" android:screenDensity="xhdpi" />

    </compatible-screens>

    ...

    <application ... >

        ...

    <application>

</manifest> 


주의) 하지만 2014년 6월 현재 하나가 더 있다. xxhdpi 따라서 이제 10개가 필요하게 되겠다.

Note) <compatible-screens>엘리먼트는 리버스를 통해 호환 여부를 작은사이즈만 지원하는지 알수 있으나 <supports-screens>를 사용하면 density를 요구하지 않기 때문에 피할 수 있다.


태블릿만 지원하기

폰을 지원하지 않겠다면(즉 큰스크린만 지원하겠다면) 또는 작은 스크린에 대해 최적화하는데 시간이 필요하다면 작은스크린 디바이스는 설치안되도록 할 수 있다. 이 방법은 <support-screens>를 통해 가능하다.

<manifest ... >

    <supports-screens android:smallScreens="false"

                      android:normalScreens="false"

                      android:largeScreens="true"

                      android:xlargeScreens="true"

                      android:requiresSmallestWidthDp="600" />

    ...

    <application ... >

        ...

    </application>

</manifest>

두가지 방법이 있다.

  • "small"과 "normal"사이즈를 미지원으로 설정, 일반적으로 이런 디바이스는 태블릿이 아니다
  • 앱이 허용하는 영역이 최소 600dp이상으로 설정

첫번째는 android 3.1과 그 이 하의 디바이스들을 위한것이다. 왜냐하면 그 디바이스들은 일반화된 스크린사이즈에 기반하여 사이즈를 정의하였기 때문이다. requiresSmallestWidthDp속성은 android 3.2와 그이상을 위한것이다. 이는 dip의 최소값에 기반하여 요구되는 사이즈를 정할 수 있다. 예를 들면 600dp로 설정하면 일반적으로 7인치 이상의 스크린을 갖는 디바이스들이 해당된다.

당신이 필요로 하는 사이즈는 당현히 다를수 있다. 만인 9인치 이상의 스크린을 지원하고자 한다면 720dp로 설정할 수 있을 것이다.

앱은 requiresSmallestWidthDp속성을 빌드하기 위해 Android 3.2이상으로 빌드를 해야 한다. 이전버전은 에러가 날것이다. 가장 안전한 방법은 필요로 하는 API level에 맞게 minSdkVersion을 설정하는 것이다. 최종배포 질드시 빌드타겟을 3.2로 변경하고 requeiresSmallestWidthDp를 추가한다. 3.2이전버전은 런타임에 이 값을 무시할것이기 때문에 문제가 없을 것이다.

왜 가로사이즈만으로 스크린을 구분하는지 궁금하면 다음 링크를 참조해라. -> New Tools for Managing Screen Sizes.

여러화면을 지원하기 위해 필요한 모든 기술을 적용하여 가능한 많은 장치에 앱을 사용할 수 있도록 하기 위해 <compatible-screens>를 사용하거나 모든 화면구성에 호환성을 제공할 수 없을 경우에만 <support-screens>을 사용하거나 또는 특정한 화면구성의 다른 세트에 대한 다른 버전의 앱을 제공할 수 있다.


다른 화면을 위한 여러개의 APK제공

하나의 APK만을 제공하는것이 권장사항이긴 하지만 구글플레이는 여러 화면설정에 따라 여러개의 APK를 동일 앱에 대하여 제공하는것을 허용한다. 예를 들어 폰과 태블릿버전을 모두 제공하고자 하는데 동일 APK로 만들수 없는 경우에 실제로 2개의 APK를 게시할 수 있다. 구글 플레이는 디바이스 화면에 맞춰서 APK를 배포할것이다.

하지만 하나로 게시하는제 좋을거다. 라는게 구글의 여전한 권장사항이다. 

더 자세한 정보를 원하면 다음 링크를 더 살펴봐라. Multiple APK Support.



Posted by 삼스
Android/App개발2014. 5. 21. 21:57


Bluetooth Low Energy

4.3부터 BLE central role을 지원하기 시작했다. 그리고 디바이스 검색과 서비스 검색 그리고 속성들의 읽기/쓰기를 지원한다. BLE를 통해 저전력으로 설계된 다양한 디바이스와통신이 가능해졌다.

Key Terms and Concepts
  • Generic Attribute Profile (GATT) - BLE link상에서 송수신 가능한 일반적인 사양을 말하며 모든 LE 애플리케이션프로파일은 GATT에 기반한다.
    • 블루투스 SIG는 많은 프로파일을 정의하였는데 하나의 프로파일은 앱에서 어떻게 동작할지에 대한 사양을 의미한다. 하나의 장치는 여러 프로파일을 구현할 수 있다. 
  • Attribute Protocol (ATT) - GATT는 ATT의 최상위 구현체이며 GATT/ATT로 참조되기도 한다. ATT는 BLE장치에서 동작하도록 최적화 되어 있다. 개개의 속성(Attribute)은 UUID를 가지며 128비트로 구성된다. ATT에 의해 부여된 속성은 특성과 서비스를 결정한다.
  • Characteristic - 하나의 특성(characteristic)은 하나의 값과 n개의 디스크립터를 포함하는데 디스크립터는 특성 값을 기술한다. 하나의 특성은 클래스와 유사한 타입으로 생각하면 된다. 
  • Descriptor - 디스크립터는 특성의 값을 기술한다.
  • Service - 하나의 서비스는 특성드의 집합니다. 예를 들어 "Heart Rate Monitor"라고 불리는 서비스를 가지고 있다면 그 서비스는 "heart rate measurement"같은 특성을 포함한다. GATT-based profile의 리스트와 서비스를 확인하려면 bluetooth.org를 방문하라.
Roles and Responsibilities
이 문서에서 설명하는 역할과 책임은 Android장치가  BLE장치와 연동하는데 적용되는것들이다.
  • Central vs. peripheral - BLE 연결에 적용된다. central 역할은 scan, 게시검색(looking for advertisement), 그리고 peripheral역할은 게시를 만든다.
  • GATT server vs. GATT client - 디바이스가 연결된 이 후 서로 어떻게 대화하는지에 대해 정의한다. 
차이점을 이해하려면 안드로이드폰하나와 움직임을 감지하는 BLE장치를 가지고 있다고 가정 해보자. 폰은 central역할을 한다. BLE장치는 peripheral역할을 한다. 

폰과 BLE장치가 한번 연결이 되면 두 장치는 서로  GATT metadata를 주고 받는다. 주고받는 데이터에 따라 하나이상의 서버 액션이 있을 수 있다. 예를 들어 BLE장치가 폰에 센서정보를 전달하려고 할 수 있다. 이 때 장치는 서버로 동작한다. 장치가 폰으로부터 업데이트를 받고자 할수 있다 이 때는 폰이 서버로 동작한다.

이 문서에서 제공하는 샘플은 안드로이드 디바이스로 동작하는 앱으로 GATT client이다. 앱은 GATT server로부터 데이터를 가져오는데 그것은 Heart Rate Profile을 지원하는 BLE heart rate monitor이다.
반면에 앱을 GATT 서버역할을 하도록 구현할 수 도 있다. BluetoothGattServer를 참조하라.

BLE permissions
블루투스통신(연결요청, 연결수락, 데이터 전송)을 하려면 BLUETOOTH 퍼미션을 추가해야 함다.
블루투스장치를 검색하고 설정을 조작하려면 BLUETOOTH_ADMIN 퍼미션을 추가해야 한다.
BLUETOOTH_ADMIN은 BLUETOOTH퍼미션과 함께 정의되어야 한다.

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
블루투스 BLE만 사용할거라면 아래와 같이 manifest에 포함시켜야 한다.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
BLE를 지원하지 않는 앱으로 만들고자 한다면 여전히 동일한  feature를 추가해야 하며 다만 required="false"로 하면 된다. 런타임에 BLE활성화 여부를 PackageManager.hasSystemFeature로 알아낼 수 있다.

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
   
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish
();
}

Setting Up BLE

BLE로 통신하기 전에 디바이스가 BLE를 지원하는지 확이해야 한다. 지원한다면 활성화되어야 한다. 
만일  BLE를 지원하지 않는다면 BLE feature들을 비활성화해야 한다. 지원한다면 사용자에게 BT를 앱을 떠나지 않고 활성화하도록 유도해야 한다. 이 과정은 BluetoothAdapter를 사용하여 2단계로 가능하다.

1. BluetoothAdapter얻기

BluetoothAdapter는 블루투스관련 일부 또는 모든 블루투스 동작들을 필요로 한다. BluetoothAdapter는 디바이스자체의 BluetoothAdapter를 나타낸다.  전체시스템을 위한 하나의 어댑터가 있고 앱은 이 객체를 통해서 상호작용을 한다. 다음 코드조각은 어댑터를 얻는 방법을 보여준다.

    1. // Initializes Bluetooth adapter.
      final BluetoothManager bluetoothManager =
             
      (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
      mBluetoothAdapter
      = bluetoothManager.getAdapter();
    2. Bluetooth 활성화.
    다음에 블루투스를 활성화해야 한다. isEnabled()로 활성화여부를 확인 가능하다. false면 비활성화이다. 다음은 그 샘플이다.
    1. private BluetoothAdapter mBluetoothAdapter;
      ...
      // Ensures Bluetooth is available on the device and it is enabled. If not,
      // displays a dialog requesting user permission to enable Bluetooth.
      if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
         
      Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
          startActivityForResult
      (enableBtIntent, REQUEST_ENABLE_BT);
      }

    BLE 장치 찾기
    BLE장치를 찾으려면 startLeScan() 메소드를 호출한다. LeScanCallack이 파라메터로 호출된다. 이 메소드를 구현해야 하고 스캔결과를 받을 수 있다. 배터리 소모가 심심하기 때문에 다음의 가이드라인을 잘 지켜야 한다.
    • 원하는 디바이스를 찾으면 바로 스캔을 중단해야 한다.
    • 하나의 루프에서 스캔하지 말고 타임아웃을 적용해라. 디바이스가 범위안에서 벗어났을 수도 있고 그럴경우 배터리 는 골로 간다.
    다음 코드는 스캔을 시작하고 중단하는 예이다.
    /**
     * Activity for scanning and displaying available BLE devices.
     */

    public class DeviceScanActivity extends ListActivity {

       
    private BluetoothAdapter mBluetoothAdapter;
       
    private boolean mScanning;
       
    private Handler mHandler;

       
    // Stops scanning after 10 seconds.
       
    private static final long SCAN_PERIOD = 10000;
       
    ...
       
    private void scanLeDevice(final boolean enable) {
           
    if (enable) {
               
    // Stops scanning after a pre-defined scan period.
                mHandler
    .postDelayed(new Runnable() {
                   
    @Override
                   
    public void run() {
                        mScanning
    = false;
                        mBluetoothAdapter
    .stopLeScan(mLeScanCallback);
                   
    }
               
    }, SCAN_PERIOD);

                mScanning
    = true;
                mBluetoothAdapter
    .startLeScan(mLeScanCallback);
           
    } else {
                mScanning
    = false;
                mBluetoothAdapter
    .stopLeScan(mLeScanCallback);
           
    }
           
    ...
       
    }
    ...
    }

    특정타입의 페리퍼렁만 스캔하고자 한다면 startLeScan(UUID[], BluetoothAdapter.LeScanCallback)을 사용할 수 있다. UUID[]에는 앱에서 지원하고자 하는  GATT 서비스목록이 들어간다.
    다음코드 예를 보아라.

    private LeDeviceListAdapter mLeDeviceListAdapter;
    ...
    // Device scan callback.
    private BluetoothAdapter.LeScanCallback mLeScanCallback =
           
    new BluetoothAdapter.LeScanCallback() {
       
    @Override
       
    public void onLeScan(final BluetoothDevice device, int rssi,
               
    byte[] scanRecord) {
            runOnUiThread
    (new Runnable() {
               
    @Override
               
    public void run() {
                   mLeDeviceListAdapter
    .addDevice(device);
                   mLeDeviceListAdapter
    .notifyDataSetChanged();
               
    }
           
    });
       
    }
    };
    BLE또는 Class BT장치를 검색할 수 는 있지만 두가지를 동시에 검색은 할 수 없다.

    GATT 서버에 연결하기
    첫번째 연동을 위한 과정은 BLE디바이스에 연결하는것인데 좀더 자세히 말하면 디바이스의 GATT서버에 연결하는 것이다. connectGatt()메소드로 하면 된다. 이 메소드는 3개의 파라메터가 있는데 context, autoConnect, 그리고 BluetoothGattCallback이 있다. autoConnect는 검색되었을 때 자동으로 연결할지에 대한 파라메터다.

    mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

    이 코드는 BLE device를 GATT 서버 호스트로 연결한다. 그리고 BluetoothGatt 인스턴스를 반환한다. 이 인스턴스로 GATT client를 운영한다.
    BluetoothGattCallback은 클라이언트에 연결상태나 client 운영에 대한 결과를 전달한다.

    이 예제에서 BLE 앱은 하나의 액티비티가 연결하고 데이터를 표시하고 GATT 서비스와 특성들을 표시한다. 사용자 입력에 기반하여 BluetoothLeService로 불리는 서비스와 통신을 수행하도 Android BLE API를 통해 장치와 상호연동을 한다.


    // A service that interacts with the BLE device via the Android BLE API.
    public class BluetoothLeService extends Service {
       
    private final static String TAG = BluetoothLeService.class.getSimpleName();

       
    private BluetoothManager mBluetoothManager;
       
    private BluetoothAdapter mBluetoothAdapter;
       
    private String mBluetoothDeviceAddress;
       
    private BluetoothGatt mBluetoothGatt;
       
    private int mConnectionState = STATE_DISCONNECTED;

       
    private static final int STATE_DISCONNECTED = 0;
       
    private static final int STATE_CONNECTING = 1;
       
    private static final int STATE_CONNECTED = 2;

       
    public final static String ACTION_GATT_CONNECTED =
               
    "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
       
    public final static String ACTION_GATT_DISCONNECTED =
               
    "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
       
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
               
    "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
       
    public final static String ACTION_DATA_AVAILABLE =
               
    "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
       
    public final static String EXTRA_DATA =
               
    "com.example.bluetooth.le.EXTRA_DATA";

       
    public final static UUID UUID_HEART_RATE_MEASUREMENT =
                UUID
    .fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

       
    // Various callback methods defined by the BLE API.
       
    private final BluetoothGattCallback mGattCallback =
               
    new BluetoothGattCallback() {
           
    @Override
           
    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                   
    int newState) {
               
    String intentAction;
               
    if (newState == BluetoothProfile.STATE_CONNECTED) {
                    intentAction
    = ACTION_GATT_CONNECTED;
                    mConnectionState
    = STATE_CONNECTED;
                    broadcastUpdate
    (intentAction);
                   
    Log.i(TAG, "Connected to GATT server.");
                   
    Log.i(TAG, "Attempting to start service discovery:" +
                            mBluetoothGatt
    .discoverServices());

               
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    intentAction
    = ACTION_GATT_DISCONNECTED;
                    mConnectionState
    = STATE_DISCONNECTED;
                   
    Log.i(TAG, "Disconnected from GATT server.");
                    broadcastUpdate
    (intentAction);
               
    }
           
    }

           
    @Override
           
    // New services discovered
           
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
               
    if (status == BluetoothGatt.GATT_SUCCESS) {
                    broadcastUpdate
    (ACTION_GATT_SERVICES_DISCOVERED);
               
    } else {
                   
    Log.w(TAG, "onServicesDiscovered received: " + status);
               
    }
           
    }

           
    @Override
           
    // Result of a characteristic read operation
           
    public void onCharacteristicRead(BluetoothGatt gatt,
                   
    BluetoothGattCharacteristic characteristic,
                   
    int status) {
               
    if (status == BluetoothGatt.GATT_SUCCESS) {
                    broadcastUpdate
    (ACTION_DATA_AVAILABLE, characteristic);
               
    }
           
    }
         
    ...
       
    };
    ...
    }
    콜백이 호출되면 broadcastUpdate()를 호출한다. 여기서 데이터 파싱은 Bluetooth Heart Rate Measurement profile 사양에 맞춰서 수행하고 있다.
    private void broadcastUpdate(final String action) {
       
    final Intent intent = new Intent(action);
        sendBroadcast
    (intent);
    }

    private void broadcastUpdate(final String action,
                                 
    final BluetoothGattCharacteristic characteristic) {
       
    final Intent intent = new Intent(action);

       
    // This is special handling for the Heart Rate Measurement profile. Data
       
    // parsing is carried out as per profile specifications.
       
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
           
    int flag = characteristic.getProperties();
           
    int format = -1;
           
    if ((flag & 0x01) != 0) {
                format
    = BluetoothGattCharacteristic.FORMAT_UINT16;
               
    Log.d(TAG, "Heart rate format UINT16.");
           
    } else {
                format
    = BluetoothGattCharacteristic.FORMAT_UINT8;
               
    Log.d(TAG, "Heart rate format UINT8.");
           
    }
           
    final int heartRate = characteristic.getIntValue(format, 1);
           
    Log.d(TAG, String.format("Received heart rate: %d", heartRate));
            intent
    .putExtra(EXTRA_DATA, String.valueOf(heartRate));
       
    } else {
           
    // For all other profiles, writes the data formatted in HEX.
           
    final byte[] data = characteristic.getValue();
           
    if (data != null && data.length > 0) {
               
    final StringBuilder stringBuilder = new StringBuilder(data.length);
               
    for(byte byteChar : data)
                    stringBuilder
    .append(String.format("%02X ", byteChar));
                intent
    .putExtra(EXTRA_DATA, new String(data) + "\n" +
                        stringBuilder
    .toString());
           
    }
       
    }
        sendBroadcast
    (intent);
    }
    이 이벤드들은 B roadcastReceiver에 의해 처리된다.
    // Handles various events fired by the Service.
    // ACTION_GATT_CONNECTED: connected to a GATT server.
    // ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
    // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
    // ACTION_DATA_AVAILABLE: received data from the device. This can be a
    // result of read or notification operations.
    private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
       
    @Override
       
    public void onReceive(Context context, Intent intent) {
           
    final String action = intent.getAction();
           
    if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                mConnected
    = true;
                updateConnectionState
    (R.string.connected);
                invalidateOptionsMenu
    ();
           
    } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
                mConnected
    = false;
                updateConnectionState
    (R.string.disconnected);
                invalidateOptionsMenu
    ();
                clearUI
    ();
           
    } else if (BluetoothLeService.
                    ACTION_GATT_SERVICES_DISCOVERED
    .equals(action)) {
               
    // Show all the supported services and characteristics on the
               
    // user interface.
                displayGattServices
    (mBluetoothLeService.getSupportedGattServices());
           
    } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
                displayData
    (intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
           
    }
       
    }
    };

    BLE 속성 읽기
    앱이 GATT 서버에 연결하고 서비스를 찾게 되면 속성을  읽고/쓸수 있게 된다. 다음 코드는 서비스와 특성들을 나열하여 표시해준다.
    public class DeviceControlActivity extends Activity {
       
    ...
       
    // Demonstrates how to iterate through the supported GATT
       
    // Services/Characteristics.
       
    // In this sample, we populate the data structure that is bound to the
       
    // ExpandableListView on the UI.
       
    private void displayGattServices(List<BluetoothGattService> gattServices) {
           
    if (gattServices == null) return;
           
    String uuid = null;
           
    String unknownServiceString = getResources().
                    getString
    (R.string.unknown_service);
           
    String unknownCharaString = getResources().
                    getString
    (R.string.unknown_characteristic);
           
    ArrayList<HashMap<String, String>> gattServiceData =
                   
    new ArrayList<HashMap<String, String>>();
           
    ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                   
    = new ArrayList<ArrayList<HashMap<String, String>>>();
            mGattCharacteristics
    =
                   
    new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

           
    // Loops through available GATT Services.
           
    for (BluetoothGattService gattService : gattServices) {
               
    HashMap<String, String> currentServiceData =
                       
    new HashMap<String, String>();
                uuid
    = gattService.getUuid().toString();
                currentServiceData
    .put(
                        LIST_NAME
    , SampleGattAttributes.
                                lookup
    (uuid, unknownServiceString));
                currentServiceData
    .put(LIST_UUID, uuid);
                gattServiceData
    .add(currentServiceData);

               
    ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                       
    new ArrayList<HashMap<String, String>>();
               
    List<BluetoothGattCharacteristic> gattCharacteristics =
                        gattService
    .getCharacteristics();
               
    ArrayList<BluetoothGattCharacteristic> charas =
                       
    new ArrayList<BluetoothGattCharacteristic>();
               
    // Loops through available Characteristics.
               
    for (BluetoothGattCharacteristic gattCharacteristic :
                        gattCharacteristics
    ) {
                    charas
    .add(gattCharacteristic);
                   
    HashMap<String, String> currentCharaData =
                           
    new HashMap<String, String>();
                    uuid
    = gattCharacteristic.getUuid().toString();
                    currentCharaData
    .put(
                            LIST_NAME
    , SampleGattAttributes.lookup(uuid,
                                    unknownCharaString
    ));
                    currentCharaData
    .put(LIST_UUID, uuid);
                    gattCharacteristicGroupData
    .add(currentCharaData);
               
    }
                mGattCharacteristics
    .add(charas);
                gattCharacteristicData
    .add(gattCharacteristicGroupData);
             
    }
       
    ...
       
    }
    ...
    }

    GATT 통지 수신
    디바이스의 특성이 변경에 대한 통지를 앱이 알수 있다. 다음 코드는 setCharacteristicNotification()메소드로 어떻게 통지를 설정하는지 보여준다.
    private BluetoothGatt mBluetoothGatt;
    BluetoothGattCharacteristic characteristic;
    boolean enabled;
    ...
    mBluetoothGatt
    .setCharacteristicNotification(characteristic, enabled);
    ...
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID
    .fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
    descriptor
    .setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt
    .writeDescriptor(descriptor);
    하나의 특성에 대해 통지가 활성화되면 onCharacteristicChanged()가 장치에서 해당 특성정보가 변경이 되면 호출된다.
    @Override
    // Characteristic notification
    public void onCharacteristicChanged(BluetoothGatt gatt,
           
    BluetoothGattCharacteristic characteristic) {
        broadcastUpdate
    (ACTION_DATA_AVAILABLE, characteristic);
    }

    클라이언트 앱의 종료
    BLE 장치를 사용하는 앱이 종료되면 반드시 close()를 호출하여 시스템이 관련 리소스를 반환하도록 해야 한다.
    public void close() {
       
    if (mBluetoothGatt == null) {
           
    return;
       
    }
        mBluetoothGatt
    .close();
        mBluetoothGatt
    = null;
    }


    Posted by 삼스