Diary #1, Dagger의 Child Fragment 관련

주의사항!

이 포스트는 주인장(@WindSekirun) 과 여러 사람들과 개발(특히 Android 관련이 많습니다)에 관련된 이야기를 할 때, 간단히 다루기 좋은 이야기를 있는 그대로 표현하는 포스트입니다. 대화 내용은 보기 좋게만 변경하고, 오탈자는 수정하지 않습니다. 만일 대화중 틀린 내용이나 보강할 내용이 있다면 댓글로 알려주시면 감사합니다!

아니면 텔레그램 WindSekirun 으로 주셔도 됩니다.

이번 주제

Dagger에서 ChildFragment를 ParentFragment가 관리해야 되는 것인가, 아니면 Activity가 관리해야 되는 것인가?

등장인물

  • Pyxis, 주인장
  • @Zeallat , 언제나 도움을 주셔서 매우 감사한 분입니다.

정답

등잔 밑이 어둡다고, Dagger 공식 레포 상에 답이 있습니다. 링크는 [여기] 로, DialogFragment의 베이스 클래스의 구현체입니다. 보면 onAttach에서 AndroidSupportInjection 을 통해 injection 하는 것을 확인할 수 있습니다.

즉 ChildFragment는 Parent Fragment가 관리하는 게 맞고, injection 은 위를 참고하면 됩니다

