Android2021. 5. 27. 14:24


고전적인 비동기 처리라 하면 Thread와 mutex, semaphore를 활용한 방법이 있겠다.
이어 개발자들은 비동기 처리를 좀더 쉽게 하기 위해 나온게 Rx시리즈가 있는데 이는 데이터 흐름과 함수형프로그래밍의 관점에서 다루어서 더 인기가 있게 되었다.
이어서 일부 언어에서는 코루틴을 지원하기 시작했고 안드로이드의 새로운 개발언어인 kotlin도 지원하기에 이르렀다.
c#, javascript, go등의 언어에서 await, async를 사용해보았다면 이미 알고 있는 것이다.

스레드로 비동기적이면서 순차적인 작업을 하려고 하면 콜백을 사용하게 마련이고 이런 경우 단계가 늘어날수록 콜백의 늪에 빠지게 된다.

이를 좀더 이해하기 쉽게 코딩할수 있도록 지원한게 Rx시리즈이다.
순차적으로 파이프라인을 이어서 다음 작업을 진행할 수 있다.

그런데 Rx는 학습곡선이 좀 있다. 그리고 원하는 기능을 구현하기 위해서는 실제 작업을 수행하는 루틴과 별도로 부수적이 액세서리 코드들이 더 들어가게 된다.

코루틴은 이를 더 쉽고 간결하게 표한하게 해주며 코드의 가독성도 높여준다.

코루틴이 어떻게 이를 지원하는지에 대해서 먼저 간단하게 설명하겠다.

일반적인 루틴은 호출되면 프로그램의 컨텍스트가 해당 서브루틴으로 넘어가며 서브루틴에서 return될때 호출한 메인 루틴으로 컨텍스트가 복구된다. 이는 예상하는것보다 많은 비용이 수반된다.
코루틴은 메인루틴에서 코루틴이 호출될때 바로 빠져나오게 된다. 그리고 해당 코루틴이 끝나면 코루틴이 호출되었던 부분으로 다시 돌아올수 있다.

일반적인 루틴과 어떤 차이가 있는지에 대해 쉽게 설명하자면 서브루틴이 아주 오래걸리는 작업을 수행하는데 이 루틴이 메인UI스레드에서 동작중이라면 해당 작업이 완료되기전까지 화면이 멈추게 된다.
하지만 코루틴의 이 경우에도 화면이 멈추지 않고 다른 스레드는 돌아가게 된다. 

코루틴이 호출될때 별도의 스레드로 분리되어 호출되고 코드는 더 진행되지 않고 대기하는데 다른 스레드에 영향을 주지 않으면서 대기하다가 코루틴이 완료되면 다시 대기하였던 부분부터 수행된다고 생각하면 된다.

c#등의 언어로 아래와 같은 코드에 해당한다.

  await doSomething();

개발자들은 코드로 설명하는것이 항상 옳다.
thread, RxJava, coroutine으로 동일한 작업을 할 때의 차이를 코드로 살펴보자.

라면을 끓여 먹는다고 하자

라면을 사서 -> 냄비를 준비하고 -> 물을 부어 끊이고 -> 면과 스프를 넣어 끓인 후 -> 그릇에 담아서 -> 먹는다.

위 과정은 모두 순차적으로 진행해야함 최종적으로 내가 먹을 수 있다.
라면을 사지 않고 라면을 먹을수는 없다.

이를 비동기로 구현하려면 thread 호출로 해야 할것이고 이 경우 콜백형태를 사용할것이고

buyRamyeon(money) { ramyeon -> 
  getPot(ramyeon) { ramyeon, pot ->
    doBoilPot(ramyeon, pot) { cookedRamyeon ->
       moveToDish(cookedRamyeon) { ramyeonInDish ->
          eatIt(ramyeonInDish)
       }
    }
  }
}



이렇게 작업해도 문제는 없다. 다만 많은 비용을 수반하고 우리가 그 동안 많이 보아온 콜백지옥의 향연이 펼쳐진다.

이를 RxJava로 구현한다고 하면 아래와 비슷할것이다.