대회내용

  • Pyxis: 무튼
  • Pyxis: Dagger 에 스코프가 있을 때
  • Pyxis: Activity : Service: BroadcastReceiver: ContentProvider는
  • Pyxis: Application 스코프잖아요
  • Pyxis: 그래서 DispatchingAndroidInjector 를 Application에서
  • Pyxis: 구현하고
  • Pyxis: 그리고 Fragment는
  • Pyxis: 공식 컴포넌트가 아니고: Activity에 종속적이기 때문
  • Pyxis: 에
  • Pyxis: Activity에 종속적이고
  • Pyxis: 그리고 DialogFragment가 있어요
  • Pyxis: Fragment도 Activity 종속이고: 다이얼로그도 Activity종속이라 볼 수 있어요 (Window Token 관련 살펴보면 이렇게 결론 내릴 수 있음)
  • Pyxis: 여기서: DialogFragment 안에 ChildFragment가 있다고 할때
  • Pyxis: 이 Fragment는 Activity에 관리하는게 맞을까요
  • Pyxis: 아니면 parnet fragment (여기서는 DialogFragment)가 관리해야될까요
  • Zeallat: 스코프를요?
  • Pyxis: DispatchingAndroidInjector를 받아서
  • Zeallat: activity life cycle에 맞추느냐
  • Pyxis: HasSupportFragmentInjector 같은거
  • Pyxis: 구현하고
  • Zeallat: fragment lifecycle에 맞추느냐
  • Zeallat: 그건가여?
  • Pyxis: 라이프사이클 관련
  • Pyxis: 네 어떻게 보면 라이프사이클이네여
  • Pyxis: 저는 진짜 왠만하면
  • Pyxis: child fragment는 안만들려고 하거든요
  • Pyxis: 예전에 당한게 하도 많아서
  • Zeallat: 어떤 케이스이죠?
  • Pyxis: 그래서 저 경우를 아예 생각하고 있지 않았는데
  • Pyxis: DialogFragment 하나로
  • Pyxis: 여러개의 Fragment를 replace?
  • Pyxis: 하는 그런목적인거같아요
  • Pyxis: 지금 구성하는 앱이
  • Pyxis: 연속적으로 다이얼로그만 4~6개 뜨게 되있어서
  • Pyxis: 하나하나 dismiss해서 하는거보다 내부에서 관리하는게
  • Pyxis: 더 효율성있다고는 보거든요
  • Zeallat: 음
  • Zeallat: 그러니까
  • Zeallat: 페이지 하나에서 fragment(팝업)을 여러개 표시해야하는데
  • Zeallat: 그걸 replace로 하기위해서
  • Zeallat: dialog 하나 만들고 그안에서
  • Zeallat: child 여러개를 컨트롤한다는 이야기인가요?
  • Pyxis: Exactly
  • Zeallat: 그런데
  • Zeallat: 이 parent fragment가
  • Zeallat: 여러 activity에서 불리는 시츄에이션인가요?
  • Pyxis: 아뇨
  • Pyxis: 기획적으로는 하나의 activity
  • Pyxis: 에서만 불리는데
  • Zeallat: 그럼 왜 굳이 parent를 만들어요?
  • Pyxis: 한 화면내에서 여러번 불릴 가능성은
  • Pyxis: 있어요
  • Zeallat: 그냥 activity에서 replace 컨트롤 하면 안되나요?
  • Pyxis: activity는
  • Zeallat: 아니면 공통 view를 공유하나요?parent의?
  • Pyxis: 그대로 두고
  • Pyxis: parent의 공통 뷰를
  • Pyxis: 공유하죠
  • Pyxis: parent dialog에는 타이틀이랑 x버튼
  • Pyxis: 같은거 두고
  • Pyxis: 그 안 fragment에서는
  • Pyxis: N:1 관계로
  • Pyxis: (1이 ViewModel)
  • Pyxis: 상태 관리하고
  • Pyxis: 대충 어떤 시나리오인지
  • Pyxis: 예상가시나여?
  • Zeallat: 네 이해가요
  • Zeallat: parent fragment에서 단순 호출이아니고
  • Zeallat: parent의 일부 뷰에만
  • Zeallat: child를 표시한다는거죠?
  • Pyxis: 네
  • Pyxis: AndroidSupportInjection.inject 코드 보니까
  • Pyxis: parent fragment를 찾는거같은 코드가
  • Pyxis: 있더라고요
  • Pyxis: 그렇게 되면 제 생각은
  • Pyxis: ParentFragment 가 ChildFragment를 관리하는게
  • Pyxis: 관점에선 맞아보이는데
  • Zeallat: 일단 뭐 그런 케이스라면
  • Zeallat: parent fragment 죽으면
  • Zeallat: child도 다 죽어야죠
  • Pyxis: 그렇겠져
  • Zeallat: scope측면에서
  • Zeallat: parent에 종속적이여야하죠
  • Pyxis: 그렇죠
  • Pyxis: 그게 좀 더 하긴 하는데
  • Pyxis: AndroidSupportInjection 쓰니까
  • Pyxis: Non injected 로그 뜨면서
  • Pyxis: 인젝트가 안되더라고요
  • Pyxis: parent fragmentㅇ서
  • Pyxis: 에서
  • Pyxis: 여기서 parent fragment인 DialogFragment에 injection이 왜 필요한가
  • Pyxis: 하면
  • Pyxis: scope 의 생성은 HasSupportFragmnetInjector 인터페이스 기반이라
  • Pyxis: 서브컴포넌트도 이 기준으로 생성되고
  • Pyxis: 그래서 parent fragment가 child fragment를 가지기 위해서는
  • Pyxis: parent fragment가 HasSupportFragmentInjector를 구현하고
  • Pyxis: DIspatchingAndroidInjector를 반환해야 되는데
  • Pyxis: 구현하는거까진 문제가 없는데
  • Pyxis: DialogFragment의 onCreateView에서 AndroidSupportInjection 하니까
  • Pyxis: 죽더라고요
  • Pyxis: 그래서 일단 임시방편으로 AppComponent에다가
  • Pyxis: inject 메서드 만들어놓고
  • Pyxis: 그걸 호출하는 식으로 강제 인젝트는 했는데
  • Pyxis: 결론적으로
  • Pyxis: 이 접근방법이 맞는건지: 그리고 이 경우에 dialogfragment에 정상적으로 inject하는 방법이 어떤건지
  • Pyxis: 이 두개가 문제였어요
  • Pyxis: 일단 dagger-android 자체는 DialogFragment를 정식적으로 지원하고는 있어요
  • Pyxis: https://github.com/google/dagger/blob/master/java/dagger/android/DaggerDialogFragment.java
  • Pyxis: ?
  • Pyxis: 잠만
  • Pyxis: 이 링크가 답이네요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 휴 오늘도 제가 Pyxis님 고민을 한건 해결해드렸군요
  • Zeallat: Pyxis님 저 없으면 어떻게 사시나요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 하하
  • Pyxis: 혼자 막 정리해보니
  • Pyxis: 답이 나옴
  • Zeallat: 그래서 답이뭔가요?
  • Zeallat: 그냥 저 링크식으로
  • Pyxis: 네
  • Zeallat: android injection
  • Zeallat: 하면되나여
  • Pyxis: onAttach에서
  • Pyxis: 해주면 되는거같아요]
  • Zeallat: onCreateView가 아니고
  • Zeallat: onAttatch가 해담인가요
  • Zeallat: ㅋㅋ
  • Pyxis: 뭐 그러면 복잡하게 할 필요 없이
  • Pyxis: DialogFragment에다가 InjectFragmnet달아놓고
  • Pyxis: onAttach에다가 걸면 되겠네여
  • Pyxis: 담 버전부터 대응해봐야될듯

Android 9.0 : Google Map, Volley 관련 이슈

지난주 금요일 (18일) 기준으로 Galaxy S8에 9.0 Beta가 시작되어 업데이트 하여 사용하고 있었으나, 구글 맵을 불러오면 아래 오류가 노출되면서 앱이 죽는 문제가 있었다.

2019-01-21 10:38:14.551 31596-31686/com.*** E/AndroidRuntime: FATAL EXCEPTION: Thread-7
    Process: com.***, PID: 31596
    java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/ProtocolVersion;
        at ez.b(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):3)
        at ey.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):3)
        at fa.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):15)
        at com.google.maps.api.android.lib6.drd.al.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):6)
        at ed.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):21)
        at ed.run(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):8)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "org.apache.http.ProtocolVersion" on path: DexPathList[[zip file "/data/user_de/0/com.google.android.gms/app_chimera/m/0000006a/MapsDynamite.apk"],nativeLibraryDirectories=[/data/user_de/0/com.google.android.gms/app_chimera/m/0000006a/MapsDynamite.apk!/lib/arm64-v8a, /system/lib64, /system/vendor/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at ad.loadClass(:com.google.android.gms.dynamite_dynamiteloader@14799083@14.7.99 (100408-223214910):4)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at ez.b(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):3) 
        at ey.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):3) 
        at fa.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):15) 
        at com.google.maps.api.android.lib6.drd.al.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):6) 
        at ed.a(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):21) 
        at ed.run(:com.google.android.gms.dynamite_mapsdynamite@14799083@14.7.99 (100408-223214910):8) 

이 문제인데, 이 문제에 대해 제기된 이슈가 구글쪽 이슈 트래커에 존재하나 wont fix라 적혀있었다.

즉, 이 행동은 정상 행동으로 AndroidManifest.xml 의 application 태그 내부에 아래 코드를 적어주면 된다는 것이다.

  <uses-library
      android:name="org.apache.http.legacy"
      android:required="false" />

다만, Apache Http가 Android 6.0 부터 삭제되었음에도 불구하고 아직까지 사용하는지는 아직도 의문이긴 하다.

추가 1. Volley

Google 의 Http 통신 라이브러리인 Volley도 같은 문제가 발생하는 것 같다. 마찬가지로 해결 방법은 위와 같다.

NaraeAudioRecorder, AudioRecorder for Android

회사에서 프로젝트를 진행하던 도중, 음성 녹음에 대한 기능을 개발할 일이 있었다.

예전(아마 2017년쯤)에 쓰던 lameMp3 wrapper가 있었기에 그대로 활용했었는데, 최신 환경에서는 녹음된 소리가 제대로 나오지 않고 이상하게 나오는 현상이 있었다.

그래서, 이번 주말동안 기존에 AudioRecord를 만졌던 기억을 되살려 음성 녹음 라이브러리를 만들게 되었고, 그것이 바로 WindSekirun/NaraeAudioRecorder 이다.

구조

크게 바라보면 해당 라이브러리는 총 4개의 파트로 구성된다.

  • AudioChunk: AudioRecord에서 bufferSize 만큼 읽어온 만큼의 raw 데이터와 raw 데이터에 대한 정보를 담고 있다.
  • AudioRecorder: Wav, PCM, FFmpeg 등 여러 포맷에 대한 객체로 start, stop, resume, pause 이벤트에 맞춰 작업을 실행한다.
  • AudioSource: 기본과 NoiseSuppressor (배경 노이즈를 제거하는 안드로이드 API 전처리기) 에 대한 기본 속성이며, AudioRecord에 대한 정보를 갖고 있다.
  • RecordWriter: AudioSource 와 AudioRecorder 사이에 있는 중간 매개체로 AudioSource로 설정한 AudioRecord에서 bufferSize만큼 raw data를 가져와 OutputStream에 쓰고, 그 이벤트를 AudioRecorder 에 전달하는 역할을 한다.