Observable.just(money)
.observeOn(MAIN_Thread)
.subscribeOn(IO_Thread)
.flatMap { money -> buyRamyeon(money) }
.flatMap { ramyeon -> getPot(ramyeon) }
.flatMap { ramyeon, pot -> doBoilPot(ramyeon, pot) }
.flatMap { ramyeonInDish -> moveToDish(cookedRamyeon) }
.subscribe({ 
ramyeonInDish -> eatIt(ramyeonInDish)
}, {
// 실패케이스 처리
})



콜백헬에서는 확실히 벗어난듯 하다.
하지만 just, observeOn, subscribeOn등에 대해 알아야 하며 Rx는 상당히 급한 학습곡선에 해당한다.

이를 coroutine으로 구현한다면 아래와 비슷할것이다.

suspend iWantEatRamyeon(money) {
  try {
    val ramyeon = buyRamyeon(money)
    val pot = getPot()
    val cookedRamyeon = doBoilPot(ramyeon, pot)
    val ramyeonInDish = moveToDish(cookedRamyeon)
    eatIt(ramyeonInDish)
  } catch (e: Exception) {
    // 실패케이스 처리
  }
}


Coroutine 문법을 자세히 아라보려면 https://selfish-developer.com/entry/Kotlin-Coroutine 여그를 보자. 잘 정리되어 있넹 

Posted by 삼스
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 삼스
Android2020. 9. 8. 11:09

Android 10(Q, 29 level)으로 TargetApi로 설정한 앱이 Android10이상의 단말에 설치된 경우 외부저장소에 대해 Scoped storage모드로 동작한다.

 

Scoped Stroage

 

Android10 타겟에 Android10 단말에서 동작한다.


외부저장소의 공용파일공간이 모두 사라지고 개별앱공간이 샌드박스로 격리되어 제공되며 다른앱이 접근 불가하다

 

MediaStore는 내가 추가한 파일을 읽거나 쓰는데 권한없이 사용 가능하나. 다른앱의 파일을 읽기 위해서는 권한이 필요하다.

 

파일경로(file:///) 만으로 읽고 쓰기가 불가(FileNotFound혹은 권한이 없다는 에러)하다. FileProvider 혹은 시스템 파일 선택기를 통해서 사용자가 직접 파일을 선택한 이후 에야 접근이 가능하다.


Environment.getExternalStorageDirectory()는 deprecated되었다.

 

타앱에 파일을 전달할때 파일경로로 전달불가하며 FileProvider로 ContentUri를 만들고 공유 받을 앱에 임시로 URI접근권한을 허용하는 방법을 사용한다(이전 포스팅 참조)

 

공용저장공간은 /sdcard자체가 공용공간이었으나 이제는 MediaStore와 StorageAccessFramework를 이용하여 공용저장공간에 읽고 쓸수 있다. 이제 이 영역만이 외부저장소라고 할수 있다.

 

사진, 동영상, 음악파일은 MediaStore를 통해 접근이 이미 가능했고 추가로 Download에 저장된 파일에 접근하기 위한 콜렉션을 제공한다. Download도 내가 생성한 파일을 제외하고는 사용자가 시스템파일선택기를 통해 사용자가 명시적으로 선택한 경우에만 접근이 가능하다.

 

파일접근 정리

파일위치 권한 접근 앱삭제시 제거
개별앱 공간 필요없음 getExternalFileDir() Y
미디어콜랙션(사진,비디오,오디오) 다른앱파일접근시만 읽기권한필요 MediaStore or SAF N
다운로드 필요없음 SAF N

느끼고 있겠지만 많은 앱개발자들은 이전 방식을 더 선호한다. 보안에 민감한 클라이언트들은 안그러겠지만...

어쨌든 이전방식을 아직은 사용가능하다.

 

AndroidManifest.xml의 application tag에 requestLegacyExternalStorage플래그를 true로 하면 임시로 이전 저장소정책을 사용할수 있다. 하지만 Target을 Android11로 하면 이 플래그도 지원하지 않을 것이라고 한다.

 

<!-- 임시로 Q(10)에서 opt-out함 -->
<uses-sdk android:targetSdkVersion="29" />
<application android:requestLegacyExternalStorage="true" ... >

문제는 참조하는 다른 라이브러리들이 대응이 안된 경우이다. 별수 없이 해당 라이브러리에서 대응이 되어야 한다.

코드내에서 정책을 확인하기 위해서 isExternalStorageLegacy()를 제공한다.

// storage mode 체크
if (Envirioment.isExternalStorageLegacy()) {
...
}

경우에 따라서는 isExternalStorageLegacy()로 분기하여 파일접근하는 로직을 구분하여 적용해야 할수도 있을것 같다.

Posted by 삼스
Android2020. 9. 8. 10:59

ContentProvider의 특별한 서브클래스로 file:/// Uri가 아니라 content:// Uri로 파일에 접근하고 공유할 수 있게 해준다.

 

file:/// Uri는 보안에 취약하지만 content:// Uri는 안드로이드의 보안인프라의 기반으로 보안문제를 해결해준다.

 

FileProvider를 저으이하고 유효한 파일을 지정하고 Content URI로 파일탐색하고 임시적으로 권한을 획득하고 타앱에 Content URI를 제공하는 것에 대해 정리하겠다.

 

FileProvider의 정의

 

코드를 작성할 필요가 없고 XML에 FileProvider를 기술한다.
android:authorities속성이 중요하며 당신만의 도메인으로 정의한다. android:export는 false로 하여 public하지 않게 설정한다. android:grantUriPermissions속성은 true로 파일에 대해 임시권한을 부여할수 있게 허용한다.

 

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

유효한 파일 기술하기

 

FileProvider는 사전에 기술한 폴더의 파일들에 대해서만 content uri를 생성한다. 이는 별도의 xml파일에 기술한다. 아래코드는 private 파일영역의 images/폴더를 정의한 예이다.

 

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>

<paths>엘리먼트는 하나이상의 엘리먼트를 포함해야 한다.

다음은 다양한 폴더경로에 대한 설정들이다.

 

<!-- cache는 앱의 내부저장소의 캐시폴더를 의미하며 getCacheDir()의 결과와 동일하다.-->
<cache-path name="name" path="path" />  
<!--external은 외부저장소의 루트폴더를 의미하며 Context.getExternalFilesDir(String)의 결과와 동일하다. -->
<external-path name="name" path="path" /> 
<!--외주저장소의 캐시폴더를 의미한다. Context.getExternalCacheDir() -->
<external-cache-path name="name" path="path" />
<!--외부저장소의 media폴더를 의미한다. Context.getExternalMediaDirs(). 이 폴더는 API 21+에서만 유효하다.-->
<external-media-path name="name" path="path" />

엘리먼트의 name속성은 URI path 세그먼트로 실제 경로를 숨겨주는 역할을 한다. 실제 경로는 path속성에 기술된다. path는 실제 서브폴더명이다.

 

<paths>엘리먼트에 여러개의 files-path를 기술하여 여러 폴더의 내용을 공유할 수 있다.

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    <files-path name="my_docs" path="docs/"/>
</paths>

paths를 정의한 파일은 별도의 파일로 res/xml/폴더에 file_paths.xml로 저장하고 provider 엘리먼트의 mata-data에 기술해준다.

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.mydomain.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

파일의 Content URI생성하기

 

타앱에 파일을 공유하려면 해당 파일에 대해 content URI를 생성해야 한다. File객체를 생성 후 getUriForFile()의 인자로 호출한다. 이렇게 만들어진 URI를 인텐트에 담아서 전달하고 전달받은 앱은 ContentResolver.openFileDescriptor로 ParcelFIleDescriptor를 얻어서 파일에 접근한다.

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

//위 코드의 결과로 생성된 URI는 다음과 같다.
content://com.mydomain.fileprovider/my_images/default_image.jpg

 

타앱에 ContentURI 제공

타앱에 특정파일의 content URI를 제공하는 여러가지 방법이 있는데 일반적으로 startActivityForResult()으로 당신의 앱에 인텐트를 전달하는 방법을 사용한다. 당신의 앱에서는 바로 응답하거나 사용자의 선택할 수 있는 UI를 제공하여 응답하는 방법이 있다. 두 방법다 선택한 파일의 content URI를 setResult()로 인텐트에 담아서 반환한다.
또한 ClipData 객체에 content URI를 담아서 반환할수 있으며 Intent.setClipData()를 호출하여 담으면 된다. 이 때는 여러개의 각각의 content URI를 갖는 ClipData객체를 담을수 있다.

Posted by 삼스
Android2020. 7. 10. 17:03

Firebase analytics 기본

모바일앱의 사용자로그를 수집하기 위해 만들어졌으며 Bigquery와 함께 좀더 자세한 로그분석이 가능하나 Bigquery는 비용이 발생한다.
먼가 계속 내용이 추가되고 변경되기 때문에 문서는 영문을 보기를 권한다.

현재는 iOS, android만 지원함. 웹은 지원하지 않음.

 수집하는 정보
 사용자속성과, 이벤트 두가지로 구분하여 로그정보를 수집한다.

 "홍길동이 회원가입을 했다"라는 일이 일어났을 경우 "홍길동"은 사용자속성이고 "회원가입"은 이벤트이다.

 둘다 정보에 키/값쌍의 파라메터를 추가할 수 있다. 

이벤트
 자동으로 수집되는 이벤트(앱실행, 제거(iOS는 수집불가), 업데이트, 스크린뷰 등)
 최대 500개의 이벤트를 사용할 수 있다.
 저장 개수는 무제한이나 일반적인 상황이 아닌경우 구글에서 제한을 걸 수 있다. 명확하게 명시되어 있지는 않으나 일반적인 상황에서는 발생하지 않음.
 대소문자 구분, firebase_, google_, ga_, ...등으로 시작할 수 없음.
 하나의 이벤트에 25개의 파라메터 추가가 가능하며 파라메터의 키/값은 각 40/100글자의 제한이 있음.
 iOS의 경우 앱이 오류로 종료된 이 후 앱을 재시작없이 삭제되면 이벤트가 누락될 수 있음
사용자속성
 사용자고유의 속성을 추가 가능.
 정보가 유지되고 이벤트에 붙여 읽을수 있다.
 25개의 사용자 속성 사용 가능. 생각보다 많은 개수가 아니므로 잘 설계하여 사용해야 한다.
 키는 24자, 값은 36자 제한이 있으며 firebase_, google_, ga_, ...등은 사용불가.

디버그
 디버그뷰를 통해서 이벤트가 전달되는것을 실시간으로 확인이 가능함.

 안드로이드와 iOS각 디버그모드를 활성화한 후 진행해야 한다.

 

이상과 현실

 애널리틱스를 처음 하려고 하는 이유는 마케팅적인 이유와 서비스운영적인 면에서 오류를 추적하거나 사용패턴을 분석하려는 등의 목적으로 접근하곤 한다.

 적용자체는 굉장히 쉬우나 실제로 유용한 정보를 얻기는 ... 비용적으로 힘들다. 

 의미 있는 정보를 얻고자 한다면 BigQuery를 사용해야 하며 사용하려면 애널리틱스360 계정이 필요하며 대기업용, 중소기업용이 있는데 15만불/년 의 비용을 지불해야 한다고 한다. 이게 대기업용인지 중소기업용인지 모르겠으나 거의 2억정도의 비용을 들인다면 작은 기업들은 접근이 아예 힘들듯..

 따라서 해당 라이센스를 가지고 마케팅정보를 분석하여 서비스 해주는 업체들이 따로 있다고 한다. 그러면 비용이 좀 줄기는 하곘으나 공짜를 기대한 개발자는 좌절하게 될것이다.

 무료버전으로 분석할 수 있는 정보는 아주 미약하지만 특정 이벤트가 어느날에 몇 회 정도 발생했는지 정도의 정보만을 목적으로 한다면 무료로 가능하다.

 

 

 

 

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 삼스
Android2019. 11. 10. 16:57

https://medium.com/ionic-and-the-mobile-web/how-to-write-cordova-plugins-864e40025f2

 

How to write Cordova Plugins

Cordova Plugins are one of the dark arts of hybrid app development, helping web apps running natively access the full power of the device…

medium.com

두가지 방법이 있다. 첫번째는 plugman toole로 빠르게 만들수 있고 또는 기존 플러그인을 복제하여 수정하는 방법이다.

여기서는 복제하여 수정하는 방법을 설명한다.

 

Cordova Device plugin이 선택되었다.

 

 

plugin.xml에 중요한 정보들이 기술된다. src 폴더에는 네이티브 코드가 플랫폼별로 있고 www폴더는 javascript코드가 있다. 

 

이제 나만의 플러그인을 만들기 위해 위코드를 작업을 하기 위한 폴더로 모두 옮겨보자

 

먼저  plugin.xml을 열어서 플러그인아이디를 변경한다. 

여기서는 my-cordova-plugin으로 정했다.

 

javascript code의 위치도 기술한다.

js-module은 실행된 js code에 대해 기술하며 clobber는 window변수를 의미한다. 여기서는 www/plugin.js가 로드되면 이 때 부터 window.MyCordovaPlugin으로 플러그인에 접근가능해진다.

 

다음으로는 지원한 플랫폼에 대해 기술한다.

 

config파일에서 안드로이드 패키지명과 플러그인에서 사용한 코도바 심볼에 대해서 기술하게 되는데 이 경우 MyCordovaPlugin이며 자바코드와 연결된다.

 

iOS의 경우는 

안드로이드와 비슷하다. 플러그인은 swift로 개발이 가능하나 코도바가 objc이어서 인터페이스하는데 비용이 좀 든다.

 

 

자바스크립트의 작성

www/plugin.js의 코드를 살펴보자

echo, getDate를 함수로 갖는 간단한 객체를 정의하고 있다. 실행 시 에 개발자는 다음과 같이 플러그인을 호출할 수 있다.

 

window.MyCordovaPlugin.echo('hello!', function() { // callback

});

 

이코드가 실행되는 순간 마법이 있어나게 되며 안드로이드 혹은 iOS의 네이티브 코드가 실행되어지고 그 결과를 콜백으로 받을 수 있다.

 

 

iOS 네이티브 작성

objc code파트이다. 헤더를 먼저 보자

 

이 헤더파일은 m파일과 대응된다.

 

이 메서드들은 js의 exec()가 실행될 때 호출된다. CDVInvokedUrlCommand 인자가 js로부터 전달한 인자를 넘겨주게 된다. 콜백은 한번만 사용할 지 반복적으로 사용([result setKeepCallbackAsBool]을 호출하여)할지 정할 수 있다.

 

결과를 반환해주려면 CDVPluginResult 객체를 만들고 commandDelegate로 넘겨주면 js까지 전달된다.

 

Android 플러그인 작성

iOS와 컨셉은 동일하다 문법적으로 약간 다를뿐. 다음 코드를 보자 src/android/com/example/MyCordovaPlugin.java

execute메서드 하나에서 action인자로 echo와 getDate메서드를 분기하여 처리하고 있다. 결과를 반환하는 방식은 비슷하다.

 

플러그인 테스트

타이핑 끝났으면 테스트를 하자. 

코도바플러그인을 테스트하려면 Cordova/PhoneGap/Ionic앱이 실행하기 위해 있어야 한다. 단독으로 동작하지 않는다.

 

Cordova 앱이 있다고 가정하면 다음과 같이 로컬에서 플러그인을 추가할 수 있다.

 

cordova plugin add --link ~/git/echo-plugin

 

--link플래그는 심볼릭링크로 플러그인을 추가하는것이며 플러그인을 다시 추가하지 않고 코드를 수정하고 빌드하고 실행할 수 있다는것을 의미한다. 하지만 javascript파트의 코드는 자동으로 업데이트 되지 않을수 있다. 이 때는 cordova rm my-cordova-plugin으로 빠르게 삭제하고 다시 추가하면 된다.

 

여기까지 기존 플러그인을 수정하여 적용하는 방법을 알아 봤다.

이는 좀 해키한 방법이며 권장하는 방법은 아니다.. 따라서 별로 이렇게 하고 싶지는 않다. 일을 빨리 하기 위해서는 이 방법도 괜찮겠지만 plugman으로 하는 방법에 대해서도 정리해서 포스팅할 생각이다!!

 

 

 

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 삼스