각각 유기적으로 연결되어있어, 처리 순서는 AudioRecorder > RecordWriter > AudioChunk 이고, AudioSource는 RecordWriter를 사용하기 위한 AudioRecord 설정 객체라 이해하면 편하다.

특이점

특이점이라 한다면 AudioRecord에서 가져오는 데이터는 raw 데이터이고, 이를 wav나 mp3 등으로 변환하기 위해서는 특별한 작업이 필요하다.

예전에도 AudioRecord to Wav 란 주제로 다룬 적이 있었고, 이번에도 특별한 작업은 없다.

mp3, aac, wma 로 변환하기 위해서 FFmpeg를 사용했는데, FFmpeg에 대한 command를 구성하도록 했다

val tempFile = File(file.parent, "tmp-${file.name}")
commandBuilder.addAll(listOf("-y", "-i", file.path))

if (convertConfig.samplingRate != FFmpegSamplingRate.ORIGINAL) {
     commandBuilder.addAll(listOf("-ar", convertConfig.samplingRate.samplingRate.toString()))
}

if (convertConfig.bitRate !== FFmpegBitRate.def) {
    commandBuilder.addAll(listOf("-sample_fmt", convertConfig.bitRate.bitRate))
}

if (convertConfig.mono) {
    commandBuilder.addAll(listOf("-ac", "1"))
}

commandBuilder.add(tempFile.path)

FFmpeg를 다뤄보는 것은 두번째이기 때문에, 다소 그렇게 삽질하지는 않은 것 같다. (아마도… (._.

물론 FFmpeg를 사용한다면 라이센스 문제가 나오기 때문에, 멀티 모듈로 구성해서 core만을 사용한다면 FFmpeg에 의존성이 없게 구성했다. 아무래도 FFmpeg를 프로덕션에서 바로 사용하기에는 부적절한 면도 있기도 하다.

마지막으로 조금 특이한 점이라면, 목적 파일의 확장자에 따라 wav, mp3 등 각각의 recorder로 넘기는데 이 과정에서 리플렉션을 통해 접근했다는 점이다.

class FFmpegRecordFinder : RecordFinder {

    /**
     * see [RecordFinder.find]
     */
    override fun find(extension: String, file: File, writer: RecordWriter): AudioRecorder {
        return when (extension) {
            "wav" -> WavAudioRecorder(file, writer)
            "pcm" -> PcmAudioRecorder(file, writer)
            "aac" -> FFmpegAudioRecorder(file, writer)
            "mp3" -> FFmpegAudioRecorder(file, writer)
            "m4a" -> FFmpegAudioRecorder(file, writer)
            "wma" -> FFmpegAudioRecorder(file, writer)
            "flac" -> FFmpegAudioRecorder(file, writer)
            else -> PcmAudioRecorder(file, writer)
        }
    }

}

RecordFinder 란 클래스에서 find 메서드를 통해 Recorder 로 넘기고, 이 RecordFinder를 Class 객체를 사용하여 바로 인스턴스를 만들고 활용하는 방법으로 접근했다.

 try {
            val finder = recordFinder.getConstructor().newInstance() as? RecordFinder
                    ?: throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR)

            val file = recorderConfig.destFile ?: return // it can't be null
            audioRecorder = finder.find(file.extension, file, recordWriter)
} catch (exception: Exception) {
            throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR, exception)
}

물론 해당 RecordFinder 클래스를 정확한 형식에 맞추지 않고 개발을 진행했을 경우 사용하지 못한다는 문제점이 있지만, 가능한 멀티모듈로 구성하면서도 유기적으로 연계되기 위해서 해당 방법을 사용했다. (적어도 개발자 자신이 확정할 수 있으면 괜찮지 않을까.. 라는 조금 어린 생각을 가지기도 했다.)

마무리

총 3일동안 개발하면서 나름대로 코드를 최대한 깔끔하게 짜고, 문서화도 제대로 하려고는 노력은 했지만 아직까지 표현력이 부족한 점도 많긴 많았다.

그래도 2019년 한 해의 시작을 라이브러리로 시작했으니, 이번 년도 말에도 라이브러리 개발로 끝날 것 같기도 하다.

마지막으로 개발했던 모든 소스와 샘플들은
WindSekirun/NaraeAudioRecorder 에 공개되어 있다. JCenter에도 배포되어있으니, 바로 다운 받아 사용이 가능하다.